
openapi: 3.0.3
info:
  title: BillionVerify API
  description: |
    BillionVerify provides a powerful email verification API for validating email addresses in real-time.

    ## Authentication

    All API endpoints require authentication using an API key. Include your API key in the request header:

    ```
    BILLIONVERIFY-API-KEY: your_api_key_here
    ```

    The following header names are all accepted, in priority order:
    - `BV-API-KEY` *(recommended)*
    - `BILLIONVERIFY-API-KEY`
    - `Authorization: Bearer {api_key}`

    ## Rate Limits

    | Endpoint | Rate Limit |
    |----------|------------|
    | Single Verification (`/verify/single`) | 6,000 requests/minute |
    | Batch Verification (`/verify/bulk`) | 1,500 requests/minute |
    | File Upload (`/verify/file`) | 300 requests/minute |
    | Other endpoints | 200 requests/minute |

    ## Credits & Billing

    - **Unknown** status: 0 credits (we could not reach a deterministic conclusion, so you are not charged)
    - All other statuses (valid, invalid, risky, disposable, catchall, role): 1 credit each

    `invalid` is a deterministic result — we confirmed the address is not deliverable (SMTP 550, missing MX, mailbox not found, etc.) — and is just as valuable for list cleaning as `valid`, so it is billed the same.

    ## Base URL

    Production: `https://api.billionverify.com`
  version: 1.0.0
  contact:
    name: BillionVerify Support
    email: support@billionverify.com
    url: https://billionverify.com
  license:
    name: Proprietary
    url: https://billionverify.com/terms

servers:
  - url: https://api.billionverify.com
    description: Production server

tags:
  - name: Verification
    description: Email verification endpoints
  - name: File Processing
    description: Batch file upload and processing endpoints
  - name: Account
    description: Account and credits management
  - name: Webhooks
    description: Webhook management endpoints
  - name: Health
    description: Health check endpoints

security:
  - ApiKeyAuth: []

paths:
  /v1/verify/single:
    post:
      tags:
        - Verification
      summary: Verify single email
      description: |
        Verify a single email address in real-time. Returns detailed verification results including
        deliverability status, domain information, and risk assessment.
      operationId: verifySingleEmail
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SingleVerifyRequest'
            example:
              email: "user@example.com"
              force_refresh: false
      responses:
        '200':
          description: Verification successful
          content:
            application/json:
              schema:
                # 2026-05-09 末轮自审 (analogous to §14 Finding 8): typed
                # single response so SDK sees verification_mode / retryable.
                $ref: '#/components/schemas/SingleVerifyApiResponse'
              example:
                success: true
                code: "0"
                message: "Success"
                data:
                  email: "user@example.com"
                  status: "valid"
                  score: 0.95
                  is_deliverable: true
                  is_disposable: false
                  is_catchall: false
                  is_role: false
                  is_free: false
                  domain: "example.com"
                  domain_age: 10
                  mx_records: ["mail.example.com"]
                  check_smtp: true
                  reason: "smtp_deliverable"
                  response_time: 250
                  credits_used: 1
                  verification_mode: "smtp"
                  retryable: false
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/InsufficientCredits'
        '429':
          $ref: '#/components/responses/RateLimitExceeded'

  /v1/verify/bulk:
    post:
      tags:
        - Verification
      summary: Verify multiple emails (batch)
      description: |
        Verify multiple email addresses in a single synchronous request.
        **Maximum 50 emails per request.** Submitting more than 50 returns
        HTTP 400 with `code=3103` (`Batch size limit exceeded`); use
        `/v1/verify/file` for larger lists. The endpoint never auto-promotes
        oversized batches into a file/River job.
      operationId: verifyBatchEmails
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BatchVerifyRequest'
            example:
              emails:
                - "user1@example.com"
                - "user2@example.com"
                - "user3@example.com"
      responses:
        '200':
          description: Batch verification successful
          content:
            application/json:
              schema:
                # 2026-05-09 §14 Finding 8: typed response so SDK generators
                # see data.deliverability_buckets in the contract.
                $ref: '#/components/schemas/BulkVerifyApiResponse'
              example:
                success: true
                code: "0"
                message: "Success"
                data:
                  results:
                    - email: "user1@example.com"
                      status: "valid"
                      score: 0.95
                      is_deliverable: true
                      credits_used: 1
                      verification_mode: "smtp"
                      retryable: false
                    - email: "user2@example.com"
                      status: "invalid"
                      score: 0.0
                      is_deliverable: false
                      credits_used: 1
                      verification_mode: "syntax_only"
                      retryable: false
                  total_emails: 2
                  valid_emails: 1
                  invalid_emails: 1
                  credits_used: 2
                  process_time: 1500
                  deliverability_buckets:
                    strict_deliverable: 1
                    usable_with_catchall: 1
                    not_deliverable: 1
                    unknown: 0
                    anti_verified: 0
                    transient_failure: 0
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/InsufficientCredits'
        '429':
          $ref: '#/components/responses/RateLimitExceeded'

  /v1/verify/disposable:
    post:
      tags:
        - Verification
      summary: Disposable-only check (free)
      description: |
        Pure in-memory lookup against the embedded disposable-domain table.
        Does NOT consume credits, does NOT call SMTP / MX / cache. Every
        call writes one row to `email_verification` history with
        `metadata.needs_charging=false` + `metadata.filtered_at="api_gateway"`,
        so the call is auditable but never billed.

        See `docs/disposable/disposable.md` for the full design.
      operationId: checkDisposable
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email:
                  type: string
                  format: email
                  example: "user@mailinator.com"
      responses:
        '200':
          description: Disposable check completed
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  code: { type: string, example: "0" }
                  message: { type: string, example: "Success" }
                  data:
                    type: object
                    properties:
                      email: { type: string, example: "user@mailinator.com" }
                      domain: { type: string, example: "mailinator.com" }
                      is_disposable: { type: boolean, example: true }
                      checked_at: { type: string, format: date-time }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimitExceeded'
    get:
      tags:
        - Verification
      summary: Disposable-only check (query string variant)
      description: |
        Same semantics as `POST /v1/verify/disposable` but accepts the email
        in the query string for easy form-validation usage from the browser.
      operationId: checkDisposableViaQuery
      parameters:
        - in: query
          name: email
          required: true
          schema:
            type: string
            format: email
          example: "user@mailinator.com"
      responses:
        '200':
          description: Disposable check completed
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  code: { type: string, example: "0" }
                  message: { type: string, example: "Success" }
                  data:
                    type: object
                    properties:
                      email: { type: string, example: "user@mailinator.com" }
                      domain: { type: string, example: "mailinator.com" }
                      is_disposable: { type: boolean, example: true }
                      checked_at: { type: string, format: date-time }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimitExceeded'

  /v1/verify/file:
    post:
      tags:
        - File Processing
      summary: Upload file for verification
      description: |
        Upload a file containing email addresses for asynchronous batch verification.

        **Supported formats:**
        - CSV (.csv)
        - Excel (.xlsx, .xls)
        - Text (.txt) - one email per line

        **Limits:**
        - Maximum file size: 20MB
        - Maximum emails: 100,000 per file
      operationId: uploadFileForVerification
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/FileUploadRequest'
      responses:
        '200':
          description: File uploaded successfully
          content:
            application/json:
              schema:
                # 2026-05-09 F26: typed response so SDK generators see
                # data.task_id / data.status_url / data.estimated_count / etc.
                $ref: '#/components/schemas/FileUploadApiResponse'
              example:
                success: true
                code: "0"
                message: "Success"
                data:
                  task_id: "7143874e-21c5-43c1-96f3-2b52ea41ae6a"
                  status: "pending"
                  message: "File uploaded successfully, processing started"
                  file_name: "emails.csv"
                  file_size: 45678
                  estimated_count: 1000
                  unique_emails: 950
                  total_rows: 1000
                  email_column: "email"
                  status_url: "/v1/verify/file/7143874e-21c5-43c1-96f3-2b52ea41ae6a"
                  created_at: "2026-02-04T10:30:00Z"
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/InsufficientCredits'
        '413':
          description: File too large
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /v1/verify/file/{job_id}:
    get:
      tags:
        - File Processing
      summary: Get file verification job status
      description: |
        Check the status and progress of a file verification job.
        Supports long-polling with the `timeout` parameter.
        File job statuses are exhaustively enumerated as
        `preparing`, `pending`, `running`, `processing`, `merging`, `paused`,
        `completed`, `completed_with_warning`, `failed`, and `cancelled`.
      operationId: getFileJobStatus
      parameters:
        - name: job_id
          in: path
          required: true
          description: The job ID returned from file upload
          schema:
            type: string
            format: uuid
        - name: timeout
          in: query
          required: false
          description: Long-polling timeout in seconds (0-300). If set, the request will wait until job completes or timeout.
          schema:
            type: integer
            minimum: 0
            maximum: 300
            default: 0
      responses:
        '200':
          description: Job status retrieved
          content:
            application/json:
              schema:
                # 2026-05-09 §14 Finding 8: typed response so SDK generators
                # see data.deliverability_buckets in the contract.
                $ref: '#/components/schemas/FileVerifyTaskStatusApiResponse'
              example:
                success: true
                code: "0"
                message: "Success"
                data:
                  task_id: "7143874e-21c5-43c1-96f3-2b52ea41ae6a"
                  status: "completed"
                  progress: 100
                  email_progress: 100
                  chunk_progress: 100
                  progress_source: "email"
                  total_emails: 1000
                  processed_emails: 1000
                  valid_emails: 850
                  invalid_emails: 100
                  unknown_emails: 30
                  role_emails: 15
                  catchall_emails: 5
                  risky_emails: 0
                  disposable_emails: 0
                  credits_used: 970
                  runtime_hints:
                    has_live_execution: true
                    inflight_batches: 4
                    last_execution_fact_at: "2026-02-04T10:31:58Z"
                  download_url: "https://api.billionverify.com/v1/verify/file/7143874e-21c5-43c1-96f3-2b52ea41ae6a/results"
                  direct_download_url: "https://download.example.com/results_7143874e.csv?signature=..."
                  direct_download_expires_at: "2026-02-04T11:32:05Z"
                  result_download_available: true
                  result_download_expires_at: "2026-02-11T10:32:05Z"
                  result_download_unavailable_reason: ""
                  started_at: "2026-02-04T10:30:00Z"
                  completed_at: "2026-02-04T10:32:05Z"
                  can_pause: false
                  can_resume: false
                  can_restart: false
                  restart_attempts: 0
                  current_attempt: 1
                  deliverability_buckets:
                    strict_deliverable: 865
                    usable_with_catchall: 870
                    not_deliverable: 100
                    unknown: 30
                    anti_verified: 12
                    transient_failure: 4
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /v1/verify/file/{job_id}/results:
    get:
      tags:
        - File Processing
      summary: Download verification results
      description: |
        Download the verification results for a completed job.

        **Without filters:** Returns redirect (HTTP 307) to the full result file.

        **With filters:** Returns a CSV containing only matching emails.

        Multiple filters can be combined (OR logic).
      operationId: downloadFileResults
      parameters:
        - name: job_id
          in: path
          required: true
          description: The job ID
          schema:
            type: string
            format: uuid
        - name: valid
          in: query
          description: Include valid emails
          schema:
            type: boolean
        - name: invalid
          in: query
          description: Include invalid emails
          schema:
            type: boolean
        - name: catchall
          in: query
          description: Include catch-all emails
          schema:
            type: boolean
        - name: role
          in: query
          description: Include role emails
          schema:
            type: boolean
        - name: unknown
          in: query
          description: Include unknown emails
          schema:
            type: boolean
        - name: disposable
          in: query
          description: Include disposable emails
          schema:
            type: boolean
        - name: risky
          in: query
          description: Include risky emails
          schema:
            type: boolean
      responses:
        '200':
          description: CSV file with filtered results
          content:
            text/csv:
              schema:
                type: string
                format: binary
        '307':
          description: Redirect to full result file (when no filters applied)
          headers:
            Location:
              description: URL to download the result file
              schema:
                type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /v1/jobs/{job_id}/resume:
    post:
      tags:
        - File Processing
      summary: Resume a paused file verification job
      description: |
        Resume the current paused attempt of a file verification job.
        Resume does not create a new attempt. Repeated resume requests on the same
        already-resumed attempt are treated as idempotent no-ops and return the current status.
      operationId: resumePausedJob
      parameters:
        - name: job_id
          in: path
          required: true
          description: The paused job ID
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Job resumed successfully
          content:
            application/json:
              schema:
                # 2026-05-09 F26+: same Go return type as /v1/verify/file/{job_id} GET
                # (*FileVerifyTaskStatus), so reuse FileVerifyTaskStatusApiResponse.
                $ref: '#/components/schemas/FileVerifyTaskStatusApiResponse'
              example:
                success: true
                code: "0"
                message: "Success"
                data:
                  task_id: "7143874e-21c5-43c1-96f3-2b52ea41ae6a"
                  status: "processing"
                  progress: 66
                  total_emails: 1000
                  processed_emails: 660
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          description: Failed to update task state
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /v1/credits:
    get:
      tags:
        - Account
      summary: Get credit balance
      description: Get current credit balance and usage information for the authenticated account.
      operationId: getCreditBalance
      responses:
        '200':
          description: Credit balance retrieved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse'
              example:
                success: true
                code: "0"
                message: "Success"
                data:
                  account_id: "abc123"
                  api_key_id: "key_xyz"
                  api_key_name: "Default API Key"
                  credits_balance: 9500
                  credits_consumed: 500
                  credits_added: 10000
                  last_updated: "2026-02-04T10:30:00Z"
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/webhooks:
    post:
      tags:
        - Webhooks
      summary: Create webhook
      description: |
        Create a new webhook to receive notifications for file verification events.

        **Available events:**
        - `file.completed` - File verification job completed successfully
        - `file.failed` - File verification job failed

        The webhook `secret` is returned only on creation. Store it securely for signature verification.

        **Security requirements:**
        - Webhook URLs must use HTTPS
        - `X-Webhook-Signature` is `sha256=<hex>`
        - Verify the signature over `{X-Webhook-Timestamp}.{raw_body}`
        - Reject old timestamps to reduce replay risk
      operationId: createWebhook
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookCreateRequest'
            example:
              url: "https://your-app.com/webhooks/billionverify"
              events:
                - "file.completed"
                - "file.failed"
      responses:
        '200':
          description: Webhook created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse'
              example:
                success: true
                code: "0"
                message: "Success"
                data:
                  id: "673f3bff-81ea-4730-9cef-af1eb7f134bf"
                  url: "https://your-app.com/webhooks/billionverify"
                  events:
                    - "file.completed"
                    - "file.failed"
                  secret: "5c609e6893ab3b65ce8940549a030bc165762fe171e0b38f880bd570099619bf"
                  is_active: true
                  created_at: "2026-02-04T10:30:00Z"
                  updated_at: "2026-02-04T10:30:00Z"
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

    get:
      tags:
        - Webhooks
      summary: List webhooks
      description: |
        List all webhooks for the authenticated account. Note that the `secret` is not returned in list responses.

        Delivery health fields:
        - `last_delivery_status` uses `success` or `failed`
        - `last_delivery_at` stores the final delivery timestamp
        - `last_error` stores the final error string when the last delivery failed
      operationId: listWebhooks
      responses:
        '200':
          description: Webhooks retrieved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse'
              example:
                success: true
                code: "0"
                message: "Success"
                data:
                  webhooks:
                    - id: "673f3bff-81ea-4730-9cef-af1eb7f134bf"
                      url: "https://your-app.com/webhooks/billionverify"
                      events:
                        - "file.completed"
                        - "file.failed"
                      is_active: true
                      last_delivery_status: "success"
                      last_delivery_at: "2026-04-01T12:00:00Z"
                      last_error: null
                      created_at: "2026-02-04T10:30:00Z"
                      updated_at: "2026-02-04T10:30:00Z"
                  total: 1
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/webhooks/{webhook_id}:
    delete:
      tags:
        - Webhooks
      summary: Delete webhook
      description: Delete a webhook by ID.
      operationId: deleteWebhook
      parameters:
        - name: webhook_id
          in: path
          required: true
          description: The webhook ID
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Webhook deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiResponse'
              example:
                success: true
                code: "0"
                message: "Success"
                data:
                  message: "Webhook deleted successfully"
                  webhook_id: "673f3bff-81ea-4730-9cef-af1eb7f134bf"
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /health:
    get:
      tags:
        - Health
      summary: Health check
      description: Basic health check endpoint. No authentication required.
      operationId: healthCheck
      security: []
      responses:
        '200':
          description: Service is healthy
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: "ok"
                  time:
                    type: integer
                    description: Unix timestamp
                    example: 1705319400

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: BV-API-KEY
      description: |
        API key for authentication. The following header names are accepted, in priority order:
        - `BV-API-KEY` (recommended)
        - `BILLIONVERIFY-API-KEY`
        - `Authorization: Bearer {api_key}`

  schemas:
    SingleVerifyRequest:
      type: object
      required:
        - email
      properties:
        email:
          type: string
          format: email
          description: Email address to verify
          example: "user@example.com"
        check_smtp:
          type: boolean
          description: "Optional SMTP override: when omitted, the server default applies (production: enabled). true explicitly enables SMTP; false explicitly disables SMTP for this request."
        force_refresh:
          type: boolean
          description: Bypass cached results and force a fresh live verification. When true, all cache layers are skipped and a credit is always consumed.
          default: false
        include_domain_reputation:
          type: boolean
          description: When true and a cached result is returned, also synchronously fetch the domain MX IP reputation (blacklist status). Adds a small latency cost but enriches `domain_reputation` in the response.
          default: false

    BatchVerifyRequest:
      type: object
      required:
        - emails
      properties:
        emails:
          type: array
          items:
            type: string
            format: email
          description: Array of email addresses to verify (max 50)
          maxItems: 50
          example:
            - "user1@example.com"
            - "user2@example.com"
        check_smtp:
          type: boolean
          description: "Optional SMTP override: when omitted, the server default applies (production: enabled). true explicitly enables SMTP; false explicitly disables SMTP for this request."
        include_domain_reputation:
          type: boolean
          description: When true and a cached result is returned for any email, also synchronously fetch the domain MX IP reputation. Per-row response.domain_reputation will be populated. Adds latency cost.
          default: false

    FileUploadRequest:
      type: object
      required:
        - file
      properties:
        file:
          type: string
          format: binary
          description: File containing email addresses (CSV, Excel, or TXT). Max 20MB.
        check_smtp:
          type: boolean
          description: "Optional SMTP override: when omitted, the server default applies (production: enabled). true explicitly enables SMTP; false explicitly disables SMTP for this request."
        email_column:
          type: string
          description: Column name containing email addresses (auto-detected if not specified)
        preserve_original:
          type: boolean
          description: Keep original columns in result file
          default: true

    WebhookCreateRequest:
      type: object
      required:
        - url
        - events
      properties:
        url:
          type: string
          format: uri
          description: HTTPS URL to receive webhook notifications
          pattern: '^https://'
          example: "https://your-app.com/webhooks/billionverify"
        events:
          type: array
          items:
            type: string
            enum:
              - file.completed
              - file.failed
          description: Events to subscribe to
          example:
            - "file.completed"
            - "file.failed"

    EmailVerificationResult:
      type: object
      properties:
        email:
          type: string
          format: email
          description: The verified email address
        status:
          type: string
          enum:
            - valid
            - invalid
            - unknown
            - risky
            - disposable
            - catchall
            - role
          description: Verification status
        score:
          type: number
          format: float
          minimum: 0
          maximum: 1
          description: Quality score (0-1)
        is_deliverable:
          type: boolean
          description: Whether the email is deliverable
        is_disposable:
          type: boolean
          description: Whether it's a disposable/temporary email
        is_catchall:
          type: boolean
          description: Whether the domain accepts all emails
        is_role:
          type: boolean
          description: Whether it's a role-based email (info@, support@, etc.)
        is_free:
          type: boolean
          description: Whether it's from a free email provider
        has_gravatar:
          type: boolean
          description: Whether the email address has a Gravatar profile.
        gravatar_url:
          type: string
          description: URL of the Gravatar avatar (only when has_gravatar=true).
          nullable: true
        domain:
          type: string
          description: Email domain
        domain_age:
          type: integer
          description: Domain age in years (omitempty in Go = pointer; absent when unknown).
          nullable: true
        mx_records:
          type: array
          items:
            type: string
          description: MX records for the domain (omitempty array).
          nullable: true
        mx_ip:
          type: string
          description: Primary MX IP address when known (pointer in Go; absent if MX unresolved).
          nullable: true
        mx_asn:
          type: integer
          description: Primary MX autonomous system number when known
        mx_as_org:
          type: string
          description: Primary MX autonomous system organization when known (pointer in Go).
          nullable: true
        domain_reputation:
          allOf:
            - $ref: '#/components/schemas/DomainReputation'
          nullable: true
          description: |
            MX IP reputation (blacklist) info. Populated only when the request had
            `include_domain_reputation: true` AND a cached result was returned (otherwise
            it's recomputed on the live SMTP path and inlined). Pointer in Go = absent when
            not requested.
        check_smtp:
          type: boolean
          description: Whether the result includes SMTP verification data
        reason:
          type: string
          description: Detailed reason for the verification result. Rule-based reasons include smtp_deliverable, mailbox_not_found, no_mx_records, invalid_syntax, disposable_domain, role_account, catch_all_domain, catch_all_inferred, m365_internal_relay, gateway_accept_all, forwarding_alias, smtp_unverifiable, smtp_greylisted, mimecast_greylist, proofpoint_ad_lookup, smtp_timeout, dns_timeout, smtp_rate_limited, smtp_blocked, smtp_eof_blocked, smtp_connection_failed, m365_ip_rep_block, google_rate_limit, google_dmarc_misalignment, mailbox_full, and policy_temp_fail.
        suggestion:
          type: string
          description: Deprecated. Use `domain_suggestion`.
        domain_suggestion:
          type: string
          description: Suggested correction if typo detected
        smtp_response:
          type: string
          description: Redacted raw SMTP response detail. Only returned for accounts or API keys in the verbose SMTP response allowlist (pointer in Go = absent for most clients).
          nullable: true
        response_time:
          type: integer
          description: Response time in milliseconds
        credits_used:
          type: integer
          description: Number of credits consumed
        verification_mode:
          type: string
          description: |
            Q4 (2026-05-08): Which verification path produced this result. Helps clients
            interpret retryability and explain billing.
          enum:
            - smtp
            - yahoo_api
            - cache_hit_global
            - cache_hit_user
            - cache_hit_db_history
            - cache_hit_transient
            - skip_anti_verification
            - skip_smtp_catchall
            - skip_smtp_unknown
            - syntax_only
            - unknown_path
        retryable:
          type: boolean
          description: |
            Q4 (2026-05-08): Whether re-running with `force_refresh=true` could yield a
            different result. true for transient unknowns (rate_limited / timeouts /
            yahoo_api unavailable). false for deterministic outcomes and for
            anti-verification / smtp_blocked / dmarc misalignment.
        smtputf8:
          type: boolean
          description: |
            P1-2 (2026-05-13): True when the email's local-part contains any non-ASCII
            character (RFC 6531 SMTPUTF8 required). Clients planning to send mail to
            this address must use an outbound MTA that supports the SMTPUTF8 extension;
            MTAs lacking SMTPUTF8 will reject non-ASCII local-parts.
            Borrowed from JoshData/python-email-validator `ValidatedEmail.smtputf8`.
        retry_after_seconds:
          type: integer
          description: |
            SMTP retry hint parsed from a transient remote SMTP response body
            (for example greylisting "try again in 60 seconds"). Omitted when
            the remote server did not provide a usable delay.
          nullable: true

    DeliverabilityBuckets:
      type: object
      description: |
        Q7 (2026-05-08) + §13 Finding 6 (2026-05-09): pre-aggregated business buckets
        so clients don't have to parse each row's status/reason. Returned on
        `/v1/verify/bulk` and `/v1/verify/file/{job_id}`.

        Math: `strict_deliverable + (usable_with_catchall - strict_deliverable) +
              not_deliverable + unknown = total_emails`.

        `anti_verified + transient_failure ≤ unknown` (NOT equal — the residual
        unknown rows are smtp_blocked / google_dmarc_misalignment / non-anti-domain
        smtp_unverifiable etc., which retrying does not improve).
      properties:
        strict_deliverable:
          type: integer
          description: valid + role
        usable_with_catchall:
          type: integer
          description: strict_deliverable + catchall (broader reach if you accept catchall risk)
        not_deliverable:
          type: integer
          description: invalid + disposable
        unknown:
          type: integer
          description: total unknown rows (super-set of anti_verified + transient_failure)
        anti_verified:
          type: integer
          description: |
            unknown rows whose domain is in the anti-verification consumer list
            (Apple iCloud / Microsoft consumer / Proton / Tuta) AND reason=smtp_unverifiable.
            Retrying does NOT help — these providers refuse SMTP probing entirely.
        transient_failure:
          type: integer
          description: |
            unknown rows whose reason is in the retryable whitelist (smtp_rate_limited /
            rate_limited / smtp_timeout / dns_timeout / dns_lookup_error /
            smtp_connection_failed / smtp_eof_blocked / smtp_greylisted / mimecast_greylist /
            proofpoint_ad_lookup / policy_temp_fail / google_rate_limit /
            m365_ip_rep_block / yahoo_api_error / yahoo_api_unavailable /
            yahoo_api_eof / yahoo_api_unavailable_after_fallback /
            internal_verifier_error /
            mx_only_verification_semaphore_timeout).
            Customer can `force_refresh=true` to retry.

    ApiResponse:
      type: object
      properties:
        success:
          type: boolean
          description: Whether the request was successful
        code:
          type: string
          description: Response code ("0" for success)
        message:
          type: string
          description: Human-readable message
        data:
          type: object
          description: Response data (varies by endpoint)

    # 2026-05-09 末轮自审 (analogous to §14 Finding 8): typed single-verify
    # response so SDK generators see verification_mode / retryable fields.
    # Use this in /v1/verify/single 200 response.
    SingleVerifyApiResponse:
      type: object
      properties:
        success:
          type: boolean
        code:
          type: string
        message:
          type: string
        data:
          $ref: '#/components/schemas/EmailVerificationResult'
        error:
          allOf:
            - $ref: '#/components/schemas/ErrorInfo'
          nullable: true
          description: |
            Mirror of `response.StandardResponse.Error`. Populated only on
            non-200 paths (omitempty)—success responses won't include it.

    # 2026-05-09 F24: DomainReputation typed schema, mirrors Go
    # `model.DomainReputation` (`src/model/email_verification_model.go`).
    # Promoted from inline EmailVerificationResult.domain_reputation to a
    # named typed schema so SDK generators can emit a proper class +
    # reflect contract test can guard field completeness.
    DomainReputation:
      type: object
      description: MX IP reputation (blacklist) info attached to a verification result.
      properties:
        mx_ip:
          type: string
          description: IP address of the MX server (omitempty in Go).
          nullable: true
        is_listed:
          type: boolean
          description: Whether the IP is on any blacklist
        blacklists:
          type: array
          items:
            type: string
          description: List of blacklists where the IP appears
        checked:
          type: boolean
          description: Whether reputation check was performed

    # 2026-05-09 F23: ErrorInfo typed schema, mirrors Go `response.ErrorInfo`
    # (`src/utils/response/error_codes.go`). Added so all typed wrapper
    # schemas can `$ref` it (avoid inline duplication and missing fields).
    ErrorInfo:
      type: object
      properties:
        code:
          type: string
          description: Detailed error code
        message:
          type: string
          description: Detailed error message
        description:
          type: string
          description: Additional human-readable context (omitempty in Go)
          nullable: true
        details:
          description: Additional structured context (any type; usually map or string; omitempty in Go).
          nullable: true

    # 2026-05-09 §14 Finding 8: typed bulk response so SDK generators can see
    # the deliverability_buckets field. Use this in /v1/verify/bulk 200 response.
    BulkVerifyApiResponse:
      type: object
      properties:
        success:
          type: boolean
        code:
          type: string
        message:
          type: string
        data:
          $ref: '#/components/schemas/BatchEmailVerificationResponse'
        error:
          allOf:
            - $ref: '#/components/schemas/ErrorInfo'
          nullable: true

    BatchEmailVerificationResponse:
      type: object
      description: |
        Q7 (2026-05-08) + §13 Finding 6 (2026-05-09) + §14 Finding 8 (2026-05-09):
        Bulk verification response with pre-aggregated deliverability buckets.

        Schema fields are mirrored from `model.BatchEmailVerificationResponse`
        (`src/model/email_verification_model.go`); `omitempty` Go fields are
        marked `nullable: true` so SDK generators emit optional types.
      properties:
        results:
          type: array
          items:
            $ref: '#/components/schemas/EmailVerificationResult'
        total_emails:
          type: integer
        valid_emails:
          type: integer
        invalid_emails:
          type: integer
        credits_used:
          type: integer
        process_time:
          type: integer
          description: Total processing time in milliseconds
        retryable_emails:
          type: array
          items:
            type: string
            format: email
          description: |
            Subset of input emails whose verification result is `retryable=true`
            (rate limit / timeout / yahoo_api unavailable / etc). Customer can
            re-submit these with `force_refresh=true` to retry. Omitted when empty.
          nullable: true
        deferred_emails:
          type: array
          items:
            type: string
            format: email
          description: |
            Subset of input emails that were deferred to async processing
            (e.g., Yahoo API throttled and shifted to background). Customer
            should retry these later or wait for a webhook. Omitted when empty.
          nullable: true
        retry_after_seconds:
          type: integer
          description: |
            Hint to the customer how long to wait before retrying any deferred
            or retryable rows. 0/omitted means no recommendation.
          nullable: true
        deliverability_buckets:
          allOf:
            - $ref: '#/components/schemas/DeliverabilityBuckets'
          nullable: true
          description: |
            Pre-aggregated deliverability buckets. Pointer in Go (omitempty) →
            absent for ephemeral / partial responses; populated on terminal
            states for file jobs and on every bulk response.

    # 2026-05-09 F26 (analogous to §14 Finding 8): typed file upload response.
    # /v1/verify/file POST returns FileVerifyTaskResponse with task_id /
    # status_url / estimated_count / etc. Wrap so SDK generators see typed data.
    FileUploadApiResponse:
      type: object
      properties:
        success:
          type: boolean
        code:
          type: string
        message:
          type: string
        data:
          $ref: '#/components/schemas/FileVerifyTaskResponse'
        error:
          allOf:
            - $ref: '#/components/schemas/ErrorInfo'
          nullable: true

    # FileVerifyTaskResponse mirror of Go struct service.FileVerifyTaskResponse
    # (`src/service/file_verify_service.go`).
    FileVerifyTaskResponse:
      type: object
      description: |
        Returned from POST /v1/verify/file (file upload). Includes job_id and
        status_url customers poll to check verification progress.
      properties:
        task_id:
          type: string
          description: Job ID; use this with GET /v1/verify/file/{job_id} to poll status.
        file_name:
          type: string
        file_size:
          type: integer
          format: int64
        status:
          type: string
          description: Initial job status (typically `pending` / `running`).
        message:
          type: string
          description: Human-readable status message.
        status_url:
          type: string
          description: Convenience URL clients can GET to poll task status (relative or absolute).
        created_at:
          type: string
          format: date-time
        estimated_count:
          type: integer
          description: Pre-parse estimated email count.
        unique_emails:
          type: integer
          nullable: true
          description: Distinct email count after dedup. omitempty in Go.
        total_rows:
          type: integer
          nullable: true
          description: Total file rows excluding header. omitempty in Go.
        email_column:
          type: string
          nullable: true
          description: Detected email column name. omitempty in Go.

    # FileVerifyTaskStatus is the response shape for GET /v1/verify/file/{job_id}.
    # Wired up so the deliverability_buckets field is discoverable from machine
    # contract too (§14 Finding 8).
    FileVerifyTaskStatusApiResponse:
      type: object
      properties:
        success:
          type: boolean
        code:
          type: string
        message:
          type: string
        data:
          $ref: '#/components/schemas/FileVerifyTaskStatus'
        error:
          allOf:
            - $ref: '#/components/schemas/ErrorInfo'
          nullable: true

    FileVerifyTaskStatus:
      type: object
      description: |
        Schema mirrors Go struct `service.FileVerifyTaskStatus`
        (`src/service/file_verify_service.go`). Reflect-based contract test
        guards that every JSON-serialized Go field appears here.
      properties:
        task_id:
          type: string
        status:
          type: string
          description: preparing / pending / running / processing / merging / paused / completed / completed_with_warning / failed / cancelled
        progress:
          type: integer
          description: 0-100
        email_progress:
          type: integer
          nullable: true
        chunk_progress:
          type: integer
          nullable: true
        progress_source:
          type: string
          nullable: true
          description: "email | chunk"
        total_emails:
          type: integer
        processed_emails:
          type: integer
        valid_emails:
          type: integer
        invalid_emails:
          type: integer
        role_emails:
          type: integer
        catchall_emails:
          type: integer
        unknown_emails:
          type: integer
        risky_emails:
          type: integer
        disposable_emails:
          type: integer
        disposable_charged_emails:
          type: integer
          description: disposable 命中里实际被扣费的 distinct 邮箱数 (SMTP opt-in 路径)。默认 0; 客户带 check_smtp:true 上传 file 且 disposable SMTP 出确定结果时, 这部分被计入 credits_used。详见 docs/disposable/disposable-billing-frontend.md。
        credits_used:
          type: integer
          format: int64
        cache_hit_not_charged:
          type: integer
          nullable: true
          description: Number of rows served from user-cache without charging credits (terminal state only).
        started_at:
          type: string
          format: date-time
          nullable: true
        completed_at:
          type: string
          format: date-time
          nullable: true
        error_message:
          type: string
          nullable: true
        pause_reason:
          type: string
          nullable: true
        can_pause:
          type: boolean
          nullable: true
        can_resume:
          type: boolean
          nullable: true
        can_restart:
          type: boolean
          nullable: true
        restart_attempts:
          type: integer
          nullable: true
        current_attempt:
          type: integer
          nullable: true
        download_url:
          type: string
          nullable: true
          description: API download URL for results CSV (terminal completed state).
        direct_download_url:
          type: string
          nullable: true
          description: Pre-signed direct-storage URL for results CSV (avoids API hop).
        direct_download_expires_at:
          type: string
          format: date-time
          nullable: true
        result_download_available:
          type: boolean
          description: Whether result CSV/detail download is currently available inside the 7-day result window.
        result_download_expires_at:
          type: string
          format: date-time
          nullable: true
          description: End of the 7-day result download window.
        result_download_unavailable_reason:
          type: string
          nullable: true
          description: Machine-readable reason when result download is unavailable, for example result_download_expired.
        total_chunks:
          type: integer
          nullable: true
        completed_chunks:
          type: integer
          nullable: true
        failed_chunks:
          type: integer
          nullable: true
        runtime_hints:
          allOf:
            - $ref: '#/components/schemas/FileVerifyTaskRuntimeHints'
          nullable: true
        deliverability_buckets:
          allOf:
            - $ref: '#/components/schemas/DeliverabilityBuckets'
          nullable: true
          description: |
            Pre-aggregated deliverability buckets. Pointer in Go (omitempty) →
            absent for ephemeral / partial responses; populated on terminal
            states for file jobs and on every bulk response.
        unique_emails:
          type: integer
          nullable: true
          description: Distinct email count after dedup (used for billing).
        total_rows:
          type: integer
          nullable: true
          description: Total input rows, excluding header.

    # 2026-05-09 §15 Finding 9 后续：runtime_hints typed schema。
    # Mirror of Go struct `filejob.JobRuntimeHints` (canonical) /
    # `service.FileVerifyTaskRuntimeHints` (alias, T3.12 path B).
    FileVerifyTaskRuntimeHints:
      type: object
      description: |
        Per-job runtime telemetry hints. Canonical Go struct
        `filejob.JobRuntimeHints` (`src/domain/filejob/read_model.go`);
        re-exported from `src/service` as `FileVerifyTaskRuntimeHints` via
        type alias (T3.12 path B).
      properties:
        has_live_execution:
          type: boolean
          nullable: true
          description: Whether worker is actively processing this job right now.
        inflight_batches:
          type: integer
          nullable: true
          description: Number of batches currently in-flight on workers.
        last_execution_fact_at:
          type: string
          format: date-time
          nullable: true
          description: Last time a worker reported live progress for this job.

    ErrorResponse:
      type: object
      description: |
        Standard error response shape. Mirrors Go `response.StandardResponse`
        with Error populated. F23 (2026-05-09) replaced inline error object
        with `$ref ErrorInfo` so all typed wrappers + ErrorResponse share one
        canonical schema.
      properties:
        success:
          type: boolean
          example: false
        code:
          type: string
          description: Top-level error code (mirrors Go StandardResponse.Code)
        message:
          type: string
          description: Top-level error message
        error:
          $ref: '#/components/schemas/ErrorInfo'

    WebhookPayload:
      type: object
      description: |
        Payload sent to the configured webhook URL when an event occurs.

        Signature verification:
        1. Read the raw HTTP request body exactly as received.
        2. Build the signed payload as `{X-Webhook-Timestamp}.{raw_body}`.
        3. Compute HMAC-SHA256 with your webhook secret.
        4. Compare the hex digest with `X-Webhook-Signature`.
      properties:
        event:
          type: string
          enum:
            - file.completed
            - file.failed
          description: Event type
        timestamp:
          type: string
          format: date-time
          description: When the event occurred
        data:
          type: object
          properties:
            job_id:
              type: string
              format: uuid
            file_name:
              type: string
            total_emails:
              type: integer
            valid_emails:
              type: integer
            invalid_emails:
              type: integer
            role_emails:
              type: integer
            catchall_emails:
              type: integer
            unknown_emails:
              type: integer
            disposable_emails:
              type: integer
            credits_used:
              type: integer
            process_time_seconds:
              type: number
            download_url:
              type: string
            direct_download_url:
              type: string
            direct_download_expires_at:
              type: string
              format: date-time
            error_message:
              type: string

  responses:
    BadRequest:
      description: Bad request - invalid parameters
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            success: false
            code: "4000"
            message: "Invalid request"
            error:
              code: "INVALID_REQUEST"
              message: "Invalid email format"

    Unauthorized:
      description: Unauthorized - invalid or missing API key
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            success: false
            code: "4010"
            message: "Unauthorized"
            error:
              code: "INVALID_API_KEY"
              message: "API key is invalid or missing"

    InsufficientCredits:
      description: Payment required - insufficient credits
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            success: false
            code: "4020"
            message: "Insufficient credits"
            error:
              code: "INSUFFICIENT_CREDITS"
              message: "Your account has insufficient credits. Please purchase more credits."

    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            success: false
            code: "4040"
            message: "Not found"
            error:
              code: "JOB_NOT_FOUND"
              message: "The specified job was not found"

    RateLimitExceeded:
      description: Rate limit exceeded
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            success: false
            code: "4290"
            message: "Rate limit exceeded"
            error:
              code: "RATE_LIMIT_EXCEEDED"
              message: "Too many requests. Please try again later."
          headers:
            X-RateLimit-Limit:
              schema:
                type: integer
              description: Request limit per minute
            X-RateLimit-Remaining:
              schema:
                type: integer
              description: Remaining requests in current window
            X-RateLimit-Reset:
              schema:
                type: integer
              description: Unix timestamp when the rate limit resets
