openapi: 3.1.0

info:
  title: DocQuote API
  version: 1.0.0
  description: |
    DocQuote Platform generates professional PDF documents — sales quotes, proforma invoices,
    invoices, credit notes, receipts, and purchase orders — from structured JSON. Designed for
    autonomous AI agents and direct API integrations.
    One API key works across all document types. All text fields accept UTF-8 content.

    ## Authentication
    All `/v1/*/pdf` and `/v1/credits/*` endpoints require an API key in the `x-api-key` header.
    **The `/v1/*/preview` endpoints are public and do not require authentication.**
    Obtain a free key via `POST /v1/keys/register` (no account required).

    ## Free tier and credits
    Each API key includes **5 free generations shared across all document types** (quotes, invoices,
    proformas, receipts, credit notes, and POs all draw from the same pool). After that, credits must
    be purchased via `POST /v1/credits/checkout` (paid in USD via PayPal). Credits never expire.
    Monitor your quota with the `X-DocQuote-Free-Remaining`, `X-DocQuote-Usage`, and
    `X-DocQuote-Credits-Remaining` response headers (uniform prefix across all document types).

    ## Idempotency
    Send an `Idempotency-Key` header (max 256 chars) to make PDF generation idempotent. Identical
    keys return cached PDFs without charging again. A different payload with the same key returns 409.

servers:
  - url: http://localhost:3000
    description: Local development
  - url: https://api.docquote.dev
    description: Production

tags:
  - name: health
    description: Liveness and readiness probes
  - name: keys
    description: API key registration
  - name: quote
    description: Quote calculation and PDF generation (requires authentication)
  - name: demo
    description: Unauthenticated demo endpoint with watermark and restrictions
  - name: invoice
    description: Invoice calculation and PDF generation (requires authentication)
  - name: po
    description: Purchase order calculation and PDF generation (requires authentication)
  - name: proforma
    description: Proforma invoice calculation and PDF generation (requires authentication)
  - name: credit_note
    description: Credit note calculation and PDF generation (requires authentication)
  - name: receipt
    description: Receipt calculation and PDF generation (requires authentication)
  - name: credits
    description: Credit pack purchases via PayPal (USD)

# ─── Security ────────────────────────────────────────────────────────────────

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: x-api-key
      description: API key obtained from `POST /v1/keys/register`. Format `dq_live_<hex>`.

# ─── Reusable components ─────────────────────────────────────────────────────

  headers:
    X-Request-Id:
      description: UUID assigned to this request. Include in support requests.
      schema:
        type: string
        format: uuid
    X-DocQuote-Version:
      description: API version string.
      schema:
        type: string
        example: "1.0.0"
    X-DocQuote-Free-Remaining:
      description: Free generations remaining on this API key (0–5).
      schema:
        type: integer
        minimum: 0
        maximum: 5
    X-DocQuote-Usage:
      description: Total generations consumed by this API key.
      schema:
        type: integer
        minimum: 0
    X-DocQuote-Credits-Remaining:
      description: |
        Paid credit balance after this request. Only present on responses that consumed
        a credit (i.e., free tier was already exhausted). Use this to proactively detect
        when to call `POST /v1/credits/checkout`.
      schema:
        type: integer
        minimum: 0
    X-Generation-Ms:
      description: |
        PDF generation time in milliseconds. Present only on freshly generated PDFs.
        **Absent on cache hits** (when `Content-Disposition` contains `quote-cached.pdf`).
      schema:
        type: integer
        minimum: 0
    Retry-After:
      description: Seconds to wait before retrying after a 429 response.
      schema:
        type: integer
        minimum: 0
    RateLimit:
      description: Rate limit state per RFC 9110 draft-7 (`limit`, `remaining`, `reset`).
      schema:
        type: string
    X-DocQuote-Demo:
      description: Present and set to `"true"` on all demo endpoint responses.
      schema:
        type: string
        enum: ["true"]

  schemas:
    QuoteItem:
      type: object
      required: [name, qty, unitPrice]
      properties:
        name:
          type: string
          maxLength: 200
          description: Line item description.
          example: "Widget A"
        qty:
          type: number
          exclusiveMinimum: 0
          description: Quantity (must be positive).
          example: 2
        unitPrice:
          type: number
          minimum: 0
          description: Price per unit (non-negative).
          example: 49.99
        discount:
          type: number
          minimum: 0
          description: Absolute discount applied to this line. Defaults to 0. Must not exceed `qty × unitPrice` (line total cannot be negative).
          example: 5.00
        total:
          type: number
          minimum: 0
          description: |
            Override the computed line total. If omitted, calculated as `qty * unitPrice - discount`.
            Must not be negative.
          example: 94.98

    QuoteRequest:
      type: object
      required: [items]
      properties:
        quoteNumber:
          type: string
          description: Custom quote number. Auto-generated as `DQ-<timestamp>` if omitted.
          example: "Q-2024-001"
        date:
          type: string
          format: date
          description: Quote date (ISO 8601). Defaults to today.
          example: "2024-07-15"
        currency:
          type: string
          description: ISO 4217 currency code. Defaults to `USD`.
          example: "USD"
        company:
          type: object
          properties:
            name:
              type: string
              example: "Acme Corp"
            businessLine:
              type: string
              maxLength: 200
              description: Industry or business line (e.g., giro). Printed below company name.
              example: "Software Development"
            taxId:
              type: string
              example: "12-3456789"
            taxIdLabel:
              type: string
              description: Label shown next to the company tax ID (e.g., `EIN`, `VAT`, `RUT`).
              example: "EIN"
            address:
              type: string
              example: "123 Main St, Springfield, IL 62701"
            phone:
              type: string
              example: "+1 (555) 000-1234"
            email:
              type: string
              format: email
              example: "billing@acme.com"
            logo:
              type: string
              description: |
                Company logo. Accepts either:
                - `data:image/*;base64,...` — inline base64 image (data URI)
                - `https://...` — remote URL fetched server-side (not allowed in demo endpoint)
              example: "data:image/png;base64,iVBORw0KGgo..."
            primaryColor:
              type: string
              pattern: "^#[0-9A-Fa-f]{6}$"
              description: Hex accent color used in the PDF header. Defaults to `#0F172A`.
              example: "#1D4ED8"
        customer:
          type: object
          properties:
            name:
              type: string
              example: "Jane Smith"
            company:
              type: string
              example: "Buyer Inc."
            taxId:
              type: string
              example: "98-7654321"
            taxIdLabel:
              type: string
              maxLength: 50
              description: Label shown next to the customer tax ID (e.g., `RUT`, `EIN`). Defaults to `Tax ID`.
              example: "RUT"
            address:
              type: string
              example: "456 Oak Ave, Portland, OR 97201"
            phone:
              type: string
              example: "+1 (555) 999-8765"
            email:
              type: string
              format: email
              example: "jane@buyer.com"
        items:
          type: array
          minItems: 1
          maxItems: 100
          items:
            $ref: '#/components/schemas/QuoteItem'
        taxRate:
          type: number
          minimum: 0
          maximum: 1
          description: Tax rate as a decimal between 0 and 1 inclusive (e.g., `0.08` = 8%). Defaults to 0.
          example: 0.08
        taxLabel:
          type: string
          description: Label shown next to tax amount. Defaults to `Tax`.
          example: "VAT"
        locale:
          type: string
          description: BCP 47 locale for number and date formatting.
          example: "en-US"
        subtotal:
          type: number
          minimum: 0
          description: Override subtotal. Auto-calculated from items if omitted.
          example: 189.96
        tax:
          type: number
          minimum: 0
          description: Override tax amount. Auto-calculated from `subtotal * taxRate` if omitted.
          example: 15.20
        total:
          type: number
          minimum: 0
          description: Override total. Auto-calculated from `subtotal + tax` if omitted.
          example: 205.16
        notes:
          type: string
          maxLength: 2000
          description: Free-text notes printed at the bottom of the quote.
          example: "Prices valid for 30 days. All amounts in USD."
        validityText:
          type: string
          maxLength: 500
          description: Validity statement shown below totals.
          example: "This quote is valid for 30 days from the date of issue."
        salesRep:
          type: string
          maxLength: 200
          description: Sales representative name printed on the quote.
          example: "John Doe"
        quoteTitle:
          type: string
          maxLength: 100
          description: |
            Title printed prominently on the quote. Defaults by locale:
            `es-*` → "Cotización", `fr-*` → "Devis", `pt-*` → "Cotação", `de-*` → "Angebot", else "Quotation".
            Provide this field to override the locale-derived default.
          example: "Cotización"
        paymentTerms:
          type: string
          maxLength: 200
          description: Payment terms printed on the quote (e.g., "Net 30", "50% advance").
          example: "50% anticipo, 50% contra entrega"
        commercialNotes:
          type: string
          maxLength: 2000
          description: Commercial notes printed after the items table (nota comercial).
          example: "Prices include 3 months of post-implementation support."
        additionalNotes:
          type: string
          maxLength: 2000
          description: Additional notes printed after commercialNotes (notas adicionales).
          example: "Offer valid based on RFP No. 2024-MIN-047."
        termsAndConditions:
          type: string
          maxLength: 10000
          description: |
            Full terms and conditions text. If present, rendered as a final page in the PDF.
            Supports plain text with newlines.
          example: "1. SCOPE\nThe provider agrees to...\n\n2. IP RIGHTS\n..."

    QuoteCalculated:
      allOf:
        - $ref: '#/components/schemas/QuoteRequest'
        - type: object
          required: [quoteNumber, date, currency, quoteTitle, subtotal, tax, total]
          properties:
            quoteNumber:
              type: string
              example: "DQ-1720987654321"
            date:
              type: string
              format: date
              example: "2024-07-15"
            currency:
              type: string
              example: "USD"
            quoteTitle:
              type: string
              description: Always present in the response — derived from locale if not supplied in the request.
              example: "Cotización"
            subtotal:
              type: number
              example: 189.96
            tax:
              type: number
              example: 15.20
            total:
              type: number
              example: 205.16
            agent_preview_hints:
              type: object
              description: Machine-readable summary for agent use. Present only in `/preview` responses.
              properties:
                valid:
                  type: boolean
                  example: true
                items_count:
                  type: integer
                  example: 2
                estimated_total:
                  type: number
                  example: 205.16
                currency:
                  type: string
                  example: "USD"
                ready_for_pdf:
                  type: boolean
                  example: true

    InvoiceRequest:
      type: object
      required: [items]
      properties:
        invoiceNumber:
          type: string
          description: Custom invoice number. Auto-generated as `INV-<timestamp>` if omitted.
          example: "INV-2024-001"
        date:
          type: string
          format: date
          description: Invoice date (ISO 8601). Defaults to today.
          example: "2024-07-15"
        dueDate:
          type: string
          format: date
          description: Payment due date (ISO 8601). Optional.
          example: "2024-08-15"
        currency:
          type: string
          description: ISO 4217 currency code. Defaults to `USD`.
          example: "USD"
        company:
          $ref: '#/components/schemas/QuoteRequest/properties/company'
        customer:
          $ref: '#/components/schemas/QuoteRequest/properties/customer'
        items:
          type: array
          minItems: 1
          maxItems: 100
          items:
            $ref: '#/components/schemas/QuoteItem'
        taxRate:
          type: number
          minimum: 0
          maximum: 1
          description: Tax rate as a decimal between 0 and 1 inclusive. Defaults to 0.
          example: 0.19
        taxLabel:
          type: string
          description: Label shown next to tax amount. Defaults to `Tax`.
          example: "IVA"
        locale:
          type: string
          description: BCP 47 locale for number and date formatting.
          example: "es-CL"
        subtotal:
          type: number
          minimum: 0
          description: Override subtotal. Auto-calculated from items if omitted.
        tax:
          type: number
          minimum: 0
          description: Override tax amount. Auto-calculated from `subtotal * taxRate` if omitted.
        total:
          type: number
          minimum: 0
          description: Override total. Auto-calculated from `subtotal + tax` if omitted.
        paymentTerms:
          type: string
          maxLength: 200
          description: Payment terms (e.g., "Net 30").
          example: "Net 30"
        paymentInstructions:
          type: string
          maxLength: 2000
          description: Bank details or payment instructions printed below totals.
          example: "Bank: Example Bank\nAccount: 1234567890\nSWIFT: EXMPUS33"
        notes:
          type: string
          maxLength: 2000
          description: Free-text notes printed at the bottom of the invoice.
          example: "Thank you for your business."
        termsAndConditions:
          type: string
          maxLength: 10000
          description: Full terms and conditions. If present, rendered as a final page in the PDF.

    InvoiceCalculated:
      allOf:
        - $ref: '#/components/schemas/InvoiceRequest'
        - type: object
          required: [invoiceNumber, date, currency, subtotal, tax, total]
          properties:
            invoiceNumber:
              type: string
              example: "INV-1720987654321"
            date:
              type: string
              format: date
              example: "2024-07-15"
            currency:
              type: string
              example: "USD"
            subtotal:
              type: number
              example: 200.00
            tax:
              type: number
              example: 38.00
            total:
              type: number
              example: 238.00

    PORequest:
      type: object
      required: [items]
      properties:
        poNumber:
          type: string
          description: Custom PO number. Auto-generated as `PO-<timestamp>` if omitted.
          example: "PO-2026-001"
        issueDate:
          type: string
          format: date
          description: Issue date (ISO 8601). Defaults to today.
          example: "2026-03-07"
        requestedDeliveryDate:
          type: string
          format: date
          description: Requested delivery date (ISO 8601). Optional.
          example: "2026-04-15"
        currency:
          type: string
          description: ISO 4217 currency code. Defaults to `USD`.
          example: "USD"
        company:
          $ref: '#/components/schemas/QuoteRequest/properties/company'
          description: The buyer — the company issuing the purchase order.
        vendor:
          type: object
          description: The supplier/vendor receiving the purchase order.
          properties:
            name:
              type: string
              example: "Jane Smith"
            company:
              type: string
              example: "Supplier Co."
            taxId:
              type: string
              example: "98-7654321"
            taxIdLabel:
              type: string
              description: Label shown next to vendor tax ID (e.g., `RUT`, `EIN`). Defaults to `Tax ID`.
              example: "EIN"
            address:
              type: string
              example: "456 Warehouse Rd, Chicago, IL 60601"
            phone:
              type: string
              example: "+1 (555) 999-8765"
            email:
              type: string
              format: email
              example: "orders@supplier.com"
        shipTo:
          type: object
          description: Delivery address. Optional — shown after totals if provided.
          properties:
            name:
              type: string
              example: "Receiving Dept."
            company:
              type: string
              example: "Buyer HQ"
            address:
              type: string
              example: "123 Main St, New York, NY 10001"
            phone:
              type: string
              example: "+1 (212) 555-0100"
        items:
          type: array
          minItems: 1
          maxItems: 100
          items:
            $ref: '#/components/schemas/QuoteItem'
        taxRate:
          type: number
          minimum: 0
          maximum: 1
          description: Tax rate as a decimal between 0 and 1. Defaults to 0.
          example: 0.08
        taxLabel:
          type: string
          description: Label shown next to tax amount. Defaults to `Tax`.
          example: "Sales Tax"
        locale:
          type: string
          description: BCP 47 locale for number and date formatting.
          example: "en-US"
        subtotal:
          type: number
          minimum: 0
          description: Override subtotal. Auto-calculated from items if omitted.
        tax:
          type: number
          minimum: 0
          description: Override tax amount. Auto-calculated from `subtotal * taxRate` if omitted.
        total:
          type: number
          minimum: 0
          description: Override total. Auto-calculated from `subtotal + tax` if omitted.
        paymentTerms:
          type: string
          maxLength: 200
          description: Payment terms (e.g., "Net 30").
          example: "Net 30"
        deliveryTerms:
          type: string
          maxLength: 200
          description: Delivery/shipping terms (e.g., "FOB Origin", "CIF Destination").
          example: "FOB Origin"
        reference:
          type: string
          maxLength: 200
          description: Buyer's internal reference or project code shown in the metadata block.
          example: "PROJ-2026-042"
        requester:
          type: string
          maxLength: 200
          description: Name of the person who requested the purchase.
          example: "Maria González"
        approvedBy:
          type: string
          maxLength: 200
          description: Name of the approving authority.
          example: "Carlos Reyes"
        notes:
          type: string
          maxLength: 2000
          description: Free-text notes printed below totals.
          example: "Please confirm receipt of this PO within 2 business days."
        termsAndConditions:
          type: string
          maxLength: 10000
          description: Full terms and conditions. If present, rendered as a final page in the PDF.

    POCalculated:
      allOf:
        - $ref: '#/components/schemas/PORequest'
        - type: object
          required: [poNumber, issueDate, currency, subtotal, tax, total]
          properties:
            poNumber:
              type: string
              example: "PO-1772854197927"
            issueDate:
              type: string
              format: date
              example: "2026-03-07"
            currency:
              type: string
              example: "USD"
            subtotal:
              type: number
              example: 5000.00
            tax:
              type: number
              example: 400.00
            total:
              type: number
              example: 5400.00

    ProformaRequest:
      type: object
      required: [items]
      description: |
        Proforma invoice — a preliminary seller document used for customs, budget approval,
        or letters of credit. Not a billing document; use `invoice` for actual billing.
        `termsAndConditions` is accepted but silently ignored on render. `paymentTerms` is rendered normally.
      properties:
        proformaNumber:
          type: string
          description: Custom proforma number. Auto-generated as `PRO-<timestamp>` if omitted.
          example: "PRO-2024-001"
        date:
          type: string
          format: date
          description: Issue date (ISO 8601). Defaults to today.
          example: "2024-07-15"
        validUntil:
          type: string
          format: date
          description: Validity date (ISO 8601). Optional.
          example: "2024-08-15"
        currency:
          type: string
          description: ISO 4217 currency code. Defaults to `USD`.
          example: "USD"
        company:
          $ref: '#/components/schemas/QuoteRequest/properties/company'
        customer:
          $ref: '#/components/schemas/QuoteRequest/properties/customer'
        items:
          type: array
          minItems: 1
          maxItems: 100
          items:
            $ref: '#/components/schemas/QuoteItem'
        taxRate:
          type: number
          minimum: 0
          maximum: 1
          description: Tax rate as a decimal between 0 and 1. Defaults to 0.
          example: 0.08
        taxLabel:
          type: string
          description: Label shown next to tax amount. Defaults to `Tax`.
          example: "VAT"
        locale:
          type: string
          description: BCP 47 locale for number and date formatting.
          example: "en-US"
        subtotal:
          type: number
          minimum: 0
          description: Override subtotal. Auto-calculated from items if omitted.
        tax:
          type: number
          minimum: 0
          description: Override tax amount. Auto-calculated from `subtotal * taxRate` if omitted.
        total:
          type: number
          minimum: 0
          description: Override total. Auto-calculated from `subtotal + tax` if omitted.
        paymentTerms:
          type: string
          maxLength: 200
          description: Payment terms (e.g., "Net 30"). Rendered on the proforma invoice.
          example: "Net 30"
        paymentInstructions:
          type: string
          maxLength: 2000
          description: Bank details or payment instructions printed below totals on the proforma invoice.
          example: "Bank: Example Bank\nAccount: 1234567890\nSWIFT: EXMPUS33"
        notes:
          type: string
          maxLength: 2000
          description: Free-text notes printed at the bottom of the proforma.
          example: "Subject to final confirmation."
        seal:
          type: string
          description: |
            Company stamp or signature image rendered at bottom-right. Accepts:
            - `data:image/png;base64,...` — inline base64 data URI
            - `https://...` — remote URL fetched server-side (not allowed in demo endpoint)
          example: "data:image/png;base64,iVBORw0KGgo..."

    ProformaCalculated:
      allOf:
        - $ref: '#/components/schemas/ProformaRequest'
        - type: object
          required: [proformaNumber, date, currency, subtotal, tax, total]
          properties:
            proformaNumber:
              type: string
              example: "PRO-1720987654321"
            date:
              type: string
              format: date
              example: "2024-07-15"
            currency:
              type: string
              example: "USD"
            subtotal:
              type: number
              example: 500.00
            tax:
              type: number
              example: 40.00
            total:
              type: number
              example: 540.00
            agent_preview_hints:
              type: object
              description: Machine-readable summary for agent use. Present only in `/preview` responses.
              properties:
                valid:
                  type: boolean
                  example: true
                items_count:
                  type: integer
                  example: 2
                estimated_total:
                  type: number
                  example: 540.00
                currency:
                  type: string
                  example: "USD"
                ready_for_pdf:
                  type: boolean
                  example: true

    CreditNoteRequest:
      type: object
      required: [items]
      description: |
        Credit note — corrects or cancels a prior invoice. All amounts stay positive;
        the document itself indicates a reduction to the buyer's balance.
        `termsAndConditions` and `paymentTerms` are accepted but silently ignored on render.
      properties:
        creditNoteNumber:
          type: string
          description: Custom credit note number. Auto-generated as `CN-<timestamp>` if omitted.
          example: "CN-2024-001"
        date:
          type: string
          format: date
          description: Issue date (ISO 8601). Defaults to today.
          example: "2024-07-15"
        relatedInvoiceNumber:
          type: string
          description: Invoice number this credit note corrects. Optional but recommended.
          example: "INV-2024-001"
        currency:
          type: string
          description: ISO 4217 currency code. Defaults to `USD`.
          example: "USD"
        company:
          $ref: '#/components/schemas/QuoteRequest/properties/company'
        customer:
          $ref: '#/components/schemas/QuoteRequest/properties/customer'
        items:
          type: array
          minItems: 1
          maxItems: 100
          items:
            $ref: '#/components/schemas/QuoteItem'
        taxRate:
          type: number
          minimum: 0
          maximum: 1
          description: Tax rate as a decimal between 0 and 1. Defaults to 0.
          example: 0.19
        taxLabel:
          type: string
          description: Label shown next to tax amount. Defaults to `Tax`.
          example: "IVA"
        locale:
          type: string
          description: BCP 47 locale for number and date formatting.
          example: "es-CL"
        subtotal:
          type: number
          minimum: 0
          description: Override subtotal. Auto-calculated from items if omitted.
        tax:
          type: number
          minimum: 0
          description: Override tax amount. Auto-calculated from `subtotal * taxRate` if omitted.
        total:
          type: number
          minimum: 0
          description: Override total. Auto-calculated from `subtotal + tax` if omitted.
        reason:
          type: string
          maxLength: 500
          description: Reason for the credit note. Printed below totals.
          example: "Returned goods — order partially cancelled."
        notes:
          type: string
          maxLength: 2000
          description: Free-text notes printed at the bottom of the credit note.
          example: "Credit will be applied to next invoice."
        seal:
          type: string
          description: |
            Company stamp or signature image rendered at bottom-right. Accepts:
            - `data:image/png;base64,...` — inline base64 data URI
            - `https://...` — remote URL fetched server-side (not allowed in demo endpoint)

    CreditNoteCalculated:
      allOf:
        - $ref: '#/components/schemas/CreditNoteRequest'
        - type: object
          required: [creditNoteNumber, date, currency, subtotal, tax, total]
          properties:
            creditNoteNumber:
              type: string
              example: "CN-1720987654321"
            date:
              type: string
              format: date
              example: "2024-07-15"
            currency:
              type: string
              example: "USD"
            subtotal:
              type: number
              example: 200.00
            tax:
              type: number
              example: 38.00
            total:
              type: number
              example: 238.00
            agent_preview_hints:
              type: object
              description: Machine-readable summary for agent use. Present only in `/preview` responses.
              properties:
                valid:
                  type: boolean
                  example: true
                items_count:
                  type: integer
                  example: 1
                estimated_total:
                  type: number
                  example: 238.00
                currency:
                  type: string
                  example: "USD"
                ready_for_pdf:
                  type: boolean
                  example: true

    ReceiptRequest:
      type: object
      required: [items]
      description: |
        Receipt — confirms payment received. Typically issued after an invoice is paid.
        Also valid as a standalone document (e.g., cash sales).
        `termsAndConditions` and `paymentTerms` are accepted but silently ignored on render.
      properties:
        receiptNumber:
          type: string
          description: Custom receipt number. Auto-generated as `REC-<timestamp>` if omitted.
          example: "REC-2024-001"
        date:
          type: string
          format: date
          description: Issue date (ISO 8601). Defaults to today.
          example: "2024-07-15"
        relatedInvoiceNumber:
          type: string
          description: Invoice number this receipt corresponds to. Optional.
          example: "INV-2024-001"
        currency:
          type: string
          description: ISO 4217 currency code. Defaults to `USD`.
          example: "USD"
        company:
          $ref: '#/components/schemas/QuoteRequest/properties/company'
        customer:
          $ref: '#/components/schemas/QuoteRequest/properties/customer'
        items:
          type: array
          minItems: 1
          maxItems: 100
          items:
            $ref: '#/components/schemas/QuoteItem'
        taxRate:
          type: number
          minimum: 0
          maximum: 1
          description: Tax rate as a decimal between 0 and 1. Defaults to 0.
          example: 0.08
        taxLabel:
          type: string
          description: Label shown next to tax amount. Defaults to `Tax`.
          example: "VAT"
        locale:
          type: string
          description: BCP 47 locale for number and date formatting.
          example: "en-US"
        subtotal:
          type: number
          minimum: 0
          description: Override subtotal. Auto-calculated from items if omitted.
        tax:
          type: number
          minimum: 0
          description: Override tax amount. Auto-calculated from `subtotal * taxRate` if omitted.
        total:
          type: number
          minimum: 0
          description: Override total. Auto-calculated from `subtotal + tax` if omitted.
        paymentMethod:
          type: string
          maxLength: 100
          description: Payment method (e.g., "Bank Transfer", "Credit Card"). Printed below totals.
          example: "Bank Transfer"
        paymentReference:
          type: string
          maxLength: 200
          description: Payment reference or transaction ID. Printed below totals.
          example: "TXN-20240715-98765"
        notes:
          type: string
          maxLength: 2000
          description: Free-text notes printed at the bottom of the receipt.
          example: "Thank you for your payment."
        seal:
          type: string
          description: |
            Company stamp or signature image rendered at bottom-right. Accepts:
            - `data:image/png;base64,...` — inline base64 data URI
            - `https://...` — remote URL fetched server-side (not allowed in demo endpoint)

    ReceiptCalculated:
      allOf:
        - $ref: '#/components/schemas/ReceiptRequest'
        - type: object
          required: [receiptNumber, date, currency, subtotal, tax, total]
          properties:
            receiptNumber:
              type: string
              example: "REC-1720987654321"
            date:
              type: string
              format: date
              example: "2024-07-15"
            currency:
              type: string
              example: "USD"
            subtotal:
              type: number
              example: 1200.00
            tax:
              type: number
              example: 0
            total:
              type: number
              example: 1200.00
            agent_preview_hints:
              type: object
              description: Machine-readable summary for agent use. Present only in `/preview` responses.
              properties:
                valid:
                  type: boolean
                  example: true
                items_count:
                  type: integer
                  example: 1
                estimated_total:
                  type: number
                  example: 1200.00
                currency:
                  type: string
                  example: "USD"
                ready_for_pdf:
                  type: boolean
                  example: true

    ErrorResponse:
      type: object
      required: [error, message, request_id]
      properties:
        error:
          type: string
          description: Machine-readable error code.
          example: "validation_error"
        message:
          type: string
          description: Human-readable description.
          example: "items array is required and must not be empty"
        request_id:
          type: string
          description: Request UUID for tracing.
          example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"

    IdempotencyConflictResponse:
      type: object
      required: [error, message, request_id]
      properties:
        error:
          type: string
          enum: ["idempotency_conflict"]
        message:
          type: string
          example: "Idempotency-Key reused with a different request payload"
        request_id:
          type: string

    KeyRegistrationResponse:
      type: object
      required: [apiKey, free_remaining]
      properties:
        apiKey:
          type: string
          description: Full API key — store securely, shown only once.
          example: "dq_live_a1b2c3d4e5f6..."
        free_remaining:
          type: integer
          enum: [5]
          description: Free generations available on the new key.

    CreditPack:
      type: object
      required: [id, label, credits, amountUsd]
      properties:
        id:
          type: string
          enum: [basic, pro, agency]
        label:
          type: string
          example: "Basic"
        credits:
          type: integer
          example: 200
        amountUsd:
          type: number
          description: Price in US dollars (USD).
          example: 6.99

    CheckoutResponse:
      type: object
      required: [purchaseId, redirectUrl, pack]
      properties:
        purchaseId:
          type: string
          format: uuid
          description: Internal purchase ID — use with GET /v1/credits/verify.
        redirectUrl:
          type: string
          format: uri
          description: PayPal checkout URL. Redirect the user here to complete payment.
        pack:
          $ref: '#/components/schemas/CreditPack'

    BalanceResponse:
      type: object
      required: [credits_remaining, free_remaining]
      properties:
        credits_remaining:
          type: integer
          minimum: 0
          description: Paid credits available on this API key.
        free_remaining:
          type: integer
          minimum: 0
          maximum: 5
          description: Free generations remaining on this API key.

    PaymentRequiredCreditsResponse:
      type: object
      required: [error, message, request_id, free_remaining, credits_remaining, checkout_endpoint, recommended_pack]
      properties:
        error:
          type: string
          enum: [payment_required]
        message:
          type: string
          example: "Free tier exhausted and no credits remaining."
        request_id:
          type: string
        free_remaining:
          type: integer
          enum: [0]
        credits_remaining:
          type: integer
          enum: [0]
        checkout_endpoint:
          type: string
          example: "POST /v1/credits/checkout"
        recommended_pack:
          type: string
          enum: [basic, pro, agency]
          example: "basic"

  responses:
    Unauthorized:
      description: Missing or invalid API key.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/X-Request-Id'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error: "unauthorized"
            message: "Missing or invalid API key"
            request_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"

    ValidationError:
      description: Request validation failed.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/X-Request-Id'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error: "validation_error"
            message: "items array is required and must not be empty"
            request_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"

    RateLimited:
      description: Rate limit exceeded.
      headers:
        Retry-After:
          $ref: '#/components/headers/Retry-After'
        RateLimit:
          $ref: '#/components/headers/RateLimit'
        X-Request-Id:
          $ref: '#/components/headers/X-Request-Id'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error: "rate_limited"
            message: "Too many requests, please slow down."
            request_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"

  examples:
    quoteMinimal:
      summary: Minimal sales quote payload
      value:
        company:
          name: "DocQuote Ltd."
        customer:
          name: "Acme Corp"
        items:
          - name: "Installation service"
            qty: 1
            unitPrice: 120
    quoteFull:
      summary: Full sales quote payload
      value:
        company:
          name: "DocQuote Ltd."
          email: "sales@docquote.dev"
        customer:
          name: "Acme Corp"
          email: "procurement@acme.com"
        items:
          - name: "Installation service — on-site installation and configuration"
            qty: 1
            unitPrice: 120
          - name: "Maintenance — annual support contract"
            qty: 1
            unitPrice: 80
        taxRate: 0.08
        currency: "USD"
        notes: "Thank you for your business."
    quoteSpanish:
      summary: Spanish sales quote payload
      value:
        company:
          name: "DocQuote Ltd."
        customer:
          name: "Empresa Ejemplo"
        items:
          - name: "Servicio de instalación"
            qty: 1
            unitPrice: 120
        notes: "Gracias por su preferencia."
    invoiceMinimal:
      summary: Minimal invoice payload
      value:
        company:
          name: "DocInvoice Ltd."
        customer:
          name: "Acme Corp"
        items:
          - name: "Consulting services"
            qty: 8
            unitPrice: 150
    invoiceFull:
      summary: Full invoice payload with due date and payment instructions
      value:
        company:
          name: "DocInvoice Ltd."
          email: "billing@docinvoice.dev"
        customer:
          name: "Acme Corp"
          email: "ap@acme.com"
        items:
          - name: "Software development — 40 hours"
            qty: 40
            unitPrice: 120
          - name: "Project management"
            qty: 10
            unitPrice: 80
        dueDate: "2024-08-15"
        taxRate: 0.19
        currency: "USD"
        paymentTerms: "Net 30"
        paymentInstructions: "Bank: Example Bank\nAccount: 1234567890\nSWIFT: EXMPUS33"
        notes: "Thank you for your business."
    poMinimal:
      summary: Minimal purchase order payload
      value:
        company:
          name: "Buyer Corp."
        vendor:
          company: "Supplier Co."
          email: "orders@supplier.com"
        items:
          - name: "Office Supplies"
            qty: 10
            unitPrice: 25.00
    poFull:
      summary: Full purchase order with all fields
      value:
        poNumber: "PO-2026-001"
        issueDate: "2026-03-07"
        requestedDeliveryDate: "2026-04-15"
        currency: "USD"
        locale: "en-US"
        company:
          name: "Buyer Corp."
          businessLine: "Manufacturing"
          taxId: "12-3456789"
          taxIdLabel: "EIN"
          address: "350 Fifth Avenue, New York, NY 10118"
          phone: "+1 (212) 555-0100"
          email: "purchasing@buyer.com"
          primaryColor: "#0F172A"
        vendor:
          name: "Jane Smith"
          company: "Supplier Co."
          taxId: "98-7654321"
          taxIdLabel: "EIN"
          address: "456 Warehouse Rd, Chicago, IL 60601"
          phone: "+1 (555) 999-8765"
          email: "orders@supplier.com"
        shipTo:
          company: "Buyer Corp. — Warehouse"
          address: "789 Dock St, Newark, NJ 07102"
          phone: "+1 (212) 555-0199"
        items:
          - name: "Industrial Widget A — heavy-duty, model HW-100"
            qty: 100
            unitPrice: 45.00
          - name: "Fastener Kit — assorted M5/M6"
            qty: 500
            unitPrice: 1.20
          - name: "Shipping & Handling"
            qty: 1
            unitPrice: 150.00
        taxRate: 0.08
        taxLabel: "Sales Tax"
        paymentTerms: "Net 30"
        deliveryTerms: "FOB Origin"
        reference: "PROJ-2026-042"
        requester: "Maria González"
        approvedBy: "Carlos Reyes"
        notes: "Please confirm receipt of this PO within 2 business days. Partial deliveries accepted."
        termsAndConditions: "1. ACCEPTANCE\nVendor acceptance of this PO constitutes agreement to all terms.\n\n2. DELIVERY\nTime is of the essence. Late deliveries may incur penalties.\n\n3. INSPECTION\nBuyer reserves the right to inspect goods upon delivery.\n\n4. PAYMENT\nInvoice must reference this PO number."

# ─── Paths ───────────────────────────────────────────────────────────────────

paths:

  # ── Health ──────────────────────────────────────────────────────────────────

  /health:
    get:
      tags: [health]
      operationId: getHealth
      summary: Liveness probe
      description: Returns `200 ok` immediately. No DB check. Safe to poll frequently.
      responses:
        "200":
          description: Service is up.
          content:
            application/json:
              schema:
                type: object
                required: [status, version]
                properties:
                  status:
                    type: string
                    enum: ["ok"]
                  version:
                    type: string
                    example: "1.0.0"
              example:
                status: "ok"
                version: "1.0.0"

  /status:
    get:
      tags: [health]
      operationId: getStatus
      summary: Readiness probe
      description: Checks DB connectivity. Use this before running smoke tests.
      responses:
        "200":
          description: Service and database are healthy.
          content:
            application/json:
              schema:
                type: object
                required: [status, version, db, uptime_seconds]
                properties:
                  status:
                    type: string
                    enum: ["ok"]
                  version:
                    type: string
                  db:
                    type: string
                    enum: ["ok"]
                  uptime_seconds:
                    type: integer
              example:
                status: "ok"
                version: "1.0.0"
                db: "ok"
                uptime_seconds: 3600
        "503":
          description: Database unreachable.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: ["degraded"]
                  db:
                    type: string
                    enum: ["unreachable"]
                  uptime_seconds:
                    type: integer

  # ── Keys ────────────────────────────────────────────────────────────────────

  /v1/keys/register:
    post:
      tags: [keys]
      operationId: registerKey
      summary: Register a new API key
      description: |
        Creates a new API key with 5 free generations. No authentication required.

        Providing an `email` is optional but recommended — it associates the key with your
        account for billing and recovery. Max 3 keys per email address.

        **The full API key is returned only once. Store it securely.**
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                email:
                  type: string
                  format: email
                  description: Optional. Associates this key with your account.
                  example: "you@example.com"
            example:
              email: "you@example.com"
      responses:
        "201":
          description: API key created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KeyRegistrationResponse'
              example:
                apiKey: "dq_live_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6"
                free_remaining: 5
        "400":
          $ref: '#/components/responses/ValidationError'
        "429":
          $ref: '#/components/responses/RateLimited'

  # ── Quote ───────────────────────────────────────────────────────────────────

  /v1/quote/preview:
    post:
      tags: [quote]
      operationId: previewQuote
      summary: Validate and calculate a quote (no billing, no PDF)
      description: |
        Validates the request, resolves defaults, and returns the fully-calculated quote object.
        No billing, no database writes, no PDF generated. Free to call.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/QuoteRequest'
            examples:
              minimal:
                $ref: '#/components/examples/quoteMinimal'
              full:
                $ref: '#/components/examples/quoteFull'
              spanish:
                $ref: '#/components/examples/quoteSpanish'
      responses:
        "200":
          description: Calculated quote object.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/QuoteCalculated'
              example:
                quoteNumber: "DQ-1720987654321"
                date: "2024-07-15"
                currency: "USD"
                items:
                  - name: "Widget A"
                    qty: 2
                    unitPrice: 49.99
                    discount: 0
                    total: 99.98
                taxRate: 0.08
                subtotal: 99.98
                tax: 8.00
                total: 107.98
        "400":
          $ref: '#/components/responses/ValidationError'

  /v1/quote/pdf:
    post:
      tags: [quote]
      operationId: generateQuotePdf
      summary: Generate a quote PDF
      description: |
        Generates a professional PDF quote. Requires authentication.

        DocQuote is quote generation infrastructure for autonomous AI agents.

        ## Billing
        - The first 5 calls per API key are free (`X-DocQuote-Free-Remaining` counts down from 5).
        - After the free tier, one credit is consumed per PDF (`X-DocQuote-Credits-Remaining` decrements).
        - If the free tier is exhausted and credits are zero, returns `402` with `checkout_endpoint: "POST /v1/credits/checkout"` and `recommended_pack`.
        - Purchase credits via `POST /v1/credits/checkout`. Credits never expire.

        ## Idempotency
        Include an `Idempotency-Key` header to make the request idempotent:
        - Same key + same payload → returns the cached PDF, usage count unchanged.
        - Same key + different payload → `409 idempotency_conflict`.
        - Idempotency-Key must not exceed 256 characters.

        ## Cache hit vs fresh PDF
        | Indicator | Fresh PDF | Cache hit |
        |---|---|---|
        | `Content-Disposition` | `attachment; filename="quote-DQ-xxx.pdf"` | `attachment; filename="quote-cached.pdf"` |
        | `X-Generation-Ms` | Present | **Absent** |
        | Billing | Charged | Not charged |
      security:
        - ApiKeyAuth: []
      parameters:
        - name: Idempotency-Key
          in: header
          required: false
          schema:
            type: string
            maxLength: 256
          description: Client-generated idempotency key. Safe to retry on network failures.
          example: "my-quote-job-abc123"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/QuoteRequest'
            examples:
              minimal:
                $ref: '#/components/examples/quoteMinimal'
              full:
                $ref: '#/components/examples/quoteFull'
              spanish:
                $ref: '#/components/examples/quoteSpanish'
      responses:
        "200":
          description: |
            PDF generated (or returned from cache).

            **Fresh PDF:** `Content-Disposition` contains the quote number filename and
            `X-Generation-Ms` is present.

            **Cache hit:** `Content-Disposition` filename is `quote-cached.pdf` and
            `X-Generation-Ms` is absent.
          headers:
            Content-Disposition:
              schema:
                type: string
              description: |
                - Fresh: `attachment; filename="quote-DQ-<timestamp>.pdf"`
                - Cache hit: `attachment; filename="quote-cached.pdf"`
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
            X-DocQuote-Version:
              $ref: '#/components/headers/X-DocQuote-Version'
            X-DocQuote-Free-Remaining:
              $ref: '#/components/headers/X-DocQuote-Free-Remaining'
            X-DocQuote-Usage:
              $ref: '#/components/headers/X-DocQuote-Usage'
            X-DocQuote-Credits-Remaining:
              $ref: '#/components/headers/X-DocQuote-Credits-Remaining'
            X-Generation-Ms:
              $ref: '#/components/headers/X-Generation-Ms'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
          content:
            application/pdf:
              schema:
                type: string
                format: binary
        "400":
          description: Validation error (invalid request body or oversized Idempotency-Key).
          headers:
            X-DocQuote-Free-Remaining:
              $ref: '#/components/headers/X-DocQuote-Free-Remaining'
            X-DocQuote-Usage:
              $ref: '#/components/headers/X-DocQuote-Usage'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                emptyItems:
                  summary: Empty items array
                  value:
                    error: "validation_error"
                    message: "items array is required and must not be empty"
                    request_id: "a1b2c3d4-..."
                idempotencyKeyTooLong:
                  summary: Idempotency-Key exceeds 256 chars
                  value:
                    error: "validation_error"
                    message: "Idempotency-Key must not exceed 256 characters"
                    request_id: "a1b2c3d4-..."
        "401":
          $ref: '#/components/responses/Unauthorized'
        "402":
          description: Free tier exhausted and no credits remaining.
          headers:
            X-DocQuote-Free-Remaining:
              $ref: '#/components/headers/X-DocQuote-Free-Remaining'
            X-DocQuote-Usage:
              $ref: '#/components/headers/X-DocQuote-Usage'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaymentRequiredCreditsResponse'
              example:
                error: "payment_required"
                message: "Free tier exhausted and no credits remaining."
                request_id: "a1b2c3d4-..."
                free_remaining: 0
                credits_remaining: 0
                checkout_endpoint: "POST /v1/credits/checkout"
                recommended_pack: "basic"
        "409":
          description: Idempotency key reused with a different payload.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IdempotencyConflictResponse'
              example:
                error: "idempotency_conflict"
                message: "Idempotency-Key reused with a different request payload"
                request_id: "a1b2c3d4-..."
        "429":
          $ref: '#/components/responses/RateLimited'
        "500":
          description: |
            Internal error. Includes an optional `code` field for machine handling:
            - `pdf_timeout` — PDF renderer timed out; retry is safe.
            - `pdf_too_large` — Generated PDF exceeded size limit; unlikely to succeed on retry.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ErrorResponse'
                  - type: object
                    properties:
                      code:
                        type: string
                        enum: [pdf_timeout, pdf_too_large]
              example:
                error: "internal_error"
                message: "PDF generation timed out"
                request_id: "a1b2c3d4-..."
                code: "pdf_timeout"

  # ── Invoice ─────────────────────────────────────────────────────────────────

  /v1/invoice/preview:
    post:
      tags: [invoice]
      operationId: previewInvoice
      summary: Validate and calculate an invoice (no billing, no PDF)
      description: |
        Validates the request, resolves defaults, and returns the fully-calculated invoice object.
        No billing, no database writes, no PDF generated. Free to call. No authentication required.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InvoiceRequest'
            examples:
              minimal:
                $ref: '#/components/examples/invoiceMinimal'
              full:
                $ref: '#/components/examples/invoiceFull'
      responses:
        "200":
          description: Calculated invoice object.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InvoiceCalculated'
              example:
                invoiceNumber: "INV-1720987654321"
                date: "2024-07-15"
                currency: "USD"
                items:
                  - name: "Consulting services"
                    qty: 8
                    unitPrice: 150
                    discount: 0
                    total: 1200
                subtotal: 1200
                tax: 0
                total: 1200
        "400":
          $ref: '#/components/responses/ValidationError'

  /v1/invoice/pdf:
    post:
      tags: [invoice]
      operationId: generateInvoicePdf
      summary: Generate an invoice PDF
      description: |
        Generates a professional invoice PDF. Requires authentication.

        Uses the same billing system as `POST /v1/quote/pdf` — the same API key, free tier,
        and credits apply across both quote and invoice generation.

        ## Billing
        - The first 5 calls per API key are free (`X-DocQuote-Free-Remaining` counts down from 5).
        - After the free tier, one credit is consumed per PDF (`X-DocQuote-Credits-Remaining` decrements).
        - If the free tier is exhausted and credits are zero, returns `402` with `checkout_endpoint: "POST /v1/credits/checkout"` and `recommended_pack`.

        ## Idempotency
        Include an `Idempotency-Key` header to make the request idempotent:
        - Same key + same payload → returns the cached PDF, usage count unchanged.
        - Same key + different payload → `409 idempotency_conflict`.
        - Idempotency-Key must not exceed 256 characters.
      security:
        - ApiKeyAuth: []
      parameters:
        - name: Idempotency-Key
          in: header
          required: false
          schema:
            type: string
            maxLength: 256
          description: Client-generated idempotency key. Safe to retry on network failures.
          example: "my-invoice-job-abc123"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InvoiceRequest'
            examples:
              minimal:
                $ref: '#/components/examples/invoiceMinimal'
              full:
                $ref: '#/components/examples/invoiceFull'
      responses:
        "200":
          description: |
            PDF generated (or returned from cache).

            **Fresh PDF:** `Content-Disposition` contains the invoice number filename and
            `X-Generation-Ms` is present.

            **Cache hit:** `Content-Disposition` filename is `invoice-cached.pdf` and
            `X-Generation-Ms` is absent.
          headers:
            Content-Disposition:
              schema:
                type: string
              description: |
                - Fresh: `attachment; filename="invoice-INV-<timestamp>.pdf"`
                - Cache hit: `attachment; filename="invoice-cached.pdf"`
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
            X-DocQuote-Version:
              $ref: '#/components/headers/X-DocQuote-Version'
            X-DocQuote-Free-Remaining:
              $ref: '#/components/headers/X-DocQuote-Free-Remaining'
            X-DocQuote-Usage:
              $ref: '#/components/headers/X-DocQuote-Usage'
            X-DocQuote-Credits-Remaining:
              $ref: '#/components/headers/X-DocQuote-Credits-Remaining'
            X-Generation-Ms:
              $ref: '#/components/headers/X-Generation-Ms'
          content:
            application/pdf:
              schema:
                type: string
                format: binary
        "400":
          description: Validation error.
          headers:
            X-DocQuote-Free-Remaining:
              $ref: '#/components/headers/X-DocQuote-Free-Remaining'
            X-DocQuote-Usage:
              $ref: '#/components/headers/X-DocQuote-Usage'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          $ref: '#/components/responses/Unauthorized'
        "402":
          description: Free tier exhausted and no credits remaining.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaymentRequiredCreditsResponse'
        "409":
          description: Idempotency key reused with a different payload.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IdempotencyConflictResponse'
        "429":
          $ref: '#/components/responses/RateLimited'
        "500":
          description: |
            Internal error. Includes an optional `code` field:
            - `pdf_timeout` — PDF renderer timed out; retry is safe.
            - `pdf_too_large` — Generated PDF exceeded size limit.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ErrorResponse'
                  - type: object
                    properties:
                      code:
                        type: string
                        enum: [pdf_timeout, pdf_too_large]

  # ── Purchase Order ───────────────────────────────────────────────────────────

  /v1/po/preview:
    post:
      tags: [po]
      operationId: previewPO
      summary: Validate and calculate a purchase order (no billing, no PDF)
      description: |
        Validates the request, resolves defaults, and returns the fully-calculated PO object.
        No billing, no database writes, no PDF generated. Free to call. No authentication required.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PORequest'
            examples:
              minimal:
                $ref: '#/components/examples/poMinimal'
              full:
                $ref: '#/components/examples/poFull'
      responses:
        "200":
          description: Calculated purchase order object.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/POCalculated'
              example:
                poNumber: "PO-1772854197927"
                issueDate: "2026-03-07"
                currency: "USD"
                items:
                  - name: "Office Supplies"
                    qty: 10
                    unitPrice: 25.00
                    discount: 0
                    total: 250.00
                subtotal: 250.00
                tax: 0
                total: 250.00
        "400":
          $ref: '#/components/responses/ValidationError'

  /v1/po/pdf:
    post:
      tags: [po]
      operationId: generatePOPdf
      summary: Generate a purchase order PDF
      description: |
        Generates a professional purchase order PDF. Requires authentication.

        Uses the same billing system as `POST /v1/quote/pdf` and `POST /v1/invoice/pdf` —
        the same API key, free tier, and credits apply across quote, invoice, and PO generation.

        ## Billing
        - The first 5 calls per API key are free (`X-DocQuote-Free-Remaining` counts down from 5).
        - After the free tier, one credit is consumed per PDF (`X-DocQuote-Credits-Remaining` decrements).
        - If the free tier is exhausted and credits are zero, returns `402` with `checkout_endpoint: "POST /v1/credits/checkout"` and `recommended_pack`.

        ## Idempotency
        Include an `Idempotency-Key` header to make the request idempotent:
        - Same key + same payload → returns the cached PDF, usage count unchanged.
        - Same key + different payload → `409 idempotency_conflict`.
        - Idempotency-Key must not exceed 256 characters.
      security:
        - ApiKeyAuth: []
      parameters:
        - name: Idempotency-Key
          in: header
          required: false
          schema:
            type: string
            maxLength: 256
          description: Client-generated idempotency key. Safe to retry on network failures.
          example: "my-po-job-abc123"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PORequest'
            examples:
              minimal:
                $ref: '#/components/examples/poMinimal'
              full:
                $ref: '#/components/examples/poFull'
      responses:
        "200":
          description: |
            PDF generated (or returned from cache).

            **Fresh PDF:** `Content-Disposition` contains the PO number filename and
            `X-Generation-Ms` is present.

            **Cache hit:** `Content-Disposition` filename is `po-cached.pdf` and
            `X-Generation-Ms` is absent.
          headers:
            Content-Disposition:
              schema:
                type: string
              description: |
                - Fresh: `attachment; filename="po-PO-<timestamp>.pdf"`
                - Cache hit: `attachment; filename="po-cached.pdf"`
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
            X-DocQuote-Version:
              $ref: '#/components/headers/X-DocQuote-Version'
            X-DocQuote-Free-Remaining:
              $ref: '#/components/headers/X-DocQuote-Free-Remaining'
            X-DocQuote-Usage:
              $ref: '#/components/headers/X-DocQuote-Usage'
            X-DocQuote-Credits-Remaining:
              $ref: '#/components/headers/X-DocQuote-Credits-Remaining'
            X-Generation-Ms:
              $ref: '#/components/headers/X-Generation-Ms'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
          content:
            application/pdf:
              schema:
                type: string
                format: binary
        "400":
          description: Validation error.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          $ref: '#/components/responses/Unauthorized'
        "402":
          description: Free tier exhausted and no credits remaining.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaymentRequiredCreditsResponse'
        "409":
          description: Idempotency key reused with a different payload.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IdempotencyConflictResponse'
        "429":
          $ref: '#/components/responses/RateLimited'
        "500":
          description: |
            Internal error. Includes an optional `code` field:
            - `pdf_timeout` — PDF renderer timed out; retry is safe.
            - `pdf_too_large` — Generated PDF exceeded size limit.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ErrorResponse'
                  - type: object
                    properties:
                      code:
                        type: string
                        enum: [pdf_timeout, pdf_too_large]

  # ── Proforma ─────────────────────────────────────────────────────────────────

  /v1/proforma/preview:
    post:
      tags: [proforma]
      operationId: previewProforma
      summary: Validate and calculate a proforma invoice (no billing, no PDF)
      description: |
        Validates the request, resolves defaults, and returns the fully-calculated proforma object.
        No billing, no database writes, no PDF generated. Free to call. No authentication required.
        Returns `agent_preview_hints` for machine-readable validation feedback.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProformaRequest'
            example:
              company:
                name: "Acme Corp"
              customer:
                name: "Client Ltd"
              items:
                - name: "Software License"
                  qty: 5
                  unitPrice: 200.00
              validUntil: "2024-08-15"
      responses:
        "200":
          description: Calculated proforma object with agent_preview_hints.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProformaCalculated'
              example:
                proformaNumber: "PRO-1720987654321"
                date: "2024-07-15"
                currency: "USD"
                items:
                  - name: "Software License"
                    qty: 5
                    unitPrice: 200.00
                    discount: 0
                    total: 1000.00
                subtotal: 1000.00
                tax: 0
                total: 1000.00
                agent_preview_hints:
                  valid: true
                  items_count: 1
                  estimated_total: 1000.00
                  currency: "USD"
                  ready_for_pdf: true
        "400":
          $ref: '#/components/responses/ValidationError'

  /v1/proforma/pdf:
    post:
      tags: [proforma]
      operationId: generateProformaPdf
      summary: Generate a proforma invoice PDF
      description: |
        Generates a professional proforma invoice PDF. Requires authentication.

        Uses the same billing system as all other `/pdf` endpoints — the same API key,
        free tier, and credits apply across all six document types.

        ## Billing
        - The first 5 calls per API key are free (`X-DocQuote-Free-Remaining` counts down from 5).
        - After the free tier, one credit is consumed per PDF (`X-DocQuote-Credits-Remaining` decrements).
        - If the free tier is exhausted and credits are zero, returns `402` with `checkout_endpoint: "POST /v1/credits/checkout"` and `recommended_pack`.

        ## Idempotency
        Include an `Idempotency-Key` header to make the request idempotent:
        - Same key + same payload → returns the cached PDF, usage count unchanged.
        - Same key + different payload → `409 idempotency_conflict`.
        - Idempotency-Key must not exceed 256 characters.
      security:
        - ApiKeyAuth: []
      parameters:
        - name: Idempotency-Key
          in: header
          required: false
          schema:
            type: string
            maxLength: 256
          description: Client-generated idempotency key. Safe to retry on network failures.
          example: "my-proforma-job-abc123"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProformaRequest'
            example:
              company:
                name: "Acme Corp"
              customer:
                name: "Client Ltd"
              items:
                - name: "Software License"
                  qty: 5
                  unitPrice: 200.00
              validUntil: "2024-08-15"
              taxRate: 0.08
              currency: "USD"
      responses:
        "200":
          description: |
            PDF generated (or returned from cache).

            **Fresh PDF:** `Content-Disposition` contains the proforma number filename and
            `X-Generation-Ms` is present.

            **Cache hit:** `Content-Disposition` filename is `proforma-cached.pdf` and
            `X-Generation-Ms` is absent.
          headers:
            Content-Disposition:
              schema:
                type: string
              description: |
                - Fresh: `attachment; filename="proforma-PRO-<timestamp>.pdf"`
                - Cache hit: `attachment; filename="proforma-cached.pdf"`
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
            X-DocQuote-Version:
              $ref: '#/components/headers/X-DocQuote-Version'
            X-DocQuote-Free-Remaining:
              $ref: '#/components/headers/X-DocQuote-Free-Remaining'
            X-DocQuote-Usage:
              $ref: '#/components/headers/X-DocQuote-Usage'
            X-DocQuote-Credits-Remaining:
              $ref: '#/components/headers/X-DocQuote-Credits-Remaining'
            X-Generation-Ms:
              $ref: '#/components/headers/X-Generation-Ms'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
          content:
            application/pdf:
              schema:
                type: string
                format: binary
        "400":
          description: Validation error.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          $ref: '#/components/responses/Unauthorized'
        "402":
          description: Free tier exhausted and no credits remaining.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaymentRequiredCreditsResponse'
        "409":
          description: Idempotency key reused with a different payload.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IdempotencyConflictResponse'
        "429":
          $ref: '#/components/responses/RateLimited'
        "500":
          description: |
            Internal error. Includes an optional `code` field:
            - `pdf_timeout` — PDF renderer timed out; retry is safe.
            - `pdf_too_large` — Generated PDF exceeded size limit.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ErrorResponse'
                  - type: object
                    properties:
                      code:
                        type: string
                        enum: [pdf_timeout, pdf_too_large]

  # ── Credit Note ───────────────────────────────────────────────────────────────

  /v1/credit-note/preview:
    post:
      tags: [credit_note]
      operationId: previewCreditNote
      summary: Validate and calculate a credit note (no billing, no PDF)
      description: |
        Validates the request, resolves defaults, and returns the fully-calculated credit note object.
        No billing, no database writes, no PDF generated. Free to call. No authentication required.
        Returns `agent_preview_hints` for machine-readable validation feedback.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreditNoteRequest'
            example:
              company:
                name: "Acme Corp"
              customer:
                name: "Client Ltd"
              items:
                - name: "Returned goods"
                  qty: 2
                  unitPrice: 50.00
              relatedInvoiceNumber: "INV-2024-001"
              reason: "Order partially cancelled."
      responses:
        "200":
          description: Calculated credit note object with agent_preview_hints.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreditNoteCalculated'
              example:
                creditNoteNumber: "CN-1720987654321"
                date: "2024-07-15"
                currency: "USD"
                items:
                  - name: "Returned goods"
                    qty: 2
                    unitPrice: 50.00
                    discount: 0
                    total: 100.00
                subtotal: 100.00
                tax: 0
                total: 100.00
                agent_preview_hints:
                  valid: true
                  items_count: 1
                  estimated_total: 100.00
                  currency: "USD"
                  ready_for_pdf: true
        "400":
          $ref: '#/components/responses/ValidationError'

  /v1/credit-note/pdf:
    post:
      tags: [credit_note]
      operationId: generateCreditNotePdf
      summary: Generate a credit note PDF
      description: |
        Generates a professional credit note PDF. Requires authentication.

        Uses the same billing system as all other `/pdf` endpoints — the same API key,
        free tier, and credits apply across all six document types.

        ## Billing
        - The first 5 calls per API key are free (`X-DocQuote-Free-Remaining` counts down from 5).
        - After the free tier, one credit is consumed per PDF (`X-DocQuote-Credits-Remaining` decrements).
        - If the free tier is exhausted and credits are zero, returns `402` with `checkout_endpoint: "POST /v1/credits/checkout"` and `recommended_pack`.

        ## Idempotency
        Include an `Idempotency-Key` header to make the request idempotent:
        - Same key + same payload → returns the cached PDF, usage count unchanged.
        - Same key + different payload → `409 idempotency_conflict`.
      security:
        - ApiKeyAuth: []
      parameters:
        - name: Idempotency-Key
          in: header
          required: false
          schema:
            type: string
            maxLength: 256
          description: Client-generated idempotency key.
          example: "my-credit-note-job-abc123"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreditNoteRequest'
            example:
              company:
                name: "Acme Corp"
              customer:
                name: "Client Ltd"
              items:
                - name: "Returned goods"
                  qty: 2
                  unitPrice: 50.00
              relatedInvoiceNumber: "INV-2024-001"
              reason: "Order partially cancelled."
              taxRate: 0.19
              currency: "USD"
      responses:
        "200":
          description: |
            PDF generated (or returned from cache).
          headers:
            Content-Disposition:
              schema:
                type: string
              description: |
                - Fresh: `attachment; filename="credit-note-CN-<timestamp>.pdf"`
                - Cache hit: `attachment; filename="credit-note-cached.pdf"`
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
            X-DocQuote-Version:
              $ref: '#/components/headers/X-DocQuote-Version'
            X-DocQuote-Free-Remaining:
              $ref: '#/components/headers/X-DocQuote-Free-Remaining'
            X-DocQuote-Usage:
              $ref: '#/components/headers/X-DocQuote-Usage'
            X-DocQuote-Credits-Remaining:
              $ref: '#/components/headers/X-DocQuote-Credits-Remaining'
            X-Generation-Ms:
              $ref: '#/components/headers/X-Generation-Ms'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
          content:
            application/pdf:
              schema:
                type: string
                format: binary
        "400":
          description: Validation error.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          $ref: '#/components/responses/Unauthorized'
        "402":
          description: Free tier exhausted and no credits remaining.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaymentRequiredCreditsResponse'
        "409":
          description: Idempotency key reused with a different payload.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IdempotencyConflictResponse'
        "429":
          $ref: '#/components/responses/RateLimited'
        "500":
          description: Internal error (pdf_timeout or pdf_too_large).
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ErrorResponse'
                  - type: object
                    properties:
                      code:
                        type: string
                        enum: [pdf_timeout, pdf_too_large]

  # ── Receipt ───────────────────────────────────────────────────────────────────

  /v1/receipt/preview:
    post:
      tags: [receipt]
      operationId: previewReceipt
      summary: Validate and calculate a receipt (no billing, no PDF)
      description: |
        Validates the request, resolves defaults, and returns the fully-calculated receipt object.
        No billing, no database writes, no PDF generated. Free to call. No authentication required.
        Returns `agent_preview_hints` for machine-readable validation feedback.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReceiptRequest'
            example:
              company:
                name: "Acme Corp"
              customer:
                name: "Client Ltd"
              items:
                - name: "Consulting services"
                  qty: 10
                  unitPrice: 150.00
              relatedInvoiceNumber: "INV-2024-001"
              paymentMethod: "Bank Transfer"
              paymentReference: "TXN-20240715-98765"
      responses:
        "200":
          description: Calculated receipt object with agent_preview_hints.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReceiptCalculated'
              example:
                receiptNumber: "REC-1720987654321"
                date: "2024-07-15"
                currency: "USD"
                items:
                  - name: "Consulting services"
                    qty: 10
                    unitPrice: 150.00
                    discount: 0
                    total: 1500.00
                subtotal: 1500.00
                tax: 0
                total: 1500.00
                agent_preview_hints:
                  valid: true
                  items_count: 1
                  estimated_total: 1500.00
                  currency: "USD"
                  ready_for_pdf: true
        "400":
          $ref: '#/components/responses/ValidationError'

  /v1/receipt/pdf:
    post:
      tags: [receipt]
      operationId: generateReceiptPdf
      summary: Generate a receipt PDF
      description: |
        Generates a professional receipt PDF confirming payment received. Requires authentication.

        Uses the same billing system as all other `/pdf` endpoints — the same API key,
        free tier, and credits apply across all six document types.

        ## Billing
        - The first 5 calls per API key are free (`X-DocQuote-Free-Remaining` counts down from 5).
        - After the free tier, one credit is consumed per PDF (`X-DocQuote-Credits-Remaining` decrements).
        - If the free tier is exhausted and credits are zero, returns `402` with `checkout_endpoint: "POST /v1/credits/checkout"` and `recommended_pack`.

        ## Idempotency
        Include an `Idempotency-Key` header to make the request idempotent:
        - Same key + same payload → returns the cached PDF, usage count unchanged.
        - Same key + different payload → `409 idempotency_conflict`.
      security:
        - ApiKeyAuth: []
      parameters:
        - name: Idempotency-Key
          in: header
          required: false
          schema:
            type: string
            maxLength: 256
          description: Client-generated idempotency key.
          example: "my-receipt-job-abc123"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReceiptRequest'
            example:
              company:
                name: "Acme Corp"
              customer:
                name: "Client Ltd"
              items:
                - name: "Consulting services"
                  qty: 10
                  unitPrice: 150.00
              relatedInvoiceNumber: "INV-2024-001"
              paymentMethod: "Bank Transfer"
              paymentReference: "TXN-20240715-98765"
              currency: "USD"
      responses:
        "200":
          description: |
            PDF generated (or returned from cache).
          headers:
            Content-Disposition:
              schema:
                type: string
              description: |
                - Fresh: `attachment; filename="receipt-REC-<timestamp>.pdf"`
                - Cache hit: `attachment; filename="receipt-cached.pdf"`
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
            X-DocQuote-Version:
              $ref: '#/components/headers/X-DocQuote-Version'
            X-DocQuote-Free-Remaining:
              $ref: '#/components/headers/X-DocQuote-Free-Remaining'
            X-DocQuote-Usage:
              $ref: '#/components/headers/X-DocQuote-Usage'
            X-DocQuote-Credits-Remaining:
              $ref: '#/components/headers/X-DocQuote-Credits-Remaining'
            X-Generation-Ms:
              $ref: '#/components/headers/X-Generation-Ms'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
          content:
            application/pdf:
              schema:
                type: string
                format: binary
        "400":
          description: Validation error.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          $ref: '#/components/responses/Unauthorized'
        "402":
          description: Free tier exhausted and no credits remaining.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaymentRequiredCreditsResponse'
        "409":
          description: Idempotency key reused with a different payload.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IdempotencyConflictResponse'
        "429":
          $ref: '#/components/responses/RateLimited'
        "500":
          description: Internal error (pdf_timeout or pdf_too_large).
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ErrorResponse'
                  - type: object
                    properties:
                      code:
                        type: string
                        enum: [pdf_timeout, pdf_too_large]

  # ── Demo ────────────────────────────────────────────────────────────────────

  /v1/demo/pdf:
    post:
      tags: [demo]
      operationId: generateDemoPdf
      summary: Generate a demo PDF for any document type (no auth, watermarked)
      description: |
        No authentication required. Generates a PDF with a **diagonal DEMO watermark**.
        Supports all six document types via the required `documentType` field.

        **Restrictions compared to authenticated endpoints:**
        - `documentType` field required in the request body.
        - Maximum **3 line items** (vs 100 on authenticated endpoints).
        - `company.logo` and `seal` must not be `https://` URLs (SSRF protection). Base64 data URIs are allowed.
        - `logoUrl` field is blocked entirely.
        - Rate limited to **5 requests per minute** per IP.
        - No idempotency support.
        - Always returns `X-DocQuote-Demo: true`.

        Use this endpoint to prototype any document type before registering an API key.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [documentType, items]
              properties:
                documentType:
                  type: string
                  enum: [quote, proforma, invoice, receipt, credit_note, purchase_order]
                  description: Selects which document type to generate.
                  example: "invoice"
                items:
                  type: array
                  minItems: 1
                  maxItems: 3
                  description: Line items. Maximum 3 in demo mode.
                  items:
                    $ref: '#/components/schemas/QuoteItem'
              additionalProperties: true
              description: |
                All other fields are identical to the corresponding `POST /v1/*/pdf` endpoint for the
                chosen document type. The `documentType` field is stripped before dispatching.
            examples:
              demoInvoice:
                summary: Demo invoice
                value:
                  documentType: "invoice"
                  company:
                    name: "Demo Corp"
                    primaryColor: "#1D4ED8"
                  customer:
                    name: "Demo Customer"
                  items:
                    - name: "Consulting services"
                      qty: 5
                      unitPrice: 200.00
                  taxRate: 0.1
                  notes: "This is a demo invoice."
              demoQuote:
                summary: Demo quote
                value:
                  documentType: "quote"
                  company:
                    name: "Demo Corp"
                  customer:
                    name: "Demo Customer"
                  items:
                    - name: "Sample Product"
                      qty: 1
                      unitPrice: 99.00
              demoCreditNote:
                summary: Demo credit note
                value:
                  documentType: "credit_note"
                  company:
                    name: "Demo Corp"
                  customer:
                    name: "Demo Customer"
                  items:
                    - name: "Returned goods"
                      qty: 2
                      unitPrice: 50.00
                  reason: "Order partially cancelled."
      responses:
        "200":
          description: Demo PDF with watermark.
          headers:
            X-DocQuote-Demo:
              $ref: '#/components/headers/X-DocQuote-Demo'
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
            Content-Disposition:
              schema:
                type: string
                example: 'attachment; filename="demo-invoice.pdf"'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
          content:
            application/pdf:
              schema:
                type: string
                format: binary
        "400":
          description: Validation error.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                missingDocumentType:
                  summary: documentType missing
                  value:
                    error: "validation_error"
                    message: "documentType is required"
                    request_id: "a1b2c3d4-..."
                invalidDocumentType:
                  summary: Invalid documentType value
                  value:
                    error: "validation_error"
                    message: "documentType must be one of: quote, proforma, invoice, receipt, credit_note, purchase_order"
                    request_id: "a1b2c3d4-..."
                tooManyItems:
                  summary: More than 3 items
                  value:
                    error: "validation_error"
                    message: "Demo endpoint allows a maximum of 3 items"
                    request_id: "a1b2c3d4-..."
                logoUrlBlocked:
                  summary: Remote logo URL blocked
                  value:
                    error: "validation_error"
                    message: "company.logo HTTPS URLs are not allowed in demo endpoint"
                    request_id: "a1b2c3d4-..."
        "429":
          $ref: '#/components/responses/RateLimited'
        "500":
          description: PDF generation failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /v1/demo/quote/pdf:
    post:
      tags: [demo]
      operationId: generateDemoQuotePdf
      summary: Generate a demo quote PDF (legacy — use /v1/demo/pdf instead)
      description: |
        **Deprecated in favour of `POST /v1/demo/pdf` with `documentType: "quote"`.**
        This endpoint remains available for backwards compatibility and will not be removed.

        No authentication required. Generates a quote PDF with a **diagonal "DOCQUOTE DEMO" watermark**.

        **Restrictions:**
        - Maximum **3 line items**.
        - `company.logo` must not be an `https://` URL. Base64 data URIs are allowed.
        - `logoUrl` field is blocked entirely.
        - Rate limited to **5 requests per minute** per IP (shared with `/v1/demo/pdf`).
        - No idempotency support.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/QuoteRequest'
                - type: object
                  properties:
                    items:
                      maxItems: 3
                      description: Maximum 3 items in demo mode.
            example:
              company:
                name: "Demo Corp"
                primaryColor: "#1D4ED8"
              customer:
                name: "Demo Customer"
              items:
                - name: "Sample Product"
                  qty: 1
                  unitPrice: 99.00
              taxRate: 0.1
              notes: "This is a demo quote."
      responses:
        "200":
          description: Demo quote PDF with watermark.
          headers:
            X-DocQuote-Demo:
              $ref: '#/components/headers/X-DocQuote-Demo'
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
            Content-Disposition:
              schema:
                type: string
                example: 'attachment; filename="demo-quote.pdf"'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
          content:
            application/pdf:
              schema:
                type: string
                format: binary
        "400":
          description: Validation error.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "429":
          $ref: '#/components/responses/RateLimited'
        "500":
          description: PDF generation failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  # ── Credits ─────────────────────────────────────────────────────────────────

  /v1/credits/checkout:
    post:
      tags: [credits]
      operationId: creditsCheckout
      summary: Start a credit pack purchase via PayPal
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [packId]
              properties:
                packId:
                  type: string
                  enum: [basic, pro, agency]
                  description: |
                    Credit pack to purchase:
                    - `basic` — 200 PDFs / $6.99 USD
                    - `pro` — 600 PDFs / $16.99 USD
                    - `agency` — 2000 PDFs / $44.99 USD
            example:
              packId: "basic"
      responses:
        "200":
          description: PayPal checkout session created. Redirect user to `redirectUrl`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CheckoutResponse'
        "400":
          $ref: '#/components/responses/ValidationError'
        "401":
          $ref: '#/components/responses/Unauthorized'
        "500":
          description: Payment provider error or database unavailable.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /v1/credits/balance:
    get:
      tags: [credits]
      operationId: creditsBalance
      summary: Get current credit and free-tier balance
      security:
        - ApiKeyAuth: []
      responses:
        "200":
          description: Current balance.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BalanceResponse'
              example:
                credits_remaining: 145
                free_remaining: 0
        "401":
          $ref: '#/components/responses/Unauthorized'

  /v1/credits/verify:
    get:
      tags: [credits]
      operationId: creditsVerify
      summary: Verify a pending payment and accredit if PAID
      description: |
        Polls PayPal for the current status of a pending purchase. If PayPal reports
        the payment as completed, credits are atomically added to the API key. Idempotent — calling
        multiple times for the same confirmed purchase is safe.
      security:
        - ApiKeyAuth: []
      parameters:
        - name: purchaseId
          in: query
          required: true
          schema:
            type: string
            format: uuid
          description: Purchase UUID returned by `POST /v1/credits/checkout`.
      responses:
        "200":
          description: Purchase status and updated balance.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [pending, confirmed, rejected]
                  credits_remaining:
                    type: integer
                  purchase:
                    type: object
        "400":
          $ref: '#/components/responses/ValidationError'
        "401":
          $ref: '#/components/responses/Unauthorized'
        "502":
          description: PayPal API unavailable.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  # ── Schema ──────────────────────────────────────────────────────────────────

  /v1/schema:
    get:
      tags: [health]
      operationId: getSchema
      summary: Serve this OpenAPI spec
      description: Returns this OpenAPI 3.1 spec as `text/yaml`. Useful for agents that auto-discover capabilities.
      responses:
        "200":
          description: OpenAPI 3.1 YAML spec.
          content:
            text/yaml:
              schema:
                type: string
