openapi: 3.1.0
info:
  title: EmailValidator Public API
  version: "1"
  description: |
    REST API for programmatic email validation. All endpoints are under `/api/v1`
    and require an API key.

    **Authentication** — supply your key in the `X-Api-Key` request header (preferred)
    or as the `?api_key=` query parameter (useful for quick tests; avoid in production
    logs where the URL may be recorded).

    **Rate limiting** — 60 requests per minute per key. Exceeding the limit returns `429`.

    **Test mode** — API keys beginning with `ev_test_` return instant, canned results
    at zero credit cost. The outcome depends on the local part of the submitted address
    (e.g. `valid@`, `invalid@`, `catch_all@`, `disposable@`, `role@`, `unknown@`).
servers:
  - url: https://api.emailvalidator.ai

security:
  - apiKey: []

components:
  securitySchemes:
    apiKey:
      type: apiKey
      in: header
      name: X-Api-Key
      description: |
        API key issued from your account dashboard. Prefix `ev_` for production keys,
        `ev_test_` for test-mode keys.

        **Alternative** — pass the key as `?api_key=<key>` in the query string when
        setting a header is inconvenient (e.g. browser EventSource). Avoid this in
        production where URLs appear in server logs.

  schemas:
    VerifyRequest:
      type: object
      required:
        - email
      properties:
        email:
          type: string
          description: Email address to validate.
          example: alice@example.com
        timeout:
          type: integer
          minimum: 3
          maximum: 60
          default: 30
          description: |
            Seconds to wait for a synchronous result (3–60). When the validation
            completes within this window the response is `200 OK` with the full
            result. When the timeout elapses first the response is `202 Accepted`
            with a `result_url` to poll.
          example: 15
        callback_url:
          type: string
          format: uri
          description: |
            HTTPS URL to receive the result as a signed webhook. When supplied the
            request returns `202 Accepted` immediately and the result is delivered
            asynchronously. Store the `callback_secret` from the response to verify
            webhook signatures.
          example: https://yourapp.example.com/hooks/email

    VerifyResult:
      type: object
      description: Full result of a completed single-email validation.
      properties:
        email:
          type: string
          description: The validated email address.
          example: alice@example.com
        status:
          $ref: '#/components/schemas/VerifyStatus'
        sub_status:
          $ref: '#/components/schemas/VerifySubStatus'
        free_email:
          type: boolean
          description: Whether the domain is a known free-email provider.
          example: false
        did_you_mean:
          type: string
          nullable: true
          description: Suggested correction when a common domain typo is detected, otherwise null.
          example: null
        account:
          type: string
          description: Local part of the email address (before `@`).
          example: alice
        domain:
          type: string
          description: Domain part of the email address (after `@`).
          example: example.com
        details:
          $ref: '#/components/schemas/VerifyDetails'
        credits_remaining:
          type: integer
          nullable: true
          description: Credit balance after this validation. `null` for system/demo accounts.
          example: 4820
        processed_at:
          type: string
          format: date-time
          description: UTC timestamp when validation completed.
          example: "2025-06-10T14:02:11.432Z"

    VerifyDetails:
      type: object
      description: Low-level signal from DNS and SMTP probing.
      properties:
        role:
          type: boolean
          description: Whether the local part is a role alias (e.g. `admin`, `support`, `noreply`).
          example: false
        disposable:
          type: boolean
          description: Whether the domain belongs to a known disposable/temporary email provider.
          example: false
        catch_all:
          type: boolean
          description: Whether the domain is configured to accept all addresses.
          example: false
        mx_found:
          type: boolean
          description: Whether at least one MX record was found for the domain.
          example: true
        mx_record:
          type: string
          nullable: true
          description: The highest-priority MX hostname, or null when no MX records exist.
          example: mail.example.com
        smtp_provider:
          type: string
          nullable: true
          description: Detected SMTP provider name (e.g. `Google`, `Microsoft`), or null when unknown.
          example: Google
        smtp_code:
          type: integer
          nullable: true
          description: SMTP response code from the final RCPT TO probe, or null when no SMTP probe was attempted.
          example: 250

    VerifyStatus:
      type: string
      enum:
        - valid
        - invalid
        - catch-all
        - unknown
      description: |
        Top-level validation verdict:
        - `valid` — mailbox confirmed deliverable via SMTP.
        - `invalid` — mailbox does not exist or hard-rejected.
        - `catch-all` — domain accepts all addresses; deliverability unconfirmable.
        - `unknown` — could not determine (DNS failure, transient error, or no SMTP probe data).
      example: valid

    VerifySubStatus:
      type: string
      enum:
        - ""
        - disposable
        - role_based
        - accept_all
        - no_dns_entries
        - mailbox_not_found
        - greylisted
        - mail_server_did_not_respond
      description: |
        Additional signal within the top-level `status`. Empty string when none applies.
        Derivation is first-match: `disposable` > `role_based` > `accept_all` >
        `no_dns_entries` > `mailbox_not_found` > `greylisted` > `mail_server_did_not_respond`.
      example: ""

    BatchStartResponse:
      type: object
      description: Response from `POST /api/v1/verify/batch` when a job is accepted.
      properties:
        job_id:
          type: string
          format: uuid
          description: Unique identifier for the batch job.
          example: 7a4e2c1b-0000-0000-0000-000000000000
        total_rows:
          type: integer
          description: Total number of rows in the uploaded file.
          example: 1000
        skipped_rows:
          type: integer
          description: Number of non-email/blank rows skipped during the upload scan.
          example: 5
        charged:
          type: integer
          description: Number of email addresses billed (valid-email rows only).
          example: 995
        credits_remaining:
          type: integer
          nullable: true
          description: Credit balance after deduction. `null` for system/demo accounts.
          example: 4817
        status:
          type: string
          enum:
            - processing
          description: Always `processing` immediately after job creation.
          example: processing
        callback_secret:
          type: string
          description: |
            HMAC secret for verifying webhook signatures. Included only when `callback_url`
            was provided in the request. Store it — it is not retrievable later.
          example: bv_a3b4c5...

    BatchJobResponse:
      type: object
      description: State of a batch job returned by `GET /api/v1/verify/batch/{id}`.
      properties:
        job_id:
          type: string
          format: uuid
          example: 7a4e2c1b-0000-0000-0000-000000000000
        status:
          type: string
          enum:
            - processing
            - complete
            - failed
          description: Current job status.
          example: complete
        total_rows:
          type: integer
          description: Total rows in the uploaded file.
          example: 1000
        billed_rows:
          type: integer
          description: Number of rows that were billed.
          example: 995
        original_file_name:
          type: string
          description: File name as uploaded.
          example: emails.csv
        created_at:
          type: string
          format: date-time
          example: "2025-06-10T14:00:00Z"
        started_at:
          type: string
          format: date-time
          nullable: true
          example: "2025-06-10T14:00:01Z"
        completed_at:
          type: string
          format: date-time
          nullable: true
          description: UTC timestamp when validation completed. `null` while processing.
          example: "2025-06-10T14:00:45Z"
        expires_at:
          type: string
          format: date-time
          nullable: true
          description: UTC timestamp after which result URLs are no longer valid. `null` while processing.
          example: "2025-06-17T14:00:45Z"
        csv_url:
          type: string
          nullable: true
          description: Pre-signed download URL for the CSV result file. Valid for 7 days. `null` while processing.
          example: https://storage.example.com/results/job.csv?X-Amz-Expires=604800
        json_url:
          type: string
          nullable: true
          description: Pre-signed download URL for the JSON result file. Valid for 7 days. `null` while processing.
          example: https://storage.example.com/results/job.json?X-Amz-Expires=604800

    CreditsResponse:
      type: object
      description: Current credit balance for the authenticated account.
      properties:
        credits_remaining:
          type: integer
          nullable: true
          description: Sum of `free_credits` and `paid_credits`. `null` for system/demo accounts.
          example: 4817
        free_credits:
          type: integer
          description: Complimentary credit balance.
          example: 317
        paid_credits:
          type: integer
          description: Purchased credit balance.
          example: 4500

    WebhookEnvelope:
      type: object
      description: Wrapper for all webhook deliveries.
      properties:
        id:
          type: string
          description: |
            Stable, deterministic event ID (`evt_` + SHA-256 hex of the resource ID and
            event name). Identical across retries — use it as your idempotency key.
          example: evt_3a4b5c6d...
        event:
          type: string
          enum:
            - verify.completed
            - batch.completed
            - test
          description: Event type.
          example: verify.completed
        created_at:
          type: string
          format: date-time
          description: UTC timestamp of the event.
          example: "2025-06-10T14:02:11.432Z"
        data:
          type: object
          description: |
            Event-specific payload:
            - `verify.completed` — full `VerifyResult` object.
            - `batch.completed` — batch summary with pre-signed download URLs.
            - `test` — `{ "message": "This is a test event." }`.

    ApiProblem:
      type: object
      description: Standard error response shape used by all error status codes.
      properties:
        status:
          type: integer
          description: HTTP status code mirrored in the body.
          example: 400
        error:
          type: string
          description: Human-readable error message.
          example: Invalid email format.
        details:
          type: array
          items:
            type: string
          description: Optional per-field validation messages. Omitted when not applicable.
          example:
            - "email: required"

paths:
  /api/v1/verify:
    post:
      summary: Submit a single email for validation
      operationId: verifySingle
      description: |
        Submit a single email address for validation.

        **Synchronous mode** (no `callback_url`): the request blocks until validation
        completes or the `timeout` elapses.
        - Completes within timeout → `200 OK` with the full result.
        - Timeout exceeded → `202 Accepted` with a `result_url` to poll.

        **Asynchronous mode** (with `callback_url`): the request returns `202 Accepted`
        immediately. When validation completes the full result is delivered to your URL
        as a signed webhook.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/VerifyRequest'
            examples:
              sync:
                summary: Synchronous (no callback)
                value:
                  email: alice@example.com
                  timeout: 15
              async:
                summary: Asynchronous (with callback)
                value:
                  email: alice@example.com
                  callback_url: https://yourapp.example.com/hooks/email
      responses:
        "200":
          description: Validation completed synchronously — full result returned.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VerifyResult'
        "202":
          description: |
            Accepted — either async callback mode or sync timeout exceeded.
            When timeout exceeded, includes `result_url` to poll.
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                    format: uuid
                  status:
                    type: string
                    enum: [pending]
                  sub_status:
                    type: string
                    description: Present only on timeout — value is `timeout_exceeded`.
                  result_url:
                    type: string
                    description: Present only on timeout — URL to poll for the result.
                  callback_secret:
                    type: string
                    description: Present only in async mode — store to verify webhook signatures.
        "400":
          description: Invalid email format or bad `callback_url`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiProblem'
        "401":
          description: Missing or invalid API key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiProblem'
        "402":
          description: Insufficient credits.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiProblem'
        "429":
          description: Rate limit exceeded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiProblem'

  /api/v1/verify/{id}:
    get:
      summary: Poll for a single-email validation result
      operationId: getVerifyResult
      description: |
        Poll for a single-email validation result by its ID. The ID is returned by
        `POST /api/v1/verify`. Returns the full result when complete, or a pending
        stub while processing.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Validation ID returned by `POST /api/v1/verify`.
          example: 3fa85f64-5717-4562-b3fc-2c963f66afa6
      responses:
        "200":
          description: |
            Result returned. While still processing: `{ "id": "...", "status": "pending" }`.
            When complete: full `VerifyResult` object.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/VerifyResult'
                  - type: object
                    properties:
                      id:
                        type: string
                        format: uuid
                      status:
                        type: string
                        enum: [pending]
        "401":
          description: Missing or invalid API key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiProblem'
        "404":
          description: Validation ID not found or belongs to a different key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiProblem'
        "429":
          description: Rate limit exceeded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiProblem'

  /api/v1/verify/batch:
    post:
      summary: Upload a CSV file and start a batch validation job
      operationId: batchVerify
      description: |
        Upload a CSV file of email addresses and start a batch validation job. The CSV
        must have an `email` column; other columns are passed through to the result file.

        Credits are deducted immediately on job start based on the number of valid email
        rows. When insufficient credits: `402` with `shortfall`, `balance`, and `required`
        in the body.
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - file
              properties:
                file:
                  type: string
                  format: binary
                  description: CSV file containing email addresses. Must include an `email` column.
                callback_url:
                  type: string
                  format: uri
                  description: Optional HTTPS URL to receive a webhook when the batch job completes.
      responses:
        "200":
          description: Job accepted and processing started.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BatchStartResponse'
        "400":
          description: Not multipart, missing `file` field, or bad `callback_url`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiProblem'
        "401":
          description: Missing or invalid API key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiProblem'
        "402":
          description: Insufficient credits. Body includes `shortfall`, `balance`, and `required`.
          content:
            application/json:
              schema:
                type: object
                properties:
                  shortfall:
                    type: integer
                  balance:
                    type: integer
                  required:
                    type: integer
        "409":
          description: Job is not in pending state (concurrent submission conflict).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiProblem'
        "413":
          description: Upload exceeds the maximum allowed file size.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiProblem'
        "422":
          description: |
            Ambiguous CSV — multiple candidate email columns detected.
            Re-POST with `email_column` set to the correct column name.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    enum: [needs_column_selection]
                  candidate_columns:
                    type: array
                    items:
                      type: string
        "429":
          description: Rate limit exceeded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiProblem'

  /api/v1/verify/batch/{id}:
    get:
      summary: Get the status of a batch job
      operationId: getBatchJob
      description: |
        Check the status of a batch job and, once complete, retrieve pre-signed download
        URLs for the result files. Pre-signed URLs are valid for 7 days from job completion;
        call this endpoint again to refresh them as long as job data is retained.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Batch job ID returned by `POST /api/v1/verify/batch`.
          example: 7a4e2c1b-0000-0000-0000-000000000000
      responses:
        "200":
          description: Job found. Download URLs are included when `status == "complete"`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BatchJobResponse'
        "401":
          description: Missing or invalid API key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiProblem'
        "404":
          description: Job not found or belongs to a different key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiProblem'
        "429":
          description: Rate limit exceeded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiProblem'

  /api/v1/credits:
    get:
      summary: Get the current credit balance
      operationId: getCredits
      description: Return the current credit balance for the authenticated key's account.
      responses:
        "200":
          description: Balance returned.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreditsResponse'
              example:
                credits_remaining: 4817
                free_credits: 317
                paid_credits: 4500
        "401":
          description: Missing or invalid API key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiProblem'
        "429":
          description: Rate limit exceeded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiProblem'

  /api/v1/events/stream:
    get:
      summary: Stream validation events via Server-Sent Events
      operationId: streamEvents
      description: |
        Connect to a Server-Sent Events (SSE) stream that delivers validation results in
        real time. Each event is a JSON-encoded `WebhookEnvelope`. The stream sends a
        `: heartbeat` comment periodically to keep the connection alive.

        **Reconnection** — on disconnect reconnect with `Last-Event-ID` set to the last
        received event ID, or `?since=<ISO-8601>` to replay events from a specific time.

        Note: this endpoint is not rate-limited (reconnect loops would exhaust a
        per-minute budget).

        **Authentication** — use the `?api_key=` query parameter when a browser
        `EventSource` is used (it cannot set custom headers).
      parameters:
        - name: since
          in: query
          schema:
            type: string
          description: ISO-8601 timestamp or tick count. Returns events after this point.
          example: "2025-06-10T14:00:00Z"
        - name: Last-Event-ID
          in: header
          schema:
            type: string
          description: SSE reconnect protocol — tick count of the last received event.
      responses:
        "200":
          description: SSE stream established. Events are delivered as `text/event-stream`.
          content:
            text/event-stream:
              schema:
                type: string
                description: |
                  Each event follows the SSE wire format:
                  ```
                  id: <ticks>
                  event: verify.completed
                  data: <WebhookEnvelope JSON>

                  ```
        "401":
          description: Missing or invalid API key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiProblem'

  /api/v1/webhooks/test:
    post:
      summary: Send a test webhook delivery
      operationId: testWebhook
      description: |
        Enqueue a test webhook delivery to a URL you control. Useful for verifying that
        your endpoint is reachable and that signature verification works before running
        real validations.

        The test event is signed with the same `X-Signature` header as production events.
        It arrives at your URL within seconds.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - callback_url
              properties:
                callback_url:
                  type: string
                  format: uri
                  description: HTTPS URL to deliver the test event to.
                  example: https://yourapp.example.com/hooks/email
      responses:
        "202":
          description: Test event queued.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [queued]
              example:
                status: queued
        "400":
          description: Missing or invalid `callback_url`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiProblem'
        "401":
          description: Missing or invalid API key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiProblem'
        "429":
          description: Rate limit exceeded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiProblem'
