openapi: 3.1.0
info:
  title: YesWeCheck API
  version: "2.0"
  description: |
    ## L'API de validation email ultra-précise

    **YesWeCheck** est une plateforme de validation email combinant vérifications syntaxiques,
    DNS, SMTP multi-pass et détection de risques pour qualifier vos listes email en temps réel ou en batch.

    ### Prérequis
    Créez un compte et générez une **clé API** depuis le [Dashboard](https://app.yeswecheck.fr) →
    *Settings* → *API Keys*.

    ### Authentification
    Deux méthodes sont supportées :
    - **X-API-Key** *(recommandé)* — pour les intégrations backend et widgets
    - **JWT Bearer** — access token obtenu via votre session Dashboard

    ### Base URL
    ```
    https://api.yeswecheck.fr/v2
    ```

    ### Crédits SMTP
    La vérification SMTP consomme **1 crédit** par email. Les checks syntaxe, DNS, disposable,
    role account, typo et suggestions sont **gratuits**.

    ### Rate Limits
    - **100 req/min** par IP sur `POST /email/validate`
    - Configurable par API Key depuis le Dashboard
  contact:
    name: Support YesWeCheck
    url: https://yeswecheck.fr
    email: contact@yeswecheck.fr
  license:
    name: Propriétaire
    url: https://yeswecheck.fr/legal

servers:
  - url: https://api.yeswecheck.fr/v2
    description: Production
  - url: http://localhost:3000/v2
    description: Développement local

security:
  - ApiKeyAuth: []
  - BearerAuth: []

tags:
  - name: Email
    description: |
      Validation et analyse d'adresses email. Le cœur de l'API.

      Vérifie la **syntaxe** (RFC 5322), les **DNS/MX**, détecte les adresses **jetables**,
      les **role accounts** (info@, admin@...), les **fautes de frappe** de domaine,
      les adresses **aléatoires générées**, et vérifie la boîte via **SMTP** (1 crédit).
  - name: Batch
    description: |
      Traitement massif de fichiers CSV. Upload asynchrone, suivi en temps réel,
      export CSV/XLSX/PDF. Supporte la vérification SMTP multi-pass (Pass 1 + Pass 2).
  - name: API Keys
    description: |
      Gestion programmatique de vos clés API. Listez, créez, configurez les features,
      révoquez et consultez les statistiques d'usage. Les clés peuvent aussi être gérées
      depuis le [Dashboard](https://app.yeswecheck.fr).
  - name: Credits
    description: |
      Consultation du solde de crédits SMTP de votre organisation.
      Les crédits s'achètent depuis le [Dashboard](https://app.yeswecheck.fr) → *Facturation*.
  - name: Auth
    description: |
      Renouvellement du token JWT. La connexion et la création de compte se font
      exclusivement via le [Dashboard](https://app.yeswecheck.fr).
  - name: Health
    description: Vérification de l'état de l'API (sans authentification).

paths:

  # ─── EMAIL VALIDATION ──────────────────────────────────────────────────────

  /email/validate:
    post:
      tags: [Email]
      summary: Valider une adresse email
      operationId: validateEmail
      description: |
        Endpoint principal. Effectue une cascade de vérifications :

        | Étape | Coût | Description |
        |---|---|---|
        | Syntaxe | Gratuit | Format RFC 5322, normalisation |
        | DNS / MX | Gratuit | Résolution DNS, enregistrements MX |
        | Disposable | Gratuit | Domaines jetables (10min mail, etc.) |
        | Role account | Gratuit | info@, admin@, support@, etc. |
        | Typo domain | Gratuit | gmial.com → gmail.com |
        | Random detection | Gratuit | x7k3p9@domain.com |
        | **SMTP** | **1 crédit** | Vérification réelle de la boîte |

        > **Note SMTP** : La vérification SMTP prend 1 à 15 secondes selon le serveur.
        > Ajustez `smtpTimeout` si nécessaire (défaut : 8000ms).
      security:
        - ApiKeyAuth: []
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ValidateEmailRequest'
            examples:
              sans_smtp:
                summary: Validation rapide (gratuite)
                value:
                  email: "john.doe@gmail.com"
              avec_smtp:
                summary: Validation complète avec SMTP
                value:
                  email: "john.doe@gmail.com"
                  smtp: true
                  smtpTimeout: 8000
              depuis_widget:
                summary: Depuis un widget embarqué
                value:
                  email: "user@example.com"
                  widgetId: "550e8400-e29b-41d4-a716-446655440000"
                  deviceId: "device-abc123"
      responses:
        "200":
          description: Résultat de la validation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationResult'
              examples:
                valid:
                  summary: Email valide avec SMTP
                  value:
                    email: "john.doe@gmail.com"
                    status: "valid"
                    score: 98
                    syntax:
                      valid: true
                      normalized: "john.doe@gmail.com"
                      localPart: "john.doe"
                      domain: "gmail.com"
                    dns:
                      hasMxRecords: true
                      mxRecords: ["gmail-smtp-in.l.google.com"]
                    disposable:
                      isDisposable: false
                    roleAccount:
                      isRoleAccount: false
                    typo:
                      hasTypo: false
                      suggestions: []
                    randomDetection:
                      isRandom: false
                      confidence: 0.02
                    smtp:
                      performed: true
                      deliverable: true
                      responseCode: 250
                      responseMessage: "OK"
                    metadata:
                      creditsUsed: 1
                      creditsRemaining: 499
                      processingTimeMs: 2341
                      smtpEnabled: true
                      apiVersion: "v2"
                invalid_jetable:
                  summary: Adresse jetable
                  value:
                    email: "test@10minutemail.com"
                    status: "invalid"
                    score: 0
                    disposable:
                      isDisposable: true
                      provider: "10minutemail.com"
                    smtp:
                      performed: false
                    metadata:
                      creditsUsed: 0
                      processingTimeMs: 145
                risky_role:
                  summary: Role account (risqué)
                  value:
                    email: "contact@entreprise.fr"
                    status: "risky"
                    score: 55
                    roleAccount:
                      isRoleAccount: true
                      reason: "Generic prefix: contact"
                    smtp:
                      performed: false
                    metadata:
                      creditsUsed: 0
                      processingTimeMs: 89
        "401":
          $ref: '#/components/responses/Unauthorized'
        "402":
          $ref: '#/components/responses/InsufficientCredits'
        "422":
          $ref: '#/components/responses/ValidationError'
        "429":
          $ref: '#/components/responses/RateLimitExceeded'

  /email/suggest/domain/countries:
    get:
      tags: [Email]
      summary: Pays supportés pour les suggestions de domaine
      operationId: getSuggestCountries
      description: Endpoint **public** — aucune authentification requise.
      security: []
      responses:
        "200":
          description: Liste des pays supportés
          content:
            application/json:
              schema:
                type: object
                properties:
                  countries:
                    type: array
                    items:
                      type: object
                      properties:
                        code: { type: string, example: "FR" }
                        name: { type: string, example: "France" }
              example:
                countries:
                  - { code: "GLOBAL", name: "Global" }
                  - { code: "FR", name: "France" }
                  - { code: "DE", name: "Germany" }
                  - { code: "UK", name: "United Kingdom" }
                  - { code: "IT", name: "Italy" }
                  - { code: "ES", name: "Spain" }
                  - { code: "US", name: "United States" }

  /email/suggest/domain:
    post:
      tags: [Email]
      summary: Suggestions de domaines email par pays
      operationId: suggestDomain
      description: |
        Retourne les domaines email populaires pour un pays. Idéal pour l'**autocomplétion**
        dans les formulaires (paramètre `partial` pour le typeahead).

        Endpoint **public** — aucune authentification requise.
      security: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SuggestDomainRequest'
            examples:
              france:
                summary: Top domaines en France
                value: { country: "FR", count: 10 }
              typeahead:
                summary: Autocomplétion sur "gm"
                value: { country: "FR", partial: "gm", count: 5 }
      responses:
        "200":
          description: Suggestions de domaines
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuggestDomainResponse'
              example:
                country: "FR"
                count: 5
                suggestions:
                  - { domain: "gmail.com", provider: "Google", type: "webmail", popularity: 15 }
                  - { domain: "outlook.fr", provider: "Microsoft", type: "webmail", popularity: 14 }
                  - { domain: "hotmail.fr", provider: "Microsoft", type: "webmail", popularity: 13 }
                total: 5

  /email/suggest/typo-domain:
    post:
      tags: [Email]
      summary: Détecter et corriger les fautes de frappe de domaine
      operationId: suggestTypoDomain
      description: |
        Détecte les fautes de frappe dans le domaine email et propose une correction.

        Exemples : `gmial.com` → `gmail.com`, `hotmial.fr` → `hotmail.fr`

        Endpoint **public** — aucune authentification requise.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email:
                  type: string
                  format: email
                  example: "user@gmial.com"
            example:
              email: "user@gmial.com"
      responses:
        "200":
          description: Résultat de détection de typo
          content:
            application/json:
              schema:
                type: object
                properties:
                  original: { type: string }
                  hasTypo: { type: boolean }
                  suggestion: { type: string, nullable: true }
                  confidence: { type: number, format: float }
              examples:
                found:
                  value: { original: "user@gmial.com", hasTypo: true, suggestion: "user@gmail.com", confidence: 0.97 }
                not_found:
                  value: { original: "user@gmail.com", hasTypo: false, suggestion: null, confidence: 1.0 }

  # ─── BATCH ─────────────────────────────────────────────────────────────────

  /batch/parse:
    post:
      tags: [Batch]
      summary: Parser un fichier CSV (aperçu avant traitement)
      operationId: parseBatch
      description: |
        Analyse un fichier sans le traiter. Détecte automatiquement le séparateur,
        les headers et la colonne email.

        **Formats** : `.csv`, `.txt`, `.xlsx` — max 10 Mo
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file:
                  type: string
                  format: binary
      responses:
        "200":
          description: Aperçu du fichier parsé
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ParseResult'
              example:
                headers: ["id", "prenom", "nom", "email", "ville"]
                detectedEmailColumn: "email"
                rowCount: 15420
                separator: ","
                preview:
                  - { id: "1", email: "jean.dupont@gmail.com" }
                  - { id: "2", email: "mmartin@outlook.fr" }
        "400":
          $ref: '#/components/responses/BadRequest'
        "401":
          $ref: '#/components/responses/Unauthorized'

  /batch/upload:
    post:
      tags: [Batch]
      summary: Lancer un job de validation batch
      operationId: uploadBatch
      description: |
        Upload un fichier CSV et démarre la validation asynchrone.
        Un **seul job actif** par organisation est autorisé simultanément.

        **Phases de traitement avec SMTP** :
        1. `syntax_dns` — Syntaxe + DNS (rapide)
        2. `smtp_verification` — SMTP Pass 1 (lent, groupé par serveur MX)
        3. `smtp_retry` *(si `pass2Enabled: true`)* — Re-vérification des Unknown
        4. `finalizing` — Consolidation des résultats

        Suivez la progression avec `GET /batch/{jobId}/status`.
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/UploadBatchRequest'
      responses:
        "201":
          description: Job créé
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BatchJobCreated'
        "401":
          $ref: '#/components/responses/Unauthorized'
        "409":
          description: Un job est déjà en cours
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                message: "A batch job is already running for this organization"
                code: "ACTIVE_JOB_EXISTS"
                statusCode: 409

  /batch/active-job:
    get:
      tags: [Batch]
      summary: Récupérer le job actif de l'organisation
      operationId: getActiveJob
      security:
        - BearerAuth: []
      responses:
        "200":
          description: Job actif ou null
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/BatchJobStatus'
                  - type: "null"

  /batch/jobs:
    get:
      tags: [Batch]
      summary: Lister les jobs batch (filtres + pagination)
      operationId: listBatchJobs
      security:
        - BearerAuth: []
      parameters:
        - name: page
          in: query
          schema: { type: integer, default: 1, minimum: 1 }
        - name: limit
          in: query
          schema: { type: integer, default: 20, maximum: 100 }
        - name: status
          in: query
          schema:
            type: string
            enum: [pending, processing, completed, failed, cancelled, paused]
        - name: search
          in: query
          schema: { type: string }
        - name: dateFrom
          in: query
          schema: { type: string, format: date-time }
        - name: dateTo
          in: query
          schema: { type: string, format: date-time }
      responses:
        "200":
          description: Liste paginée
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/BatchJobSummary'
                  total: { type: integer }
                  page: { type: integer }
                  limit: { type: integer }
                  totalPages: { type: integer }

  /batch/{jobId}/status:
    get:
      tags: [Batch]
      summary: Statut détaillé d'un job
      operationId: getBatchJobStatus
      description: Sondez toutes les **2-5 secondes** pour suivre la progression.
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/JobId'
      responses:
        "200":
          description: Statut du job
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BatchJobStatus'
              example:
                jobId: "550e8400-e29b-41d4-a716-446655440000"
                status: "processing"
                progress:
                  processed: 7210
                  total: 15420
                  valid: 5834
                  invalid: 1089
                  risky: 287
                  unknown: 0
                  percentage: 47
                currentPhase: "smtp_verification"
                estimatedTimeRemaining: "8m30s"
        "403":
          $ref: '#/components/responses/Forbidden'
        "404":
          $ref: '#/components/responses/NotFound'

  /batch/{jobId}/cancel:
    post:
      tags: [Batch]
      summary: Annuler un job batch
      operationId: cancelBatchJob
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/JobId'
      responses:
        "200":
          description: Job annulé

  /batch/{jobId}/pause:
    post:
      tags: [Batch]
      summary: Mettre en pause un job batch
      operationId: pauseBatchJob
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/JobId'
      responses:
        "200":
          description: Job mis en pause

  /batch/{jobId}/resume:
    post:
      tags: [Batch]
      summary: Reprendre un job en pause
      operationId: resumeBatchJob
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/JobId'
      responses:
        "200":
          description: Job repris

  /batch/{jobId}/export/prepare:
    post:
      tags: [Batch]
      summary: Préparer un export CSV ou XLSX
      operationId: prepareBatchExport
      description: |
        Lance la génération en arrière-plan. Un lien de téléchargement (valide **48h**)
        est envoyé par email. Vérifiez la disponibilité avec `GET /batch/{jobId}/export/status`.
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/JobId'
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                format:
                  type: string
                  enum: [csv, xlsx]
                  default: csv
            example: { format: "csv" }
      responses:
        "200":
          description: Export en préparation
          content:
            application/json:
              schema:
                type: object
                properties:
                  exportId: { type: string }
                  message: { type: string }

  /batch/{jobId}/export/status:
    get:
      tags: [Batch]
      summary: Vérifier si un export est prêt
      operationId: getBatchExportStatus
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/JobId'
        - name: format
          in: query
          schema: { type: string, enum: [csv, xlsx], default: csv }
      responses:
        "200":
          description: Statut de l'export
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [pending, processing, ready, failed] }
                  downloadUrl: { type: string, nullable: true }
                  expiresAt: { type: string, format: date-time, nullable: true }

  /batch/{jobId}/download-pdf:
    get:
      tags: [Batch]
      summary: Télécharger le rapport PDF
      operationId: downloadBatchPdf
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/JobId'
      responses:
        "200":
          description: Rapport PDF
          content:
            application/pdf:
              schema:
                type: string
                format: binary

  /batch/{jobId}:
    delete:
      tags: [Batch]
      summary: Supprimer un job terminé
      operationId: deleteBatchJob
      description: Seuls les jobs `completed`, `failed` ou `cancelled` peuvent être supprimés.
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/JobId'
      responses:
        "200":
          description: Job supprimé
        "400":
          $ref: '#/components/responses/BadRequest'

  # ─── API KEYS ──────────────────────────────────────────────────────────────

  /api-keys:
    get:
      tags: [API Keys]
      summary: Lister les clés API
      operationId: listApiKeys
      security:
        - BearerAuth: []
      responses:
        "200":
          description: Liste des clés API
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/ApiKey'

    post:
      tags: [API Keys]
      summary: Créer une clé API
      operationId: createApiKey
      description: |
        La valeur complète de la clé (`key`) est retournée **uniquement à la création**.
        Stockez-la immédiatement dans vos variables d'environnement.

        > **Rôle requis** : `owner` ou `admin`
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateApiKeyRequest'
            examples:
              widget:
                summary: Clé pour widget JS
                value:
                  name: "Widget Inscription"
                  allowedDomains: ["monsite.fr"]
                  config: { smtp: false, disposable: true }
              backend:
                summary: Clé pour backend
                value:
                  name: "Production Backend"
                  allowedDomains: []
                  config: { smtp: true, enrichment: true }
      responses:
        "201":
          description: Clé API créée
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyCreated'
        "403":
          $ref: '#/components/responses/Forbidden'

  /api-keys/{id}:
    get:
      tags: [API Keys]
      summary: Récupérer une clé API
      operationId: getApiKey
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/ApiKeyId'
      responses:
        "200":
          description: Détails de la clé
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKey'

    patch:
      tags: [API Keys]
      summary: Modifier une clé API
      operationId: updateApiKey
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/ApiKeyId'
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateApiKeyRequest'
      responses:
        "200":
          description: Clé mise à jour

    delete:
      tags: [API Keys]
      summary: Supprimer définitivement une clé API
      operationId: deleteApiKey
      description: Action irréversible. Préférez `revoke` pour une désactivation temporaire.
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/ApiKeyId'
      responses:
        "200":
          description: Clé supprimée

  /api-keys/{id}/stats:
    get:
      tags: [API Keys]
      summary: Statistiques d'utilisation
      operationId: getApiKeyStats
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/ApiKeyId'
        - name: days
          in: query
          schema: { type: integer, default: 7, minimum: 1, maximum: 90 }
      responses:
        "200":
          description: Statistiques d'usage

  /api-keys/{id}/revoke:
    post:
      tags: [API Keys]
      summary: Révoquer une clé API
      operationId: revokeApiKey
      description: Désactive la clé immédiatement. Réversible via `/reactivate`.
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/ApiKeyId'
      responses:
        "200":
          description: Clé révoquée

  /api-keys/{id}/reactivate:
    post:
      tags: [API Keys]
      summary: Réactiver une clé révoquée
      operationId: reactivateApiKey
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/ApiKeyId'
      responses:
        "200":
          description: Clé réactivée

  /api-keys/rate-limits:
    get:
      tags: [API Keys]
      summary: Consulter les rate limits de l'organisation
      operationId: getApiKeyRateLimits
      security:
        - BearerAuth: []
      responses:
        "200":
          description: Rate limits configurés

  # ─── CREDITS ───────────────────────────────────────────────────────────────

  /credits/balance:
    get:
      tags: [Credits]
      summary: Solde de crédits SMTP
      operationId: getCreditsBalance
      security:
        - BearerAuth: []
        - ApiKeyAuth: []
      responses:
        "200":
          description: Solde actuel
          content:
            application/json:
              schema:
                type: object
                properties:
                  balance: { type: integer, example: 4820 }
                  organizationId: { type: string, format: uuid }
              example:
                balance: 4820
                organizationId: "550e8400-e29b-41d4-a716-446655440000"

  /credits/check/{amount}:
    get:
      tags: [Credits]
      summary: Vérifier si le solde est suffisant
      operationId: checkCredits
      security:
        - BearerAuth: []
        - ApiKeyAuth: []
      parameters:
        - name: amount
          in: path
          required: true
          schema: { type: integer, minimum: 1 }
          example: 1000
      responses:
        "200":
          description: Résultat
          content:
            application/json:
              schema:
                type: object
                properties:
                  hasEnoughCredits: { type: boolean }
                  requiredAmount: { type: integer }

  # ─── AUTH (renouvellement token uniquement) ────────────────────────────────

  /auth/refresh:
    post:
      tags: [Auth]
      summary: Renouveler le token JWT
      operationId: refreshToken
      description: |
        Renouvelle un `accessToken` expiré (durée de vie : 15 min) à partir du `refreshToken`.

        > Pour les intégrations backend, préférez les **clés API** (pas d'expiration).
        > La connexion et la gestion de session se font via le [Dashboard](https://app.yeswecheck.fr).
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [refreshToken]
              properties:
                refreshToken: { type: string }
            example:
              refreshToken: "dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4..."
      responses:
        "200":
          description: Nouveaux tokens
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthTokens'
              example:
                accessToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
                refreshToken: "dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4..."
                expiresIn: 900
        "401":
          $ref: '#/components/responses/Unauthorized'

  # ─── HEALTH ────────────────────────────────────────────────────────────────

  /health:
    get:
      tags: [Health]
      summary: État de l'API
      operationId: getHealth
      description: Vérifiez que l'API est opérationnelle. Aucune authentification requise.
      security: []
      responses:
        "200":
          description: API opérationnelle
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, example: "healthy" }
                  timestamp: { type: string, format: date-time }
                  version: { type: string, example: "v2" }
              example:
                status: "healthy"
                timestamp: "2026-02-25T10:00:00.000Z"
                version: "v2"

# ─── COMPONENTS ────────────────────────────────────────────────────────────────

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: |
        **Méthode recommandée** pour les intégrations backend et widgets.

        Créez vos clés depuis le [Dashboard](https://app.yeswecheck.fr) → *Settings* → *API Keys*.

        Format : `X-API-Key: ywc_live_xxxxxxxxxxxxx`
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: |
        Access token JWT obtenu via votre session Dashboard.
        Durée de validité : **15 minutes** — renouvelable via `POST /auth/refresh`.

        Format : `Authorization: Bearer eyJhbGciOiJIUzI1NiIs...`

  parameters:
    JobId:
      name: jobId
      in: path
      required: true
      schema: { type: string, format: uuid }
      description: Identifiant unique du job batch
      example: "550e8400-e29b-41d4-a716-446655440000"
    ApiKeyId:
      name: id
      in: path
      required: true
      schema: { type: string }
      description: Identifiant de la clé API

  responses:
    Unauthorized:
      description: Non authentifié — clé API ou token manquant/invalide
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            message: "Unauthorized"
            statusCode: 401
    Forbidden:
      description: Permissions insuffisantes (rôle owner/admin requis)
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            message: "Only owners and admins can create API keys"
            statusCode: 403
    NotFound:
      description: Ressource introuvable
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            message: "Resource not found"
            statusCode: 404
    BadRequest:
      description: Paramètres invalides ou manquants
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    ValidationError:
      description: Erreur de validation du corps de la requête
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ValidationError'
          example:
            message: ["email must be an email"]
            error: "Unprocessable Entity"
            statusCode: 422
    InsufficientCredits:
      description: Crédits SMTP insuffisants
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            message: "Insufficient credits for SMTP validation"
            code: "INSUFFICIENT_CREDITS"
            statusCode: 402
    RateLimitExceeded:
      description: Trop de requêtes — patienter Retry-After secondes
      headers:
        Retry-After:
          schema: { type: integer }
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            message: "Too many requests from this IP, please try again later"
            statusCode: 429

  schemas:

    ValidateEmailRequest:
      type: object
      required: [email]
      properties:
        email:
          type: string
          format: email
          example: "john.doe@gmail.com"
        smtp:
          type: boolean
          default: false
          description: Activer la vérification SMTP réelle (**1 crédit**)
        smtpTimeout:
          type: integer
          minimum: 1000
          maximum: 30000
          default: 8000
          description: Timeout SMTP en millisecondes
        enrichment:
          type: boolean
          default: false
          description: Activer l'enrichissement (prénom probable)
        widgetId:
          type: string
          format: uuid
          description: ID du widget JS (appels depuis un formulaire embarqué)
        deviceId:
          type: string
          maxLength: 128
          description: Identifiant de l'appareil (protection anti-abus widget)

    ValidationResult:
      type: object
      properties:
        email: { type: string }
        status:
          type: string
          enum: [valid, invalid, risky, unknown]
          description: |
            - `valid` — Email valide et délivrable
            - `invalid` — Email invalide (syntaxe, domaine inexistant, SMTP rejeté)
            - `risky` — Email risqué (jetable, role account, aléatoire)
            - `unknown` — Impossible de vérifier (timeout SMTP, greylisting)
        score:
          type: integer
          minimum: 0
          maximum: 100
          description: Score de qualité (0 = invalide, 100 = parfait)
        syntax:
          $ref: '#/components/schemas/SyntaxResult'
        dns:
          $ref: '#/components/schemas/DnsResult'
        disposable:
          $ref: '#/components/schemas/DisposableResult'
        roleAccount:
          $ref: '#/components/schemas/RoleAccountResult'
        typo:
          $ref: '#/components/schemas/TypoResult'
        randomDetection:
          $ref: '#/components/schemas/RandomDetectionResult'
        smtp:
          $ref: '#/components/schemas/SmtpResult'
        enrichment:
          $ref: '#/components/schemas/EnrichmentResult'
        metadata:
          $ref: '#/components/schemas/ValidationMetadata'

    SyntaxResult:
      type: object
      properties:
        valid: { type: boolean }
        normalized: { type: string, example: "john.doe@gmail.com" }
        localPart: { type: string, example: "john.doe" }
        domain: { type: string, example: "gmail.com" }

    DnsResult:
      type: object
      properties:
        hasMxRecords: { type: boolean }
        mxRecords:
          type: array
          items: { type: string }
          example: ["gmail-smtp-in.l.google.com"]
        hasARecord: { type: boolean }

    DisposableResult:
      type: object
      properties:
        isDisposable: { type: boolean }
        provider: { type: string, nullable: true, example: "10minutemail.com" }

    RoleAccountResult:
      type: object
      properties:
        isRoleAccount: { type: boolean }
        reason: { type: string, nullable: true, example: "Generic prefix: info" }

    TypoResult:
      type: object
      properties:
        hasTypo: { type: boolean }
        suggestions:
          type: array
          items: { type: string }
          example: ["john.doe@gmail.com"]

    RandomDetectionResult:
      type: object
      properties:
        isRandom: { type: boolean }
        confidence: { type: number, format: float, example: 0.05 }

    SmtpResult:
      type: object
      properties:
        performed: { type: boolean }
        deliverable: { type: boolean, nullable: true }
        responseCode: { type: integer, nullable: true, example: 250 }
        responseMessage: { type: string, nullable: true, example: "OK" }
        greylistedDetected: { type: boolean, nullable: true }
        catchAllDetected: { type: boolean, nullable: true }

    EnrichmentResult:
      type: object
      nullable: true
      properties:
        guessedFirstname: { type: string, nullable: true, example: "John" }
        genderConfidence: { type: number, nullable: true }

    ValidationMetadata:
      type: object
      properties:
        creditsUsed: { type: integer, example: 1 }
        creditsRemaining: { type: integer, example: 499 }
        processingTimeMs: { type: integer, example: 2341 }
        smtpEnabled: { type: boolean }
        apiVersion: { type: string, example: "v2" }

    SuggestDomainRequest:
      type: object
      properties:
        country:
          type: string
          example: "FR"
          description: "Code pays : GLOBAL, FR, DE, UK, IT, ES, US, BE, NL, CH, PT, CA, AT, PL"
        count:
          type: integer
          default: 15
          example: 10
        type:
          type: string
          enum: [webmail, isp, professional, secure]
        partial:
          type: string
          example: "gm"
          description: Texte partiel pour l'autocomplétion

    SuggestDomainResponse:
      type: object
      properties:
        country: { type: string }
        count: { type: integer }
        type: { type: string, nullable: true }
        partial: { type: string, nullable: true }
        suggestions:
          type: array
          items:
            type: object
            properties:
              domain: { type: string }
              provider: { type: string }
              type: { type: string }
              popularity: { type: integer }
        total: { type: integer }

    UploadBatchRequest:
      type: object
      required: [file]
      properties:
        file:
          type: string
          format: binary
          description: CSV, TXT ou XLSX — max 10 Mo, ~200k emails
        smtp:
          type: boolean
          default: false
          description: Vérification SMTP (1 crédit/email)
        smtpTimeout:
          type: integer
          default: 8000
        deduplicate:
          type: boolean
          default: true
          description: Supprimer les doublons avant traitement
        pass2Enabled:
          type: boolean
          default: false
          description: Activer le re-check SMTP des Unknown (améliore les résultats)
        label:
          type: string
          description: Libellé du job

    ParseResult:
      type: object
      properties:
        headers:
          type: array
          items: { type: string }
        detectedEmailColumn: { type: string, nullable: true }
        rowCount: { type: integer }
        preview:
          type: array
          items: { type: object }
        separator: { type: string }

    BatchJobCreated:
      type: object
      properties:
        jobId: { type: string, format: uuid }
        status: { type: string, enum: [pending] }
        totalEmails: { type: integer }
        fileName: { type: string }
        label: { type: string, nullable: true }
        createdAt: { type: string, format: date-time }

    BatchJobSummary:
      type: object
      properties:
        id: { type: string, format: uuid }
        status: { type: string, enum: [pending, processing, completed, failed, cancelled, paused] }
        fileName: { type: string }
        label: { type: string, nullable: true }
        totalEmails: { type: integer }
        processedEmails: { type: integer }
        percentage: { type: integer }
        createdAt: { type: string, format: date-time }
        completedAt: { type: string, format: date-time, nullable: true }

    BatchJobStatus:
      type: object
      properties:
        jobId: { type: string, format: uuid }
        status: { type: string, enum: [pending, processing, completed, failed, cancelled, paused] }
        fileName: { type: string }
        label: { type: string, nullable: true }
        progress:
          type: object
          properties:
            processed: { type: integer }
            total: { type: integer }
            valid: { type: integer }
            invalid: { type: integer }
            unknown: { type: integer }
            risky: { type: integer }
            percentage: { type: integer }
        smtp:
          type: object
          properties:
            enabled: { type: boolean }
            downgraded: { type: boolean, nullable: true }
        multiPass:
          type: object
          properties:
            pass2Enabled: { type: boolean }
            pass0Done: { type: boolean }
            pass1Done: { type: boolean }
            pass2Done: { type: boolean }
            finalized: { type: boolean }
            smtpEligibleCount: { type: integer }
            pass1Count: { type: integer }
            pass2Count: { type: integer }
        currentPhase:
          type: string
          enum: [syntax_dns, smtp_verification, smtp_retry, finalizing, completed]
        createdAt: { type: string, format: date-time }
        startedAt: { type: string, format: date-time, nullable: true }
        completedAt: { type: string, format: date-time, nullable: true }
        estimatedTimeRemaining: { type: string, nullable: true, example: "4m30s" }

    ApiKey:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        prefix: { type: string, example: "ywc_live_abc1..." }
        status: { type: string, enum: [active, revoked] }
        allowedDomains:
          type: array
          items: { type: string }
          description: Domaines autorisés (vide = tous)
        config:
          type: object
          properties:
            smtp: { type: boolean }
            disposable: { type: boolean }
            roleAccount: { type: boolean }
            enrichment: { type: boolean }
            randomDetection: { type: boolean }
            profanity: { type: boolean }
        lastUsedAt: { type: string, format: date-time, nullable: true }
        createdAt: { type: string, format: date-time }

    ApiKeyCreated:
      allOf:
        - $ref: '#/components/schemas/ApiKey'
        - type: object
          properties:
            key:
              type: string
              description: "⚠️ Affiché une seule fois. Stockez immédiatement."
              example: "ywc_live_abc123xyz789..."

    CreateApiKeyRequest:
      type: object
      required: [name]
      properties:
        name: { type: string, example: "Production Backend" }
        allowedDomains:
          type: array
          items: { type: string }
          description: Domaines autorisés (obligatoire pour les widgets JS)
          example: ["monsite.fr"]
        config:
          type: object
          properties:
            smtp: { type: boolean, default: false }
            disposable: { type: boolean, default: true }
            roleAccount: { type: boolean, default: true }
            enrichment: { type: boolean, default: false }
            randomDetection: { type: boolean, default: true }
            profanity: { type: boolean, default: true }

    UpdateApiKeyRequest:
      type: object
      properties:
        name: { type: string }
        allowedDomains: { type: array, items: { type: string } }
        config: { type: object }

    AuthTokens:
      type: object
      properties:
        accessToken: { type: string }
        refreshToken: { type: string }
        expiresIn: { type: integer, example: 900, description: "Validité en secondes (15 min)" }

    Error:
      type: object
      properties:
        message: { type: string }
        code: { type: string, nullable: true }
        statusCode: { type: integer }

    ValidationError:
      type: object
      properties:
        message:
          type: array
          items: { type: string }
        error: { type: string }
        statusCode: { type: integer }
