openapi: 3.1.0
info:
  title: AlFasil API
  version: "1.0.0"
  summary: Invoice and receipt data extraction with validation.
  description: |
    Upload invoices and receipts (PDF or images), receive structured, validated data.
    Amounts are decimal strings, dates are YYYY-MM-DD, currencies ISO 4217, countries ISO 3166-1 alpha-2.
    Errors always use the `Error` shape with English and Arabic messages.
servers:
  - url: https://alfasil.com
    description: AlFasil
security:
  - apiKey: []
tags:
  - name: Documents
  - name: Usage
  - name: Webhooks
paths:
  /v1/documents:
    post:
      tags: [Documents]
      summary: Upload a document
      description: |
        Send the raw file as the body with its Content-Type (recommended), or `multipart/form-data`
        with a `file` field for files up to 4 MB. UBL e-invoices (ZATCA, UAE PINT AE, Peppol) are
        accepted as `application/xml` and read without AI; an e-invoice counts as one page.
      operationId: uploadDocument
      parameters:
        - in: header
          name: X-File-Name
          schema: { type: string, maxLength: 200 }
          description: Original file name (URL-encoded).
        - in: header
          name: Idempotency-Key
          schema: { type: string, maxLength: 128 }
          description: Retrying with the same key returns the original document with HTTP 200.
        - in: header
          name: X-File-Sha256
          schema: { type: string, pattern: "^[a-f0-9]{64}$" }
        - in: header
          name: X-Client-Id
          schema: { type: string }
          description: Firm plan — attach to a client workspace.
        - in: header
          name: X-Page-Count
          schema: { type: integer, minimum: 1 }
          description: Page estimate for the quota check at upload. The real count is read from the file.
        - in: header
          name: X-Zatca-Qr
          schema: { type: string, maxLength: 4000 }
          description: Base64 content of the invoice's ZATCA QR code. The extracted data is checked against it. Invalid values are ignored.
      requestBody:
        required: true
        content:
          application/pdf:
            schema: { type: string, format: binary }
          image/jpeg:
            schema: { type: string, format: binary }
          image/png:
            schema: { type: string, format: binary }
          image/webp:
            schema: { type: string, format: binary }
          image/gif:
            schema: { type: string, format: binary }
          application/xml:
            schema: { type: string, format: binary }
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file: { type: string, format: binary }
                client_id: { type: string }
                zatca_qr: { type: string }
      responses:
        "202":
          description: Accepted and queued.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/DocumentEnvelope" }
              example:
                document: { id: doc_0mgf3k2q1zA8b2Kc9dX1, status: queued, source: api, file_name: invoice-4471.pdf, media_type: application/pdf, file_size: 29742, page_count: null, client_id: null, parent_id: null, created_at: "2026-10-09T08:55:10.000Z", processed_at: null, exported_at: null, approved_at: null, original_available: true, needs_review: false, needs_review_fields: [], data: null, warnings: [], error: null, model_used: null }
        "200":
          description: Idempotent replay — the document created earlier with the same key.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/DocumentEnvelope" }
        "400": { $ref: "#/components/responses/Error" }
        "401": { $ref: "#/components/responses/Error" }
        "402": { $ref: "#/components/responses/Error" }
        "403": { $ref: "#/components/responses/Error" }
        "409": { $ref: "#/components/responses/Error" }
        "411": { $ref: "#/components/responses/Error" }
        "413": { $ref: "#/components/responses/Error" }
        "415": { $ref: "#/components/responses/Error" }
        "429": { $ref: "#/components/responses/Error" }
    get:
      tags: [Documents]
      summary: List documents
      operationId: listDocuments
      parameters:
        - { in: query, name: status, schema: { type: string, example: "done,needs_review" } }
        - { in: query, name: client_id, schema: { type: string } }
        - { in: query, name: from, schema: { type: string, format: date } }
        - { in: query, name: to, schema: { type: string, format: date } }
        - { in: query, name: exported, schema: { type: string, enum: ["yes", "no"] } }
        - { in: query, name: limit, schema: { type: integer, minimum: 1, maximum: 200, default: 50 } }
        - { in: query, name: cursor, schema: { type: string } }
      responses:
        "200":
          description: A page of documents, newest first.
          content:
            application/json:
              schema:
                type: object
                required: [documents, next_cursor]
                properties:
                  documents: { type: array, items: { $ref: "#/components/schemas/Document" } }
                  next_cursor: { type: [string, "null"] }
        "401": { $ref: "#/components/responses/Error" }
  /v1/documents/{id}:
    parameters:
      - { in: path, name: id, required: true, schema: { type: string } }
    get:
      tags: [Documents]
      summary: Get a document
      operationId: getDocument
      responses:
        "200":
          description: The document.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/DocumentEnvelope" }
        "404": { $ref: "#/components/responses/Error" }
    patch:
      tags: [Documents]
      summary: Correct data and/or approve
      description: Replaces `data` (it is re-validated) and/or approves the document (clears review flags).
      operationId: updateDocument
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                data: { $ref: "#/components/schemas/ExtractedDocument" }
                approve: { type: boolean }
            example: { approve: true }
      responses:
        "200":
          description: Updated.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/DocumentEnvelope" }
        "400": { $ref: "#/components/responses/Error" }
        "404": { $ref: "#/components/responses/Error" }
    delete:
      tags: [Documents]
      summary: Delete a document (and its original file)
      operationId: deleteDocument
      responses:
        "200":
          description: Deleted.
          content:
            application/json:
              schema: { type: object, properties: { ok: { type: boolean }, deleted: { type: integer } } }
        "404": { $ref: "#/components/responses/Error" }
  /v1/documents/{id}/retry:
    post:
      tags: [Documents]
      summary: Re-process a failed document
      operationId: retryDocument
      parameters:
        - { in: path, name: id, required: true, schema: { type: string } }
      responses:
        "200": { description: Queued again. }
        "400": { $ref: "#/components/responses/Error" }
        "410": { $ref: "#/components/responses/Error" }
  /v1/documents/bulk:
    post:
      tags: [Documents]
      summary: Bulk approve, mark exported, or delete
      operationId: bulkDocuments
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [action, ids]
              properties:
                action: { type: string, enum: [approve, mark_exported, delete] }
                ids: { type: array, items: { type: string }, minItems: 1, maxItems: 100 }
            example: { action: mark_exported, ids: [doc_0mgf3k2q1zA8b2Kc9dX1] }
      responses:
        "200":
          description: Done.
          content:
            application/json:
              schema: { type: object, properties: { ok: { type: boolean }, count: { type: integer } } }
  /v1/usage:
    get:
      tags: [Usage]
      summary: Pages used this month and history
      operationId: getUsage
      responses:
        "200":
          description: Usage.
          content:
            application/json:
              example: { plan: starter, pages_limit: 500, extra_pages: 250, current_month: { pages: 42, documents: 39, pending_pages: 2 }, history: [{ month: "2026-10", pages: 42, documents: 39 }] }
webhooks:
  document.processed:
    post:
      summary: A document finished processing
      parameters:
        - in: header
          name: AlFasil-Signature
          required: true
          schema: { type: string, example: "t=1791536112,v1=5f1c…" }
          description: HMAC-SHA256 of "<t>.<raw body>" with the webhook's signing secret, hex-encoded.
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookEvent" }
      responses:
        "200": { description: Any 2xx acknowledges the event. }
  document.failed:
    post:
      summary: A document could not be processed
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookEvent" }
      responses:
        "200": { description: Any 2xx acknowledges the event. }
components:
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      description: "`Authorization: Bearer alfasil_live_…` (keys created before the rename start with `qayd_live_` and keep working)"
  responses:
    Error:
      description: Error
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: { code: quota_exceeded, message_en: "You've used all pages in your plan this month.", message_ar: "استهلكت كل صفحات باقتك لهذا الشهر." } }
  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message_en, message_ar]
          properties:
            code: { type: string }
            message_en: { type: string }
            message_ar: { type: string }
    Decimal:
      type: [string, "null"]
      pattern: "^-?\\d+(\\.\\d+)?$"
      examples: ["1680.5"]
    Party:
      type: object
      properties:
        name: { type: [string, "null"] }
        tax_id: { type: [string, "null"] }
        tax_id_type: { type: [string, "null"], enum: [TRN, VAT, EIN, GSTIN, ABN, other, null] }
        address: { type: [string, "null"] }
        country: { type: [string, "null"], pattern: "^[A-Z]{2}$" }
    LineItem:
      type: object
      properties:
        description: { type: [string, "null"] }
        quantity: { $ref: "#/components/schemas/Decimal" }
        unit_price: { $ref: "#/components/schemas/Decimal" }
        discount: { $ref: "#/components/schemas/Decimal" }
        tax_rate: { $ref: "#/components/schemas/Decimal" }
        tax_amount: { $ref: "#/components/schemas/Decimal" }
        line_total: { $ref: "#/components/schemas/Decimal" }
        line_total_includes_tax: { type: [boolean, "null"] }
    ExtractedDocument:
      type: object
      properties:
        document_type: { type: string, enum: [invoice, receipt, credit_note, proforma, other] }
        languages: { type: array, items: { type: string } }
        pages: { type: array, items: { type: integer } }
        currency: { type: [string, "null"], pattern: "^[A-Z]{3}$" }
        supplier: { $ref: "#/components/schemas/Party" }
        buyer: { $ref: "#/components/schemas/Party" }
        invoice_number: { type: [string, "null"] }
        po_number: { type: [string, "null"], description: Purchase order (PO / LPO) number as printed }
        issue_date: { type: [string, "null"], format: date }
        issue_date_original: { type: [string, "null"], description: Printed Hijri date, if any }
        due_date: { type: [string, "null"], format: date }
        due_date_original: { type: [string, "null"] }
        period_start: { type: [string, "null"], format: date, description: Start of the service or billing period the invoice covers }
        period_end: { type: [string, "null"], format: date, description: End of that period }
        stated_page_count: { type: [integer, "null"], description: "Page count printed on the document (\"Page 1 of 2\" gives 2); compared with pages for MISSING_PAGES" }
        line_items: { type: array, items: { $ref: "#/components/schemas/LineItem" } }
        subtotal: { $ref: "#/components/schemas/Decimal" }
        discount_total: { $ref: "#/components/schemas/Decimal" }
        shipping: { $ref: "#/components/schemas/Decimal" }
        tax_breakdown:
          type: array
          items:
            type: object
            properties:
              rate: { $ref: "#/components/schemas/Decimal" }
              taxable_amount: { $ref: "#/components/schemas/Decimal" }
              tax_amount: { $ref: "#/components/schemas/Decimal" }
        tax_total: { $ref: "#/components/schemas/Decimal" }
        total: { $ref: "#/components/schemas/Decimal" }
        amount_paid: { $ref: "#/components/schemas/Decimal" }
        balance_due: { $ref: "#/components/schemas/Decimal" }
        payment_terms: { type: [string, "null"] }
        field_confidence: { type: object, additionalProperties: { type: number } }
        field_pages: { type: object, additionalProperties: { type: integer } }
        notes: { type: [string, "null"] }
    Warning:
      type: object
      required: [code, severity, fields, message_en, message_ar]
      properties:
        code: { type: string, examples: [TOTAL_MISMATCH] }
        severity: { type: string, enum: [warning, info] }
        fields: { type: array, items: { type: string } }
        params: { type: object }
        message_en: { type: string }
        message_ar: { type: string }
    Document:
      type: object
      required: [id, status, source, created_at, data, warnings]
      properties:
        id: { type: string }
        status: { type: string, enum: [queued, processing, done, needs_review, failed] }
        source: { type: string, enum: [upload, email, api, free_tool] }
        file_name: { type: [string, "null"] }
        media_type: { type: string }
        file_size: { type: integer }
        page_count: { type: [integer, "null"] }
        client_id: { type: [string, "null"] }
        parent_id: { type: [string, "null"], description: Set on invoices split out of a multi-invoice file }
        created_at: { type: string, format: date-time }
        processed_at: { type: [string, "null"], format: date-time }
        exported_at: { type: [string, "null"], format: date-time }
        approved_at: { type: [string, "null"], format: date-time }
        original_available: { type: boolean }
        needs_review: { type: boolean }
        needs_review_fields: { type: array, items: { type: string } }
        data: { oneOf: [{ $ref: "#/components/schemas/ExtractedDocument" }, { type: "null" }] }
        warnings: { type: array, items: { $ref: "#/components/schemas/Warning" } }
        error:
          oneOf:
            - type: "null"
            - type: object
              properties:
                code: { type: string }
                message_en: { type: string }
                message_ar: { type: string }
        model_used: { type: [string, "null"], description: "AI model id, or einvoice:<format> for e-invoice XML" }
        einvoice_format: { type: [string, "null"], enum: [zatca, pint_ae, ubl, null], description: Set when the data was read from an e-invoice XML (no AI) }
        zatca_qr:
          description: Decoded ZATCA QR code, when the document has one.
          oneOf:
            - type: "null"
            - type: object
              properties:
                seller_name: { type: string }
                vat_number: { type: string }
                timestamp: { type: string }
                total: { type: string }
                vat_total: { type: string }
                phase: { type: integer, enum: [1, 2] }
    DocumentEnvelope:
      type: object
      required: [document]
      properties:
        document: { $ref: "#/components/schemas/Document" }
    WebhookEvent:
      type: object
      required: [id, type, created_at, data]
      properties:
        id: { type: string }
        type: { type: string, enum: [document.processed, document.failed] }
        created_at: { type: string, format: date-time }
        data:
          type: object
          properties:
            document: { $ref: "#/components/schemas/Document" }
