openapi: 3.1.0
info:
  title: Email Verifier API
  version: 1.0.0
  description: |
    Verify email addresses via REST API.

    **Billing:** Each successful `POST /verify` consumes **1 credit**.
    `GET` endpoints are free. Insufficient credits return HTTP 402.

    **Authentication:** `Authorization: Bearer ev_live_…` (production) or `ev_test_…` (sandbox) API key, or session cookie from the web panel.

    **Sandbox keys** (`ev_test_…`): deterministic mock responses, **no credits consumed**. Use for development and CI. Create from `/konto/api` with the sandbox toggle.
  contact:
    name: Email Verifier Support
    url: https://propercheck.pl/kontakt
servers:
  - url: https://propercheck.pl/api/v1
    description: Production
  - url: http://localhost:4321/api/v1
    description: Local dev (Astro proxy)
tags:
  - name: Verify
    description: Single email verification
  - name: Credits
    description: Credit balance
  - name: History
    description: Verification history
  - name: Bulk
    description: Bulk verification jobs
  - name: Uploads
    description: CSV/XLSX file ingest (session auth, panel limits)
  - name: API Keys
    description: API token management (session auth)
  - name: Webhooks
    description: Outbound webhooks (session auth)
  - name: Usage
    description: API usage statistics
paths:
  /verify:
    post:
      tags: [Verify]
      summary: Verify a single email
      description: Consumes 1 credit on HTTP 200 with `ev_live_…` keys. Sandbox keys (`ev_test_…`) return mock results without debiting credits. Rate limit 60 req/min per API key by default.
      operationId: verifyEmail
      security:
        - bearerAuth: []
        - sessionCookie: []
      parameters:
        - name: Idempotency-Key
          in: header
          required: false
          schema:
            type: string
          description: Optional idempotency key — retries return the same response without extra debit.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/VerifyRequest'
            examples:
              single:
                value:
                  email: john@company.com
      responses:
        '200':
          description: Verification result
          headers:
            X-Request-Id:
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VerifyResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/InsufficientCredits'
        '429':
          $ref: '#/components/responses/RateLimited'
        '503':
          $ref: '#/components/responses/EngineUnavailable'
  /verify/{id}:
    get:
      tags: [Verify]
      summary: Get verification by ID
      operationId: getVerification
      security:
        - bearerAuth: []
        - sessionCookie: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            example: ver_abc123
      responses:
        '200':
          description: Verification record
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VerificationRecord'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /verifications:
    get:
      tags: [History]
      summary: List verification history and stats
      operationId: listVerifications
      security:
        - bearerAuth: []
        - sessionCookie: []
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
            maximum: 200
        - name: source
          in: query
          schema:
            type: string
            enum: [all, web, api, bulk]
            default: all
      responses:
        '200':
          description: History and 30-day stats
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VerificationsListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /credits:
    get:
      tags: [Credits]
      summary: Get credit balance
      operationId: getCredits
      security:
        - bearerAuth: []
        - sessionCookie: []
      responses:
        '200':
          description: Current balance
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreditBalance'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /usage:
    get:
      tags: [Usage]
      summary: API usage summary
      operationId: getUsage
      security:
        - bearerAuth: []
        - sessionCookie: []
      parameters:
        - name: period
          in: query
          schema:
            type: string
            enum: ['7', '7d', '30', '30d', '90', '90d']
            default: '30d'
      responses:
        '200':
          description: Usage aggregates
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UsageSummary'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /usage/keys:
    get:
      tags: [Usage]
      summary: Usage per API key
      operationId: getUsageByKeys
      security:
        - bearerAuth: []
        - sessionCookie: []
      parameters:
        - name: period
          in: query
          schema:
            type: string
            default: '30d'
      responses:
        '200':
          description: Per-key usage
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UsageByKeysResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /api-keys:
    get:
      tags: [API Keys]
      summary: List API keys
      operationId: listApiKeys
      security:
        - sessionCookie: []
      responses:
        '200':
          description: Active API keys with 30-day usage
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeysListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      tags: [API Keys]
      summary: Create API key
      description: Returns the full secret once. Store it securely — it cannot be retrieved again.
      operationId: createApiKey
      security:
        - sessionCookie: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateApiKeyRequest'
      responses:
        '200':
          description: Created key with secret
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateApiKeyResponse'
        '400':
          description: Invalid request or key limit reached
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /api-keys/{id}:
    patch:
      tags: [API Keys]
      summary: Rename API key
      operationId: updateApiKey
      security:
        - sessionCookie: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateApiKeyRequest'
      responses:
        '200':
          description: Updated key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UpdateApiKeyResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      tags: [API Keys]
      summary: Revoke API key
      operationId: revokeApiKey
      security:
        - sessionCookie: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Key revoked
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    example: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /bulk:
    post:
      tags: [Bulk]
      summary: Start bulk verification job
      description: Consumes N credits where N = number of unique emails.
      operationId: createBulkJob
      security:
        - bearerAuth: []
        - sessionCookie: []
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BulkRequest'
      responses:
        '200':
          description: Job queued
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BulkJobCreated'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/InsufficientCredits'
  /bulk/{id}:
    get:
      tags: [Bulk]
      summary: Bulk job status
      operationId: getBulkJob
      security:
        - bearerAuth: []
        - sessionCookie: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Job progress
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BulkJobStatus'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /bulk/{id}/export:
    get:
      tags: [Bulk]
      summary: Export bulk results as CSV
      operationId: exportBulkJob
      security:
        - bearerAuth: []
        - sessionCookie: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: CSV file
          content:
            text/csv:
              schema:
                type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /uploads:
    post:
      tags: [Uploads]
      summary: Upload CSV/XLSX/TXT file
      description: |
        Parses the file, auto-maps the email column, stores the original on disk and structured rows in the database.
        Panel limits apply (max 250 rows / 10 MB). Consumes no credits until `POST /uploads/{id}/verify`.
      operationId: createUpload
      security:
        - sessionCookie: []
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file:
                  type: string
                  format: binary
                columnMapping:
                  type: string
                  description: Optional JSON `ColumnMapping` when auto-mapping confidence is low
      responses:
        '200':
          description: Upload parsed and ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UploadCreateResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '413':
          description: File too large
        '415':
          description: Unsupported format
    get:
      tags: [Uploads]
      summary: List saved uploads
      operationId: listUploads
      security:
        - sessionCookie: []
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
      responses:
        '200':
          description: Upload list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UploadListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /uploads/{id}:
    get:
      tags: [Uploads]
      summary: Upload metadata
      operationId: getUpload
      security:
        - sessionCookie: []
      parameters:
        - $ref: '#/components/parameters/UploadId'
      responses:
        '200':
          description: Upload details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UploadDetail'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /uploads/{id}/preview:
    get:
      tags: [Uploads]
      summary: Preview parsed rows
      operationId: previewUpload
      security:
        - sessionCookie: []
      parameters:
        - $ref: '#/components/parameters/UploadId'
        - name: limit
          in: query
          schema:
            type: integer
            default: 5
      responses:
        '200':
          description: Headers, mapping and sample rows
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UploadPreviewResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /uploads/{id}/rows:
    get:
      tags: [Uploads]
      summary: Query parsed rows
      operationId: listUploadRows
      security:
        - sessionCookie: []
      parameters:
        - $ref: '#/components/parameters/UploadId'
        - name: status
          in: query
          schema:
            type: string
        - name: limit
          in: query
          schema:
            type: integer
            default: 100
        - name: offset
          in: query
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: Paginated rows
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UploadRowsResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /uploads/{id}/verify:
    post:
      tags: [Uploads]
      summary: Start verification for upload rows
      description: Creates a bulk job linked to the upload. Consumes 1 credit per valid email row.
      operationId: verifyUpload
      security:
        - sessionCookie: []
      parameters:
        - $ref: '#/components/parameters/UploadId'
      responses:
        '200':
          description: Bulk job queued
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BulkJobCreated'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/InsufficientCredits'
        '404':
          $ref: '#/components/responses/NotFound'
  /uploads/{id}/reverify:
    post:
      tags: [Uploads]
      summary: Re-run verification for an upload
      operationId: reverifyUpload
      security:
        - sessionCookie: []
      parameters:
        - $ref: '#/components/parameters/UploadId'
      responses:
        '200':
          description: Bulk job queued
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BulkJobCreated'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/InsufficientCredits'
        '404':
          $ref: '#/components/responses/NotFound'
  /uploads/{id}/export:
    get:
      tags: [Uploads]
      summary: Export upload with verification columns
      description: |
        CSV with original columns plus `ev_status`, `ev_score`, `ev_provider`, `ev_verified_at`.
        Free (no credits).
      operationId: exportUpload
      security:
        - sessionCookie: []
      parameters:
        - $ref: '#/components/parameters/UploadId'
      responses:
        '200':
          description: CSV file
          content:
            text/csv:
              schema:
                type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /webhooks:
    get:
      tags: [Webhooks]
      summary: List webhooks
      operationId: listWebhooks
      security:
        - sessionCookie: []
      responses:
        '200':
          description: Active webhooks
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      tags: [Webhooks]
      summary: Create webhook
      operationId: createWebhook
      security:
        - sessionCookie: []
      responses:
        '200':
          description: Webhook created with secret
        '401':
          $ref: '#/components/responses/Unauthorized'
  /webhooks/{id}:
    delete:
      tags: [Webhooks]
      summary: Revoke webhook
      operationId: revokeWebhook
      security:
        - sessionCookie: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Webhook revoked
        '401':
          $ref: '#/components/responses/Unauthorized'
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: API key prefixed with `ev_live_` (production) or `ev_test_` (sandbox)
    sessionCookie:
      type: apiKey
      in: cookie
      name: ev_session
      description: Browser session from login (panel operations)
  parameters:
    UploadId:
      name: id
      in: path
      required: true
      schema:
        type: string
      description: Upload public ID (UUID)
  schemas:
    VerifyRequest:
      type: object
      required: [email]
      properties:
        email:
          type: string
          format: email
          maxLength: 320
          example: john@company.com
    VerifyResponse:
      type: object
      properties:
        verificationId:
          type: string
          example: ver_abc123
        email:
          type: string
        status:
          type: string
          enum: [valid, invalid, risky, catch_all, unknown, processing, failed]
        score:
          type: integer
          nullable: true
        provider:
          type: string
          nullable: true
        enrichment:
          type: object
          additionalProperties: true
        creditsRemaining:
          type: integer
    VerificationRecord:
      type: object
      additionalProperties: true
    VerificationsListResponse:
      type: object
      properties:
        items:
          type: array
          items:
            type: object
        stats:
          type: object
    CreditBalance:
      type: object
      properties:
        balance:
          type: integer
          example: 150
    UsageSummary:
      type: object
      properties:
        periodDays:
          type: integer
        totalRequests:
          type: integer
        creditsUsed:
          type: integer
        byDay:
          type: array
          items:
            type: object
            properties:
              date:
                type: string
              requests:
                type: integer
              credits:
                type: integer
        bySource:
          type: object
          additionalProperties:
            type: integer
    UsageByKeysResponse:
      type: object
      properties:
        periodDays:
          type: integer
        keys:
          type: array
          items:
            $ref: '#/components/schemas/ApiKey'
    ApiKey:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        keyPrefix:
          type: string
        environment:
          type: string
          enum: [live, test]
          description: Derived from key prefix — `ev_test_` keys are sandbox.
        lastUsedAt:
          type: string
          format: date-time
          nullable: true
        createdAt:
          type: string
          format: date-time
          nullable: true
        usageCount30d:
          type: integer
        creditsUsed30d:
          type: integer
    ApiKeysListResponse:
      type: object
      properties:
        keys:
          type: array
          items:
            $ref: '#/components/schemas/ApiKey'
    CreateApiKeyRequest:
      type: object
      required: [name]
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 255
        environment:
          type: string
          enum: [live, test]
          default: live
          description: Use `test` for sandbox keys (`ev_test_…`) that do not consume credits.
    CreateApiKeyResponse:
      type: object
      properties:
        key:
          $ref: '#/components/schemas/ApiKey'
        secret:
          type: string
          example: ev_live_abc123...
    UpdateApiKeyRequest:
      type: object
      required: [name]
      properties:
        name:
          type: string
    UpdateApiKeyResponse:
      type: object
      properties:
        key:
          $ref: '#/components/schemas/ApiKey'
    BulkRequest:
      type: object
      properties:
        emails:
          type: array
          items:
            type: string
            format: email
        text:
          type: string
          description: Raw CSV or newline-separated emails
        filename:
          type: string
    BulkJobCreated:
      type: object
      properties:
        jobId:
          type: string
        total:
          type: integer
        creditsRequired:
          type: integer
        status:
          type: string
    BulkJobStatus:
      type: object
      additionalProperties: true
    ColumnMapping:
      type: object
      required: [email]
      properties:
        email:
          type: string
        first_name:
          type: string
        last_name:
          type: string
        company:
          type: string
        phone:
          type: string
        nip:
          type: string
    UploadStats:
      type: object
      properties:
        totalRows:
          type: integer
        validEmailRows:
          type: integer
        invalidRows:
          type: integer
        duplicateRows:
          type: integer
    UploadCreateResponse:
      type: object
      properties:
        uploadId:
          type: string
        format:
          type: string
        headers:
          type: array
          items:
            type: string
        suggestedMapping:
          $ref: '#/components/schemas/ColumnMapping'
        mappingConfidence:
          type: number
          format: float
        stats:
          $ref: '#/components/schemas/UploadStats'
        warnings:
          type: array
          items:
            type: string
        status:
          type: string
          example: ready
    UploadListItem:
      type: object
      properties:
        id:
          type: string
        originalFilename:
          type: string
        format:
          type: string
        totalRows:
          type: integer
        validEmailRows:
          type: integer
        status:
          type: string
        createdAt:
          type: string
          format: date-time
          nullable: true
    UploadListResponse:
      type: object
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/UploadListItem'
    UploadDetail:
      type: object
      additionalProperties: true
    UploadPreviewRow:
      type: object
      properties:
        rowIndex:
          type: integer
        email:
          type: string
          nullable: true
        rawData:
          type: object
          additionalProperties:
            type: string
        parseStatus:
          type: string
    UploadPreviewResponse:
      type: object
      properties:
        headers:
          type: array
          items:
            type: string
        columnMapping:
          $ref: '#/components/schemas/ColumnMapping'
        rows:
          type: array
          items:
            $ref: '#/components/schemas/UploadPreviewRow'
    UploadRowsResponse:
      type: object
      additionalProperties: true
    ErrorResponse:
      type: object
      properties:
        error:
          type: string
        balance:
          type: integer
        limit:
          type: integer
  responses:
    Unauthorized:
      description: Missing or invalid authentication
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          examples:
            unauthorized:
              value:
                error: unauthorized
            invalidKey:
              value:
                error: invalid_api_key
    BadRequest:
      description: Invalid request payload
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error: invalid_request
    InsufficientCredits:
      description: Not enough credits
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error: insufficient_credits
            balance: 0
    RateLimited:
      description: Rate limit exceeded (default 60/min per key)
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error: rate_limit_exceeded
    EngineUnavailable:
      description: Verification engine unavailable — no credit charged
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error: engine_unavailable
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error: not_found
