openapi: 3.0.3
info:
  title: exdata API
  version: 1.0.0
  description: >
    exdata document extraction API.
servers:
  - url: https://www.exdata.app/api/v1
    description: Production API
security:
  - bearerAuth: []
paths:
  /ping:
    get:
      summary: Check API authentication
      operationId: ping
      parameters:
        - $ref: "#/components/parameters/RequestId"
      responses:
        "200":
          description: Authenticated API status.
          content:
            application/json:
              schema:
                type: object
                required: [status]
                properties:
                  status:
                    type: boolean
                    example: true
  /documents:
    get:
      summary: List documents
      operationId: listDocuments
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - name: sort
          in: query
          schema:
            type: string
            enum: [created_at, -created_at]
          description: Sort by creation time ascending or descending.
        - name: per_page
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 100
      responses:
        "200":
          description: Paginated document list.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaginatedDocuments"
        "429":
          $ref: "#/components/responses/RateLimited"
    post:
      summary: Upload a document
      operationId: uploadDocument
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/RequestId"
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file:
                  type: string
                  format: binary
                  description: Document file, maximum 500000 KB.
                text:
                  type: string
                  nullable: true
                  description: Additional body text or document context.
                custom_types:
                  type: array
                  nullable: true
                  items:
                    type: string
                  description: Optional type extensions for workflows beyond the standard document types. Standard types are invoice, credit-note, reminder, salary-statement, bank-statement, contract, balance-sheet, tax-assessment-note, timesheet, letter, and other.
                requester:
                  type: string
                  nullable: true
                  description: Caller reference, integration name, or request source.
                locale:
                  type: string
                  nullable: true
                  example: en
                allow_ai_processing:
                  type: boolean
                  default: true
                  description: Legacy processing flag. Most integrations omit this field.
      responses:
        "201":
          description: Document accepted and queued for processing.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DocumentResource"
        "422":
          $ref: "#/components/responses/ValidationError"
        "409":
          $ref: "#/components/responses/IdempotencyConflict"
        "429":
          $ref: "#/components/responses/UploadRateLimited"
        "402":
          description: Account does not have enough credits, so extraction was not queued.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/InsufficientCreditsError"
  /x402/extractions:
    post:
      summary: Upload a document with x402 payment
      operationId: createAnonymousX402Extraction
      security: []
      parameters:
        - $ref: "#/components/parameters/PaymentSignature"
        - $ref: "#/components/parameters/RequestId"
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file:
                  type: string
                  format: binary
                  description: Document file, maximum 500000 KB.
                text:
                  type: string
                  nullable: true
                  description: Additional body text or document context.
                custom_types:
                  type: array
                  nullable: true
                  items:
                    type: string
                  description: Optional type extensions for workflows beyond the standard document types.
                requester:
                  type: string
                  nullable: true
                  description: Caller reference, integration name, or request source.
                locale:
                  type: string
                  nullable: true
                  example: en
                allow_ai_processing:
                  type: boolean
                  default: true
                  description: Legacy processing flag. Most integrations omit this field.
      responses:
        "200":
          description: Same payment and same request replayed; the cached anonymous extraction response is returned.
          headers:
            X402-Replayed:
              description: Present with value `true` when the response is an idempotent replay of a settled x402 payment.
              schema:
                type: string
                enum: ["true"]
            PAYMENT-RESPONSE:
              description: Base64-encoded x402 SettlementResponse JSON for the settled payment.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/X402ExtractionAccepted"
        "201":
          description: Payment settled, document accepted, and anonymous polling access issued.
          headers:
            PAYMENT-RESPONSE:
              description: Base64-encoded x402 SettlementResponse JSON.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/X402ExtractionAccepted"
        "400":
          description: The supplied x402 payment-identifier extension is malformed.
          headers:
            PAYMENT-REQUIRED:
              description: Base64-encoded x402 PaymentRequired JSON.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "402":
          description: x402 payment is required, failed verification, or failed settlement.
          headers:
            PAYMENT-REQUIRED:
              description: Base64-encoded x402 PaymentRequired JSON.
              schema:
                type: string
            PAYMENT-RESPONSE:
              description: Base64-encoded x402 SettlementResponse JSON when settlement was attempted.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/X402PaymentError"
        "409":
          description: The x402 payment was already used for a different extraction request, is still processing, or references a settled extraction that is no longer available.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          description: Anonymous x402 extraction is not configured.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
  /x402/extractions/{document}:
    get:
      summary: Retrieve an anonymous x402 extraction
      operationId: getAnonymousX402Extraction
      security: []
      parameters:
        - $ref: "#/components/parameters/DocumentId"
        - $ref: "#/components/parameters/X402AccessToken"
        - $ref: "#/components/parameters/X402AccessTokenQuery"
        - $ref: "#/components/parameters/PaymentSignature"
        - $ref: "#/components/parameters/RequestId"
      responses:
        "200":
          description: Document metadata, status, and completed extraction fields when available.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/X402ExtractionAccepted"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
  /documents/{document}:
    get:
      summary: Retrieve a document
      operationId: getDocument
      parameters:
        - $ref: "#/components/parameters/DocumentId"
        - $ref: "#/components/parameters/RequestId"
      responses:
        "200":
          description: Document metadata, status, and completed results when available.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DocumentResource"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
    delete:
      summary: Delete a document
      operationId: deleteDocument
      parameters:
        - $ref: "#/components/parameters/DocumentId"
        - $ref: "#/components/parameters/RequestId"
      responses:
        "204":
          description: Document deleted.
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
  /documents/{document}/thumbnail:
    get:
      summary: Download document thumbnail
      operationId: getDocumentThumbnail
      parameters:
        - $ref: "#/components/parameters/DocumentId"
        - $ref: "#/components/parameters/RequestId"
      responses:
        "200":
          description: Thumbnail binary.
          content:
            application/octet-stream:
              schema:
                type: string
                format: binary
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
  /documents/{document}/previews:
    get:
      summary: List document previews
      operationId: listDocumentPreviews
      parameters:
        - $ref: "#/components/parameters/DocumentId"
        - $ref: "#/components/parameters/RequestId"
      responses:
        "200":
          description: Preview metadata for a document.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Preview"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
  /documents/{document}/extractions:
    get:
      summary: List document extractions
      operationId: listDocumentExtractions
      parameters:
        - $ref: "#/components/parameters/DocumentId"
        - $ref: "#/components/parameters/RequestId"
      responses:
        "200":
          description: Extracted fields keyed by field name.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    $ref: "#/components/schemas/Extractions"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
  /previews/{preview}:
    get:
      summary: Download a preview file
      operationId: getPreview
      parameters:
        - name: preview
          in: path
          required: true
          schema:
            type: integer
        - $ref: "#/components/parameters/RequestId"
      responses:
        "200":
          description: Preview binary.
          content:
            application/octet-stream:
              schema:
                type: string
                format: binary
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: API token
      description: Account-scoped API token created in the exdata app.
  parameters:
    DocumentId:
      name: document
      in: path
      required: true
      schema:
        type: integer
      description: Document ID.
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      schema:
        type: string
        maxLength: 255
      description: Optional unique key for safely retrying document uploads. Reusing the same key with the same payload returns the stored response and does not upload a duplicate document or reserve another credit.
    PaymentSignature:
      name: PAYMENT-SIGNATURE
      in: header
      required: false
      schema:
        type: string
      description: Base64- or Base64URL-encoded x402 v2 PaymentPayload JSON. Omit this header to receive payment requirements in a 402 response.
    X402AccessToken:
      name: X-Access-Token
      in: header
      required: false
      schema:
        type: string
      description: Anonymous capability token returned by a successful x402 upload. Prefer this header when polling anonymous extraction status.
    X402AccessTokenQuery:
      name: access_token
      in: query
      required: false
      schema:
        type: string
      description: Anonymous capability token returned by a successful x402 upload. Header-based access is preferred when possible.
    RequestId:
      name: X-Request-ID
      in: header
      required: false
      schema:
        type: string
        maxLength: 128
      description: Optional caller-supplied request ID. exdata returns this value, or a generated ID, in the `X-Request-ID` response header and error body.
  responses:
    Forbidden:
      description: The token cannot access this resource.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
    NotFound:
      description: Resource not found.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
    RateLimited:
      description: Request rate limit exceeded.
      content:
        application/json:
          schema:
            allOf:
              - $ref: "#/components/schemas/ErrorEnvelope"
              - type: object
                properties:
                  retry_after:
                    type: integer
                    nullable: true
    UploadRateLimited:
      description: Request rate limit exceeded, or a test-mode token reached its daily sandbox upload limit.
      content:
        application/json:
          schema:
            oneOf:
              - allOf:
                  - $ref: "#/components/schemas/ErrorEnvelope"
                  - type: object
                    properties:
                      retry_after:
                        type: integer
                        nullable: true
              - $ref: "#/components/schemas/TestModeLimitError"
    ValidationError:
      description: Request validation failed.
      content:
        application/json:
          schema:
            allOf:
              - $ref: "#/components/schemas/ErrorEnvelope"
              - type: object
                properties:
                  errors:
                    type: object
                    additionalProperties:
                      type: array
                      items:
                        type: string
    IdempotencyConflict:
      description: The idempotency key is still processing or was reused with a different request payload.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
  schemas:
    ErrorEnvelope:
      type: object
      required: [message, code, request_id]
      properties:
        message:
          type: string
        code:
          type: string
          example: validation_failed
        request_id:
          type: string
          example: req_01HZY9Z4Z6E7B3F42X8E8Y9ABC
    PaginatedDocuments:
      type: object
      required: [data, links, meta]
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/Document"
        links:
          type: object
        meta:
          type: object
    DocumentResource:
      type: object
      required: [data]
      properties:
        data:
          $ref: "#/components/schemas/Document"
    X402ExtractionAccepted:
      type: object
      required: [data, anonymous_access, payment]
      properties:
        data:
          $ref: "#/components/schemas/Document"
        anonymous_access:
          type: object
          required: [status_url, expires_at, token_header]
          properties:
            status_url:
              type: string
              format: uri
              description: Anonymous polling URL for this extraction. Send the returned access token in the `X-Access-Token` header when polling; query-string tokens are accepted for compatibility but are not embedded in generated URLs.
            access_token:
              type: string
              description: Capability token returned on upload and replay responses. Use it as `X-Access-Token` when polling.
            expires_at:
              type: string
              format: date-time
              nullable: true
            token_header:
              type: string
              example: X-Access-Token
        payment:
          $ref: "#/components/schemas/X402PaymentSummary"
    X402PaymentSummary:
      type: object
      properties:
        scheme:
          type: string
          example: exact
        network:
          type: string
          example: eip155:8453
        asset:
          type: string
          example: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
        amount:
          type: string
          example: "150000"
        pay_to:
          type: string
          nullable: true
        payer:
          type: string
          nullable: true
        transaction:
          type: string
          nullable: true
    X402PaymentError:
      allOf:
        - $ref: "#/components/schemas/ErrorEnvelope"
        - type: object
          required: [x402]
          properties:
            x402:
              $ref: "#/components/schemas/X402PaymentRequired"
            verification:
              type: object
              nullable: true
            settlement:
              type: object
              nullable: true
    X402PaymentRequired:
      type: object
      required: [x402Version, resource, accepts]
      properties:
        x402Version:
          type: integer
          enum: [2]
        error:
          type: string
          nullable: true
        resource:
          type: object
          required: [url, description, mimeType]
          properties:
            url:
              type: string
              format: uri
            description:
              type: string
            mimeType:
              type: string
              example: application/json
        accepts:
          type: array
          items:
            $ref: "#/components/schemas/X402PaymentRequirement"
        extensions:
          type: object
          additionalProperties: true
    X402PaymentRequirement:
      type: object
      required: [scheme, network, asset, amount, payTo]
      properties:
        scheme:
          type: string
          example: exact
        network:
          type: string
          description: CAIP-2 network identifier.
          example: eip155:8453
        asset:
          type: string
          description: Token contract or network-specific asset identifier.
          example: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
        amount:
          type: string
          description: Atomic token amount required by the x402 exact payment scheme.
          example: "150000"
        payTo:
          type: string
          description: Merchant wallet address.
        maxTimeoutSeconds:
          type: integer
          example: 60
        extra:
          type: object
          additionalProperties: true
    Document:
      type: object
      properties:
        id:
          type: integer
        mode:
          type: string
          enum: [live, test]
          description: Live documents affect billing and live usage reporting. Test documents are created by test-mode API tokens and are excluded from live billing/reporting.
        status:
          type: string
          enum: [pending, completed, error, blocked]
        processing_stage:
          type: string
          nullable: true
          example: queued
        processing_error:
          type: string
          nullable: true
        blocked_reason:
          type: string
          nullable: true
          example: insufficient_credits
        scanner_status:
          type: string
          nullable: true
          enum: [clean, rejected]
        scanner_provider:
          type: string
          nullable: true
          example: local_noop
        scanner_message:
          type: string
          nullable: true
        scanned_at:
          type: string
          format: date-time
          nullable: true
        processing_started_at:
          type: string
          format: date-time
          nullable: true
        processed_at:
          type: string
          format: date-time
          nullable: true
        filename:
          type: string
          example: invoice.pdf
        file_format:
          type: string
          example: pdf
        file_size:
          type: integer
          example: 240123
        additional_text:
          type: string
          nullable: true
        additional_text_plain:
          type: string
          nullable: true
        custom_types:
          type: array
          nullable: true
          items:
            type: string
        requester:
          type: string
          nullable: true
        locale:
          type: string
          nullable: true
          example: en
        number_of_pages:
          type: integer
          nullable: true
        extracted_text:
          type: string
          nullable: true
        extracted_text_plain:
          type: string
          nullable: true
        origin:
          type: string
          nullable: true
          enum: [api, app]
        ai_processing:
          type: boolean
          description: Whether extraction processing is enabled for this document.
        is_e_invoice:
          type: boolean
        thumbnail:
          type: string
          format: uri
          nullable: true
        previews:
          type: array
          nullable: true
          items:
            $ref: "#/components/schemas/Preview"
        extractions:
          nullable: true
          oneOf:
            - $ref: "#/components/schemas/Extractions"
        latest_extraction_run:
          nullable: true
          oneOf:
            - $ref: "#/components/schemas/ExtractionRun"
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    ExtractionRun:
      type: object
      properties:
        id:
          type: integer
        mode:
          type: string
          enum: [live, test]
        source:
          type: string
          nullable: true
          enum: [api, app]
        status:
          type: string
          enum: [pending, processing, completed, blocked, error]
        blocked_reason:
          type: string
          nullable: true
          example: insufficient_credits
        error_code:
          type: string
          nullable: true
        error_message:
          type: string
          nullable: true
        extraction_schema_version:
          type: string
          nullable: true
          example: "2026-05-17"
        extractor_version:
          type: string
          nullable: true
          example: document:2026-05-17
        ai_prompt_version:
          type: string
          nullable: true
          example: document-ai:2026-05-17
        normalization_version:
          type: string
          nullable: true
          example: base:2026-05-10
        started_at:
          type: string
          format: date-time
          nullable: true
        completed_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
    Preview:
      type: object
      properties:
        id:
          type: integer
        filename:
          type: string
        file_format:
          type: string
        file_size:
          type: integer
        preview:
          type: string
          format: uri
    Extractions:
      type: object
      description: >
        Extraction fields keyed by normalized field name. Missing or uncertain
        values are represented as JSON null, not placeholder strings. Standard
        keys include
        type, title, date, issue_date, payment_due_date, period_start, period_end,
        document_number, customer_id, contract_id, order_id, reference_id,
        payment_reference, payment_status, currency,
        net_amount, gross_amount, opening_balance, closing_balance, tax_amount,
        tax_rate, tax_breakdowns, tax_system, taxability, tax_collection_mechanism,
        cross_border_tax_treatment, supply_type,
        tax_exemption_reason, account_holder, iban, bic, bank, sender_name,
        sender_street, sender_zip, sender_city, sender_state,
        sender_country_code, sender_tax_id, sender_vat_number, recipient_name,
        recipient_street, recipient_zip, recipient_city, recipient_state,
        recipient_country_code, recipient_tax_id,
        recipient_vat_number, email, phone_number, website, company_register_id,
        cost_center, note, from, from_email, to,
        to_email, subject, message_id, and in_reply_to. Custom extraction fields
        can add additional keys.
      additionalProperties:
        $ref: "#/components/schemas/ExtractionField"
      example:
        type:
          value: invoice
          candidates: [invoice]
        document_number:
          value: RE-2026-1048
          candidates: [RE-2026-1048]
        gross_amount:
          value: "1284.60"
          candidates: ["1.284,60 EUR", "EUR 1,284.60"]
        tax_breakdowns:
          value:
            - taxable_amount: "1079.50"
              tax_amount: "205.10"
              tax_rate: "19.00"
              taxability: taxable
              tax_collection_mechanism: seller_collected
              tax_exemption_reason: null
          candidates: ["VAT 19% 205.10"]
    ExtractionField:
      type: object
      required: [value, candidates]
      properties:
        value:
          nullable: true
          description: >
            Normalized extracted value. Most fields use strings or null. Amounts
            are dot-decimal strings with two digits, dates use YYYY-MM-DD,
            currency uses ISO 4217, and tax_breakdowns uses an array of
            TaxBreakdown rows.
          oneOf:
            - type: string
            - type: array
              items:
                $ref: "#/components/schemas/TaxBreakdown"
            - type: object
        candidates:
          type: array
          nullable: true
          description: Raw or derived candidate values considered for this field.
          items: {}
    TaxBreakdown:
      type: object
      description: One visible tax rate or tax category from the document.
      properties:
        taxable_amount:
          type: string
          nullable: true
          pattern: '^-?[0-9]+(\.[0-9]{2})$'
          example: "1079.50"
        tax_amount:
          type: string
          nullable: true
          pattern: '^-?[0-9]+(\.[0-9]{2})$'
          example: "205.10"
        tax_rate:
          type: string
          nullable: true
          pattern: '^-?[0-9]+(\.[0-9]{2})$'
          example: "19.00"
        taxability:
          type: string
          nullable: true
          enum: [taxable, zero_rated, exempt, out_of_scope]
        tax_collection_mechanism:
          type: string
          nullable: true
          enum: [seller_collected, reverse_charge, buyer_use_tax, not_collected]
        tax_exemption_reason:
          type: string
          nullable: true
          description: Visible exemption, zero-rating, out-of-scope, or reverse-charge reason.
    InsufficientCreditsError:
      allOf:
        - $ref: "#/components/schemas/ErrorEnvelope"
        - type: object
          required: [available_credits, required_credits, document]
          properties:
            available_credits:
              type: integer
              example: 0
            required_credits:
              type: integer
              example: 1
            document:
              $ref: "#/components/schemas/Document"
    TestModeLimitError:
      allOf:
        - $ref: "#/components/schemas/ErrorEnvelope"
        - type: object
          required: [account_daily_document_limit, user_daily_document_limit, token_daily_document_limit]
          properties:
            account_daily_document_limit:
              type: integer
              example: 25
            user_daily_document_limit:
              type: integer
              example: 50
            token_daily_document_limit:
              type: integer
              example: 10
