openapi: 3.1.0
jsonSchemaDialect: https://json-schema.org/draft/2020-12/schema
x-last-updated: '2026-06-21T02:01:16-07:00'
x-gpdf-contract-version: 26.5.67
info:
  title: gPdf HTTP API
  summary: JSON-to-PDF rendering API — JSON Render, Template Render, and E-Invoice Render.
  description: |
    Public HTTP API for gPdf, a JSON-to-PDF rendering service.

    Three integration shapes live under one contract:

    - `POST /api/v1/pdf/render` — **JSON Render**. Caller describes pages,
      elements, coordinates, tables, and pagination explicitly.
    - `POST /api/v1/template-render` — **Template Render**. Caller sends
      `template_id` + business `data`; gPdf owns the layout.
    - `POST /api/v1/e-invoice/render` — **E-Invoice Render**. Produces a
      PDF/A-3b with embedded CII XML per Factur-X / ZUGFeRD.

    All three share the same authentication, error envelope, request-ID
    contract, and limits.

    Authoritative human-readable docs:
    - https://gpdf.com/docs/api-reference/
    - https://gpdf.com/docs/template-api/
    - https://gpdf.com/docs/e-invoice-api/
  version: 26.5.67
  contact:
    name: gPdf
    url: https://gpdf.com
  license:
    name: gPdf Public API Terms
    url: https://gpdf.com/legal/

servers:
  - url: https://api.gpdf.com
    description: Production. SLAs apply.
  - url: https://api-test.gpdf.com
    description: Test environment for integration verification.

externalDocs:
  description: Human-readable gPdf API reference.
  url: https://gpdf.com/docs/api-reference/

tags:
  - name: json-render
    description: Low-level JSON Render — full control over pages and elements.
  - name: template-render
    description: High-level Template Render — `template_id` + business `data`.
  - name: e-invoice
    description: Factur-X / ZUGFeRD PDF/A-3b with embedded CII XML.
  - name: playground
    description: No-key website sandbox for AI agents and local debugging.

security:
  - bearerAuth: []

paths:
  /api/v1/pdf/render:
    post:
      tags: [json-render]
      summary: Render a PDF from a JSON DocumentRequest
      description: |
        Lower-level rendering endpoint. The request body is a single
        `DocumentRequest` JSON object describing pages, elements, styles, and
        pagination.

        Returns `application/pdf` on success or `application/json` on error.
      operationId: renderPdf
      parameters:
        - $ref: '#/components/parameters/XRequestId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DocumentRequest'
            examples:
              minimum:
                summary: Smallest valid request
                value:
                  pages:
                    - size: a4
                      elements:
                        - type: text
                          content: Hello gPdf
                          layout:
                            left: 10
                            top: 20
              shippingLabel:
                summary: 100×150 mm label
                value:
                  pages:
                    - size: label_100_150
                      elements:
                        - type: text
                          content: Hello gPdf
                          layout:
                            left: 10
                            top: 18
              archiveWithPdfA:
                summary: PDF/A-2b archival report with metadata and file output
                value:
                  settings:
                    profile: pdfa-2b
                    metadata:
                      title: Monthly Archive Report
                      author: Acme Cloud Inc.
                    output:
                      mode: file
                      file_name: monthly-archive-report-20260310.pdf
                    layout:
                      pagination:
                        continuation_top_gap: 8
                        continuation_top_gap_with_header: 5
                      page_margin: { top: 10, right: 12, bottom: 10, left: 12 }
                  pages:
                    - size: a4
                      elements:
                        - type: text
                          content: Monthly archive report
                          layout:
                            left: 0
                            top: 0
                          style:
                            font_family: NotoSans-Regular
                            font_size: 14
                            font_weight: bold
      responses:
        '200':
          $ref: '#/components/responses/PdfBytes'
        '400': { $ref: '#/components/responses/Error400' }
        '401': { $ref: '#/components/responses/Error401' }
        '402': { $ref: '#/components/responses/Error402' }
        '403': { $ref: '#/components/responses/Error403' }
        '429': { $ref: '#/components/responses/Error429' }
        '500': { $ref: '#/components/responses/Error500' }

  /api/v1/template-render:
    post:
      tags: [template-render]
      summary: Render a PDF from template_id + data
      description: |
        Higher-level endpoint. Send a `template_id` and one or more `data`
        items; gPdf renders each item against the template and concatenates
        the resulting pages into a single PDF.

        Use this when an internal contract has been agreed on the
        `template_id` + field list. Use `POST /api/v1/pdf/render` when you
        need pixel control over the layout.

        Up to **10 items per request**; split larger batches across calls.
      operationId: renderTemplate
      parameters:
        - $ref: '#/components/parameters/XRequestId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TemplateRenderRequest'
            examples:
              shippingLabel:
                summary: Minimum shipping_label request
                value:
                  template_id: shipping_label
                  data:
                    - recipient_name: John Doe
                      recipient_address: 123 Main St, Los Angeles, CA
                      sender_name: Acme Warehouse
                      sender_address: 88 Harbor Rd, Long Beach, CA
                      tracking_number: TRK1234567890
              invoiceBatch:
                summary: Three invoices in one PDF
                value:
                  template_id: invoice
                  data:
                    - invoice_number: INV-001
                      date_of_issue: "2026-03-11"
                      issuer_name: Acme Cloud Inc.
                      bill_to_name: Receiver A
                      subtotal: "$100.00"
                      total: "$100.00"
                      amount_due: "$100.00"
                      items:
                        - { description: Service A, qty: 1, unit_price: "$100.00", amount: "$100.00" }
                    - invoice_number: INV-002
                      date_of_issue: "2026-03-11"
                      issuer_name: Acme Cloud Inc.
                      bill_to_name: Receiver B
                      subtotal: "$240.00"
                      total: "$240.00"
                      amount_due: "$240.00"
                      items:
                        - { description: Service B, qty: 2, unit_price: "$120.00", amount: "$240.00" }
                  output:
                    mode: file
                    file_name: invoice-batch.pdf
      responses:
        '200':
          $ref: '#/components/responses/PdfBytes'
        '400': { $ref: '#/components/responses/Error400' }
        '401': { $ref: '#/components/responses/Error401' }
        '402': { $ref: '#/components/responses/Error402' }
        '403': { $ref: '#/components/responses/Error403' }
        '429': { $ref: '#/components/responses/Error429' }
        '500': { $ref: '#/components/responses/Error500' }

  /api/playground:
    post:
      tags: [playground]
      summary: Render through the no-key website sandbox
      description: |
        Website sandbox gateway for AI coding assistants and local debugging.
        Send a regular `DocumentRequest` JSON body with
        `?endpoint=pdf-render`; do not send an `Authorization` header. The
        edge gateway injects a temporary debug key, applies fair-use limits,
        and is not for production or commercial traffic.

        Limits: **60 requests per minute per IP**, **256 KiB** JSON request
        body, PDF bytes on success.
      operationId: renderPlaygroundPdf
      security: []
      servers:
        - url: https://gpdf.com
          description: gPdf website sandbox gateway.
      parameters:
        - name: endpoint
          in: query
          required: true
          schema:
            type: string
            enum: [pdf-render]
          description: Selects the JSON Render sandbox target.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DocumentRequest'
      responses:
        '200':
          $ref: '#/components/responses/PdfBytes'
        '400':
          description: Invalid sandbox request or invalid upstream JSON Render request.
          content:
            text/plain:
              schema: { type: string }
            application/json:
              schema: { $ref: '#/components/schemas/ErrorEnvelope' }
        '413':
          description: Website sandbox request body exceeds 256 KiB.
          content:
            text/plain:
              schema: { type: string }
        '415':
          description: Request `Content-Type` is not `application/json`.
          content:
            text/plain:
              schema: { type: string }
        '429':
          description: Website sandbox rate limit exceeded.
          headers:
            Retry-After:
              schema: { type: string }
          content:
            text/plain:
              schema: { type: string }
        '502':
          description: Website sandbox upstream proxy error.
          content:
            text/plain:
              schema: { type: string }
        '503':
          description: Website sandbox temporarily unavailable.
          content:
            text/plain:
              schema: { type: string }

  /api/v1/e-invoice/capabilities:
    get:
      tags: [e-invoice]
      summary: Static capability registry for the e-invoice family
      description: |
        Advertises which standards (Factur-X, ZUGFeRD), profiles (`en16931`),
        delivery modes (`inline_pdf`, `object`), and residency modes are
        supported.

        **No authentication required.** Per-token authorisation is checked
        at render time.
      operationId: getEInvoiceCapabilities
      security: []
      responses:
        '200':
          description: Capability registry.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EInvoiceCapabilities'

  /api/v1/e-invoice/render:
    post:
      tags: [e-invoice]
      summary: Render a Factur-X / ZUGFeRD PDF/A-3b with embedded CII XML
      description: |
        Request body is a `DocumentRequest` (same shape as JSON Render) **plus**
        a required `settings.e_invoice` block. PDF/A profile is fixed at
        `pdfa-3b`.

        Returns `application/pdf` when `delivery.mode = inline_pdf` (default)
        or `application/json` (job descriptor) when `delivery.mode = object`.
      operationId: renderEInvoice
      parameters:
        - $ref: '#/components/parameters/XRequestId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EInvoiceRenderRequest'
            examples:
              minimumInline:
                summary: Inline PDF, basic validation
                value:
                  settings:
                    profile: pdfa-3b
                    e_invoice:
                      standard: factur_x
                      profile: en16931
                      document_type: invoice
                      xml:
                        format: cii
                        encoding: utf8
                        content: "<?xml version=\"1.0\" encoding=\"UTF-8\"?><rsm:CrossIndustryInvoice xmlns:rsm=\"urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100\" />"
                  pages:
                    - { size: a4, elements: [] }
              strictAsync:
                summary: Object delivery, strict async validation, report enabled
                value:
                  settings:
                    profile: pdfa-3b
                    e_invoice:
                      standard: zugferd
                      profile: en16931
                      document_type: invoice
                      xml:
                        format: cii
                        content: "<?xml version=\"1.0\" encoding=\"UTF-8\"?><rsm:CrossIndustryInvoice xmlns:rsm=\"urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100\" />"
                      validation: { level: strict, execution: async, fail_on_warnings: false }
                      delivery: { mode: object, url_ttl_seconds: 900 }
                      report: { enabled: true }
                      retention: { ttl_hours: 23, store_request: false }
                      data_residency: { mode: auto, country: DE }
                  pages:
                    - { size: a4, elements: [] }
      responses:
        '200':
          description: Either the rendered PDF (inline mode) or a job descriptor (object mode).
          headers:
            X-Request-Id: { $ref: '#/components/headers/XRequestId' }
            Content-Disposition: { $ref: '#/components/headers/ContentDisposition' }
          content:
            application/pdf:
              schema:
                type: string
                format: binary
            application/json:
              schema:
                $ref: '#/components/schemas/EInvoiceJob'
        '400': { $ref: '#/components/responses/Error400' }
        '401': { $ref: '#/components/responses/Error401' }
        '402': { $ref: '#/components/responses/Error402' }
        '403': { $ref: '#/components/responses/Error403' }
        '429': { $ref: '#/components/responses/Error429' }
        '500': { $ref: '#/components/responses/Error500' }

  /api/v1/e-invoice/jobs/{job_id}:
    get:
      tags: [e-invoice]
      summary: Poll an object-mode e-invoice job
      operationId: getEInvoiceJob
      parameters:
        - name: job_id
          in: path
          required: true
          schema: { type: string }
          description: Job identifier returned by `POST /api/v1/e-invoice/render` with `delivery.mode = object`.
        - $ref: '#/components/parameters/XRequestId'
      responses:
        '200':
          description: Current state of the job and its artifacts.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EInvoiceJob'
        '400': { $ref: '#/components/responses/Error400' }
        '401': { $ref: '#/components/responses/Error401' }
        '403': { $ref: '#/components/responses/Error403' }
        '500': { $ref: '#/components/responses/Error500' }

  /api/v1/e-invoice/jobs/{job_id}/artifacts/{artifact}:
    get:
      tags: [e-invoice]
      summary: Download a job artifact (pdf, xml, report, request)
      operationId: getEInvoiceJobArtifact
      parameters:
        - name: job_id
          in: path
          required: true
          schema: { type: string }
        - name: artifact
          in: path
          required: true
          schema:
            type: string
            enum: [pdf, xml, report, request]
          description: |
            - `pdf` — signed PDF/A-3b
            - `xml` — embedded CII XML
            - `report` — validation report (only when `report.enabled = true`)
            - `request` — original request body (only when `retention.store_request = true`)
        - $ref: '#/components/parameters/XRequestId'
      responses:
        '200':
          description: Raw artifact bytes. Content-Type matches the artifact.
          content:
            application/pdf:
              schema: { type: string, format: binary }
            application/xml:
              schema: { type: string }
            application/json:
              schema: { type: object }
        '400': { $ref: '#/components/responses/Error400' }
        '401': { $ref: '#/components/responses/Error401' }
        '403': { $ref: '#/components/responses/Error403' }
        '500': { $ref: '#/components/responses/Error500' }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: opaque
      description: |
        `Authorization: Bearer sk_live_<YOUR_API_KEY>`. Tokens are opaque and
        environment-scoped — production tokens are rejected by the test base
        URL and vice versa.

  parameters:
    XRequestId:
      name: X-Request-Id
      in: header
      required: false
      schema: { type: string }
      description: |
        Optional client-supplied request ID. If omitted, gPdf generates one.
        Always echoed in `X-Request-Id` response header and `error.req_id`.
        UUID v4 recommended.

  headers:
    XRequestId:
      description: Echoes the request's `X-Request-Id`, or a server-generated one if absent.
      schema: { type: string }
    ContentDisposition:
      description: |
        `inline; filename="..."` by default; `attachment; filename="..."`
        when `output.mode = "file"`.
      schema: { type: string }

  responses:
    PdfBytes:
      description: PDF document.
      headers:
        X-Request-Id: { $ref: '#/components/headers/XRequestId' }
        Content-Disposition: { $ref: '#/components/headers/ContentDisposition' }
      content:
        application/pdf:
          schema:
            type: string
            format: binary
    Error400:
      description: Client error (`API-001` through `API-009`).
      headers:
        X-Request-Id: { $ref: '#/components/headers/XRequestId' }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorEnvelope' }
          examples:
            invalidJson:
              value: { error: true, code: API-001, message: Invalid JSON payload, req_id: 7f7d2f5a-4cb0-4c4e-b6ef-8f6d3e0b1fd8 }
            validation:
              value: { error: true, code: API-002, message: left must be >= 0, req_id: 7f7d2f5a-4cb0-4c4e-b6ef-8f6d3e0b1fd8 }
    Error401:
      description: Missing or malformed authentication (`API-101`).
      headers:
        X-Request-Id: { $ref: '#/components/headers/XRequestId' }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorEnvelope' }
    Error402:
      description: Billing or entitlement (`API-201`, `API-202`, `API-204`).
      headers:
        X-Request-Id: { $ref: '#/components/headers/XRequestId' }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorEnvelope' }
    Error403:
      description: Authentication rejected or token blacklisted (`API-102`, `API-103`).
      headers:
        X-Request-Id: { $ref: '#/components/headers/XRequestId' }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorEnvelope' }
    Error429:
      description: Plan quota exceeded and overage is not allowed (`API-203`).
      headers:
        X-Request-Id: { $ref: '#/components/headers/XRequestId' }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorEnvelope' }
    Error500:
      description: Render or system error (`API-501`–`API-507`, `API-900`–`API-902`, `API-999`).
      headers:
        X-Request-Id: { $ref: '#/components/headers/XRequestId' }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorEnvelope' }

  schemas:
    ErrorEnvelope:
      type: object
      required: [error, code, message, req_id]
      properties:
        error:
          type: boolean
          const: true
        code:
          type: string
          description: |
            Public error code. Families:
            - `API-0xx` — client
            - `API-1xx` — authentication
            - `API-2xx` — billing / entitlement
            - `API-5xx` / `API-9xx` — render / system
          enum:
            - API-001
            - API-002
            - API-003
            - API-004
            - API-005
            - API-006
            - API-007
            - API-008
            - API-009
            - API-101
            - API-102
            - API-103
            - API-201
            - API-202
            - API-203
            - API-204
            - API-501
            - API-502
            - API-503
            - API-504
            - API-505
            - API-506
            - API-507
            - API-900
            - API-901
            - API-902
            - API-999
        message:
          type: string
          description: Human-readable explanation. Auth and system errors return redacted messages.
        req_id:
          type: string
          description: Mirrors `X-Request-Id`.

    DocumentRequest:
      type: object
      required: [pages]
      description: |
        Root request body for `POST /api/v1/pdf/render` and (with
        `settings.e_invoice`) `POST /api/v1/e-invoice/render`.
      properties:
        settings: { $ref: '#/components/schemas/Settings' }
        layers: { $ref: '#/components/schemas/Layers' }
        header: { $ref: '#/components/schemas/Section' }
        footer: { $ref: '#/components/schemas/Section' }
        pages:
          type: array
          minItems: 1
          items: { $ref: '#/components/schemas/Page' }

    Settings:
      type: object
      properties:
        defaults: { $ref: '#/components/schemas/Defaults' }
        metadata: { $ref: '#/components/schemas/Metadata' }
        output: { $ref: '#/components/schemas/OutputSettings' }
        profile:
          type: string
          enum: [pdfa-1b, pdfa-2b, pdfa-3b, pdfa-4, pdfa-2u, pdfa-3u, pdfa-ua1]
          description: |
            PDF/A archival profile. `pdfa-3b` is required for embedded XML
            (e-invoice). `pdfa-2u` and `pdfa-3u` are Unicode-mapped variants;
            `pdfa-ua1` is PDF/UA-1. Mutually exclusive with
            `settings.security` — setting both returns `API-002`.
        layout: { $ref: '#/components/schemas/DocumentLayout' }
        security:
          $ref: '#/components/schemas/SecuritySettings'
          description: |
            **Only valid on `POST /api/v1/pdf/render`.** Mutually exclusive
            with `settings.profile` (PDF/A) and `settings.e_invoice` — combining
            either returns `API-002`.
        e_invoice:
          $ref: '#/components/schemas/EInvoiceSettings'
          description: |
            **Only valid on `POST /api/v1/e-invoice/render`.** Sending it to
            JSON Render returns `API-002`.

    DocumentLayout:
      type: object
      additionalProperties: false
      properties:
        flow:
          type: boolean
          default: false
          description: |
            Enables source-order vertical planning for body `pages[].elements`.
            Element-level `layout.flow` overrides this global setting. It does
            not affect `header`, `footer`, `layers`, or watermark content.
            Global flow still validates normal body elements against their
            finite in-box `layout.top`; use JSON order plus `gap_after` for
            sequencing instead of large near-bottom top values. Omit top/bottom
            only when that specific element declares `layout.flow=true`.
        gap_after: { type: number, minimum: 0 }
        start_top: { type: number }
        page_margin: { $ref: '#/components/schemas/PageMargin' }
        pagination: { $ref: '#/components/schemas/DocumentPaginationSettings' }

    Defaults:
      type: object
      description: Global default styles. Only the four nested groups are accepted.
      additionalProperties: false
      properties:
        text: { $ref: '#/components/schemas/TextStyle' }
        stroke: { $ref: '#/components/schemas/StrokeStyle' }
        fill: { $ref: '#/components/schemas/FillStyle' }
        shape:
          type: object
          properties:
            corner_radius: { type: number, description: Default rounded-corner radius (mm). }

    Metadata:
      type: object
      properties:
        title: { type: string }
        author: { type: string }
        subject: { type: string }
        creator: { type: string, description: The application that created the source content. }
        producer: { type: string, description: The renderer. }
        language: { type: string, description: BCP-47 language tag. }

    SecuritySettings:
      type: object
      description: |
        PDF standard-security-handler encryption applied to the output PDF.
        Only valid on `POST /api/v1/pdf/render`. Mutually exclusive with
        `settings.profile` (PDF/A) and `settings.e_invoice`. See
        https://gpdf.com/docs/api-reference/#415-security-pdf-password-and-document-permissions.
      properties:
        algorithm:
          type: string
          enum: [aes_128, aes_256]
          default: aes_128
          description: |
            `aes_128` = standard-security-handler revision 4 (R=4, V=4,
            AESV2 crypt filter, PDF 1.7 output). `aes_256` = revision 6
            (R=6, V=5, AESV3 crypt filter, PDF 2.0 output). `aes_256` is
            an Enterprise policy.
        open_password:
          type: string
          maxLength: 32
          description: |
            Password required to open the PDF. Omitted, `null`, empty, or
            whitespace-only disables encryption. Max 32 UTF-8 bytes.
        owner_password:
          type: string
          maxLength: 32
          description: |
            Owner password granting full rights regardless of `permissions`.
            Must differ from `open_password`. Max 32 UTF-8 bytes.
            **Enterprise policy.**
        permissions:
          $ref: '#/components/schemas/Permissions'
          description: |
            Eight per-action permission flags. Each defaults to `true`.
            Restricting any flag requires `owner_password`. **Enterprise policy.**

    Permissions:
      type: object
      description: |
        Per-action permission flags written into the PDF's standard security
        handler. Each defaults to `true` (action allowed). Set to `false`
        to restrict.
      properties:
        print:
          type: boolean
          default: true
          description: When `false`, printing is blocked.
        print_high_quality:
          type: boolean
          default: true
          description: When `false`, high-quality printing is blocked; low-resolution printing may still be allowed by `print`.
        modify:
          type: boolean
          default: true
          description: When `false`, content edits other than annotations or form-field fill are blocked.
        copy:
          type: boolean
          default: true
          description: When `false`, selecting and copying text or graphics is blocked.
        annotate:
          type: boolean
          default: true
          description: When `false`, adding or modifying annotations and form-field definitions is blocked.
        fill_forms:
          type: boolean
          default: true
          description: When `false`, filling existing form fields is blocked independently of `annotate`.
        extract_accessibility:
          type: boolean
          default: true
          description: When `false`, extracting text or graphics for accessibility tools is blocked.
        assemble:
          type: boolean
          default: true
          description: When `false`, inserting, rotating, deleting pages, and creating bookmarks or thumbnails is blocked.

    OutputSettings:
      type: object
      properties:
        mode:
          type: string
          enum: [binary, file]
          default: binary
          description: |
            - `binary` (or omitted) → `Content-Disposition: inline; filename="..."`
            - `file` → `Content-Disposition: attachment; filename="..."`
        file_name:
          type: string
          description: Sanitised; `.pdf` is appended automatically. Default `gPdf-MMDDHHmmssSSS.pdf`.

    PageMargin:
      type: object
      properties:
        top: { type: number }
        right: { type: number }
        bottom: { type: number }
        left: { type: number }
      description: All values in mm.

    DocumentPaginationSettings:
      type: object
      description: |
        Document-level continuation offsets for auto-paginated content. These
        fields apply only when neither `settings.layout.page_margin` nor
        `pages[].layout.page_margin` is configured. They do not move first-page elements.
      additionalProperties: false
      properties:
        continuation_top_gap:
          type: number
          default: 8
          minimum: 0
          description: |
            Top gap in millimetres for generated continuation pages when no
            `header` is present and no page margin is configured.
        continuation_top_gap_with_header:
          type: number
          default: 5
          minimum: 0
          description: |
            Gap in millimetres after `header.layout.height` for generated continuation
            pages when a `header` exists and no page margin is configured.

    Layers:
      type: object
      description: |
        Document-level decoration layers. Layers do not participate in body
        pagination and do not consume `header` / `footer` height.
      properties:
        background: { $ref: '#/components/schemas/ElementLayerSection' }
        watermark: { $ref: '#/components/schemas/WatermarkLayer' }
        stamp: { $ref: '#/components/schemas/ElementLayerSection' }

    PageRepeat:
      type: string
      enum: [all_pages, first_page, last_page]
      default: all_pages

    ElementLayerSection:
      type: object
      description: |
        Background or stamp layer. Only lightweight elements are supported:
        `text`, `barcode`, `image`, `rect`, `line`, `circle`, `ellipse`,
        `polygon`, `path`, `link`, and `container`; `table` is not allowed
        in these layers.
      properties:
        repeat: { $ref: '#/components/schemas/PageRepeat' }
        elements:
          type: array
          items: { $ref: '#/components/schemas/LightweightElement' }

    WatermarkLayer:
      type: object
      required: [template, layout]
      description: |
        Algorithmic watermark layer. The first public version supports text
        watermarks only. Path-text presets (`arc_outside`, `arc_inside`,
        `wave`) are limited to a single line and at most 32 grapheme clusters.
      properties:
        repeat: { $ref: '#/components/schemas/PageRepeat' }
        opacity:
          type: number
          minimum: 0
          maximum: 1
          description: Watermark opacity from 0 to 1.
        template:
          type: object
          required: [type, content]
          properties:
            type:
              type: string
              const: text
            content: { type: string }
        style:
          allOf:
            - $ref: '#/components/schemas/TextStyle'
          description: |
            Text style for the watermark. Path-text presets do not support
            `width`, `height`, `text_overflow`, `shrink_to_fit`, `background`,
            `decoration`, `link_style`, or `wrap_policy`.
        layout: { $ref: '#/components/schemas/WatermarkLayout' }

    WatermarkLayout:
      type: object
      required: [preset]
      properties:
        preset:
          type: string
          enum: [center, tile, diagonal_tile, arc_outside, arc_inside, wave]
        angle:
          type: number
          description: Degrees. For arc presets, this is the arc anchor angle.
        gap_x: { type: number, description: Horizontal tile gap in mm. }
        gap_y: { type: number, description: Vertical tile gap in mm. }
        offset_x: { type: number, description: Horizontal offset in mm. }
        offset_y: { type: number, description: Vertical offset in mm. }
        stagger_x: { type: number, description: Alternate-row horizontal offset in mm. }
        center_x: { type: number, description: Arc center X in mm. }
        center_y: { type: number, description: Arc center Y in mm. }
        radius: { type: number, description: Arc radius in mm. }
        start_x: { type: number, description: Wave start X in mm. }
        start_y: { type: number, description: Wave start Y in mm. }
        amplitude: { type: number, description: Wave amplitude in mm. }
        wavelength: { type: number, description: Wave length in mm. }

    Section:
      type: object
      required: [layout]
      description: |
        Global `header` or `footer` section. `header.elements` render in the
        header region; `footer.elements` are shifted to the bottom of each page.
        They accept the full element union, including `table`, for repeated page
        furniture. Empty `elements` still leave the section active.
      properties:
        layout: { $ref: '#/components/schemas/SectionLayout' }
        elements:
          type: array
          items: { $ref: '#/components/schemas/Element' }

    SectionLayout:
      type: object
      required: [height]
      additionalProperties: false
      properties:
        height:
          type: number
          description: Section height in millimetres.

    Page:
      type: object
      description: |
        A page is described by `size` **or** (`width` + `height`) — never both.
        Body elements use mm from the page's top-left corner unless
        `layout.page_margin` is configured (then they use the content box).
      properties:
        size:
          type: string
          description: Named preset (case-insensitive).
          enum:
            - a4
            - a6
            - letter
            - legal
            - label_100_100
            - label_100_150
            - label_4_6_in
        width:
          type: number
          minimum: 10
          maximum: 2000
          description: Custom page width in millimetres. Required with `height` when no `size`; must be between 10 and 2000.
        height:
          type: number
          minimum: 10
          maximum: 2000
          description: Custom page height in millimetres. Required with `width` when no `size`; must be between 10 and 2000.
        layout: { $ref: '#/components/schemas/PageLayout' }
        elements:
          type: array
          items: { $ref: '#/components/schemas/Element' }
      oneOf:
        - required: [size]
          not:
            anyOf:
              - required: [width]
              - required: [height]
        - required: [width, height]
          not:
            required: [size]

    PageLayout:
      type: object
      additionalProperties: false
      properties:
        page_margin: { $ref: '#/components/schemas/PageMargin' }

    Element:
      oneOf:
        - $ref: '#/components/schemas/TextElement'
        - $ref: '#/components/schemas/BarcodeElement'
        - $ref: '#/components/schemas/ImageElement'
        - $ref: '#/components/schemas/LineElement'
        - $ref: '#/components/schemas/RectElement'
        - $ref: '#/components/schemas/CircleElement'
        - $ref: '#/components/schemas/EllipseElement'
        - $ref: '#/components/schemas/PolygonElement'
        - $ref: '#/components/schemas/PathElement'
        - $ref: '#/components/schemas/LinkElement'
        - $ref: '#/components/schemas/TableElement'
        - $ref: '#/components/schemas/ContainerElement'
      description: |
        Polymorphic body element. Use `type` as the discriminator. Body pages
        support `text`, `barcode`, `image`, `line`, `rect`, `circle`,
        `ellipse`, `polygon`, `path`, `link`, `table`, and `container`.
      discriminator:
        propertyName: type

    LightweightElement:
      oneOf:
        - $ref: '#/components/schemas/TextElement'
        - $ref: '#/components/schemas/BarcodeElement'
        - $ref: '#/components/schemas/ImageElement'
        - $ref: '#/components/schemas/LineElement'
        - $ref: '#/components/schemas/RectElement'
        - $ref: '#/components/schemas/CircleElement'
        - $ref: '#/components/schemas/EllipseElement'
        - $ref: '#/components/schemas/PolygonElement'
        - $ref: '#/components/schemas/PathElement'
        - $ref: '#/components/schemas/LinkElement'
        - $ref: '#/components/schemas/ContainerElement'
      description: |
        Element subset allowed in background layers, stamps, and container
        descendants. `table` is not allowed in this subset.

    PositionedElementBase:
      type: object
      properties:
        layout: { $ref: '#/components/schemas/ElementLayout' }
        comment: { type: string, description: Free-form note. Not rendered. }
        link: { $ref: '#/components/schemas/LinkSpec' }

    GeometryElementBase:
      type: object
      properties:
        layout: { $ref: '#/components/schemas/FlowOnlyElementLayout' }
        comment: { type: string, description: Free-form note. Not rendered. }
        link: { $ref: '#/components/schemas/LinkSpec' }

    ElementLayout:
      type: object
      description: |
        Layout subset for ordinary box-like elements. `layout.children` is only
        valid on `container`; `table` has a narrower `TableElementLayout`.
      additionalProperties: false
      properties:
        left: { type: number, description: Horizontal placement in mm from the left reference edge. Mutually exclusive with `right` and `anchor`. }
        top: { type: number, description: Vertical placement in mm from the top reference edge. Mutually exclusive with `bottom` when explicit vertical placement is used. }
        right: { type: number, description: Horizontal placement in mm from the right reference edge. Mutually exclusive with `left` and `anchor`; requires a finite element width. }
        bottom: { type: number, description: Vertical placement in mm from the bottom reference edge. Mutually exclusive with `top`; requires a finite element height. }
        anchor: { $ref: '#/components/schemas/Anchor' }
        flow:
          type: boolean
          description: |
            Element-level override for `settings.layout.flow`. Only body
            `pages[].elements` participate in automatic Y planning.
        gap_after: { type: number, minimum: 0 }
        z_index: { type: number, default: 0, description: Higher draws on top. }

    FlowOnlyElementLayout:
      type: object
      description: |
        Layout subset for geometry-coordinate elements such as line, circle,
        ellipse, and polygon. They do not support box placement fields.
      additionalProperties: false
      properties:
        flow:
          type: boolean
          description: |
            Element-level override for `settings.layout.flow`. Only body
            `pages[].elements` participate in automatic Y planning.
        gap_after: { type: number, minimum: 0 }
        z_index: { type: number, default: 0, description: Higher draws on top. }

    TableElementLayout:
      type: object
      description: |
        Layout subset for `table`. Tables use normal left/top placement and
        body flow, but do not support `right`, `bottom`, `anchor`, or
        `children`.
      additionalProperties: false
      properties:
        left: { type: number, description: Horizontal table placement in mm from the left reference edge. }
        top: { type: number, description: Vertical table placement in mm from the top reference edge. }
        flow:
          type: boolean
          description: |
            Element-level override for `settings.layout.flow`. Only body
            `pages[].elements` participate in automatic Y planning.
        gap_after: { type: number, minimum: 0 }
        z_index: { type: number, default: 0, description: Higher draws on top. }

    ContainerElementLayout:
      type: object
      description: |
        Layout for `container`. It uses ordinary box placement and adds
        `children` for container-local padding, overflow, and linear layout.
      additionalProperties: false
      properties:
        left: { type: number, description: Horizontal placement in mm from the left reference edge. Mutually exclusive with `right` and `anchor`. }
        top: { type: number, description: Vertical placement in mm from the top reference edge. Mutually exclusive with `bottom` when explicit vertical placement is used. }
        right: { type: number, description: Horizontal placement in mm from the right reference edge. Mutually exclusive with `left` and `anchor`; requires a finite element width. }
        bottom: { type: number, description: Vertical placement in mm from the bottom reference edge. Mutually exclusive with `top`; requires a finite element height. }
        anchor: { $ref: '#/components/schemas/Anchor' }
        flow:
          type: boolean
          description: |
            Element-level override for `settings.layout.flow`. Only body
            `pages[].elements` participate in automatic Y planning.
        gap_after: { type: number, minimum: 0 }
        z_index: { type: number, default: 0, description: Higher draws on top. }
        children: { $ref: '#/components/schemas/ChildrenLayout' }

    ChildrenLayout:
      type: object
      description: |
        Container child layout controls. Linear children are measured from
        their own rendered bounds; text children do not automatically stretch
        to the container width unless an explicit text width/frame/box width is
        provided.
      additionalProperties: false
      properties:
        mode: { type: string, enum: [coordinate, linear], default: coordinate }
        padding: { $ref: '#/components/schemas/BoxSpacing' }
        overflow: { $ref: '#/components/schemas/ContainerOverflow' }
        axis: { type: string, enum: [horizontal, vertical], default: vertical }
        gap: { type: number, minimum: 0, default: 0 }
        main_align: { type: string, enum: [start, center, end, space_between], default: start }
        cross_align: { type: string, enum: [start, center, end, stretch], default: start }
        wrap: { type: boolean, default: false }

    TextElement:
      allOf:
        - $ref: '#/components/schemas/PositionedElementBase'
        - type: object
          required: [type, layout, content]
          properties:
            type:
              type: string
              const: text
            _if:
              type: string
              description: Template-chain metadata; JSON Render does not evaluate `_if`.
            rotation:
              type: integer
              description: Degrees clockwise. Text accepts arbitrary integer angles.
            style: { $ref: '#/components/schemas/TextStyle' }
            frame: { $ref: '#/components/schemas/BlockTextFrame' }
            defaults: { $ref: '#/components/schemas/BlockTextDefaults' }
            content:
              oneOf:
                - type: string
                - $ref: '#/components/schemas/TextSpanContent'
                - $ref: '#/components/schemas/BlockTextContent'
              description: Plain text, span shorthand, or block text.
      description: |
        Text element. In explicit placement, provide `top` or `bottom` and
        exactly one of `left`, `right`, or `anchor`. When using `right` or
        `anchor`, provide `style.width` for plain/spans content or `frame.width`
        for block text.

    TextSpanContent:
      type: object
      required: [spans]
      properties:
        spans:
          type: array
          items: { $ref: '#/components/schemas/TextSpan' }

    TextSpan:
      type: object
      required: [text]
      properties:
        text: { type: string }
        style: { $ref: '#/components/schemas/InlineTextStyle' }
        link: { $ref: '#/components/schemas/LinkSpec' }

    BlockTextDefaults:
      type: object
      properties:
        run: { $ref: '#/components/schemas/InlineTextStyle' }
        paragraph: { $ref: '#/components/schemas/ParagraphStyle' }
        frame: { $ref: '#/components/schemas/BlockTextFrame' }

    BlockTextContent:
      type: object
      required: [blocks]
      properties:
        blocks:
          type: array
          items: { $ref: '#/components/schemas/TextBlock' }

    TextBlock:
      oneOf:
        - $ref: '#/components/schemas/ParagraphBlock'
        - $ref: '#/components/schemas/ListBlock'
        - $ref: '#/components/schemas/PageBreakBlock'

    ParagraphBlock:
      type: object
      required: [type, inlines]
      properties:
        type:
          type: string
          const: paragraph
        style: { $ref: '#/components/schemas/ParagraphStyle' }
        inlines:
          type: array
          items: { $ref: '#/components/schemas/InlineNode' }

    ListBlock:
      type: object
      required: [type, list, items]
      properties:
        type:
          type: string
          const: list
        list:
          type: object
          properties:
            kind: { type: string, enum: [ordered, unordered] }
            marker_gap: { type: number }
            item_spacing: { type: number }
            start_at: { type: integer }
        items:
          type: array
          items:
            type: object
            required: [blocks]
            properties:
              blocks:
                type: array
                items: { $ref: '#/components/schemas/ParagraphBlock' }
      description: Lists are allowed only in the full body text profile.

    PageBreakBlock:
      type: object
      required: [type]
      properties:
        type:
          type: string
          const: page_break

    InlineNode:
      oneOf:
        - $ref: '#/components/schemas/InlineTextNode'
        - $ref: '#/components/schemas/InlineVariableNode'
        - $ref: '#/components/schemas/InlineLineBreakNode'
        - $ref: '#/components/schemas/InlineTabNode'

    InlineTextNode:
      type: object
      required: [type, text]
      properties:
        type:
          type: string
          const: text
        text: { type: string }
        style: { $ref: '#/components/schemas/InlineTextStyle' }
        link: { $ref: '#/components/schemas/LinkSpec' }

    InlineVariableNode:
      type: object
      required: [type, name, scope]
      properties:
        type:
          type: string
          const: variable
        name:
          type: string
          description: JSON Render publicly guarantees `page` and `total_pages` for `scope=system`. Plain string variables use `{page}` and `{total_pages}`, not `${page}`.
        scope:
          type: string
          enum: [system, binding, computed]
      description: |
        `binding` values come from Template Render data. JSON Render does not
        evaluate business bindings, and `computed` currently fails validation.

    InlineLineBreakNode:
      type: object
      required: [type]
      properties:
        type:
          type: string
          const: line_break

    InlineTabNode:
      type: object
      required: [type]
      properties:
        type:
          type: string
          const: tab

    ParagraphStyle:
      type: object
      properties:
        align: { type: string, enum: [left, center, right, justify] }
        direction: { type: string, enum: [auto, ltr, rtl] }
        line_height: { type: number }
        space_before: { type: number }
        space_after: { type: number }
        indent_left: { type: number }
        indent_right: { type: number }
        indent_first_line: { type: number }
        hanging_indent: { type: number }
        keep_together: { type: boolean }
        keep_with_next: { type: boolean }
        widow_orphan_control: { type: boolean }
        tabs:
          type: array
          items: { $ref: '#/components/schemas/ParagraphTabStop' }

    ParagraphTabStop:
      type: object
      required: [position, align, leader]
      additionalProperties: false
      properties:
        position: { type: number }
        align: { type: string, enum: [left, center, right, decimal] }
        leader: { type: string, enum: [none, dot, dash, line] }

    BlockTextFrame:
      type: object
      properties:
        width: { type: number }
        height: { type: number }
        vertical_align: { type: string, enum: [top, middle, bottom] }
        overflow: { type: string, enum: [visible, clip, ellipsis, paginate] }
        shrink_to_fit: { type: boolean }
        min_font_size: { type: number }
        padding: { $ref: '#/components/schemas/BoxSpacing' }
        stroke: { $ref: '#/components/schemas/StrokeStyle' }
        fill: { $ref: '#/components/schemas/FillStyle' }
        columns: { type: integer, minimum: 1 }
        column_gap: { type: number }
      description: |
        `overflow=paginate` is not allowed with non-zero text rotation or
        `shrink_to_fit=true`; `frame.height`, `clip`, and `ellipsis` are not
        allowed together with `page_break`.

    BoxSpacing:
      type: object
      properties:
        top: { type: number }
        right: { type: number }
        bottom: { type: number }
        left: { type: number }

    BarcodeElement:
      allOf:
        - $ref: '#/components/schemas/PositionedElementBase'
        - type: object
          required: [type, layout, format, content, width, height]
          properties:
            type:
              type: string
              const: barcode
            format: { $ref: '#/components/schemas/BarcodeFormat' }
            content: { type: string }
            width: { type: number }
            height: { type: number }
            rotation:
              type: integer
              description: Degrees clockwise. Stable barcode output should use 0, 90, 180, or 270.
            style: { $ref: '#/components/schemas/BarcodeStyle' }
            options:
              type: object
              additionalProperties: true
            barcode_text: { $ref: '#/components/schemas/BarcodeText' }
      description: |
        Barcode element. Stable output should use rotation `0`, `90`, `180`,
        or `270`; other integer angles are currently rendered as `0`.

    BarcodeFormat:
      type: string
      description: Case-insensitive; `-` and `_` are treated as equivalent separators.
      enum:
        - qrcode
        - qr
        - microqr
        - micro-qr
        - pdf417
        - micropdf417
        - micro-pdf417
        - datamatrix
        - data-matrix
        - gs1datamatrix
        - gs1-datamatrix
        - gs1_datamatrix
        - aztec
        - maxicode
        - gs1qrcode
        - gs1-qrcode
        - gs1_qr
        - gs1-qr
        - code128
        - code-128
        - code128a
        - code-128a
        - code128b
        - code-128b
        - code128c
        - code-128c
        - gs1128
        - gs1-128
        - code39
        - code-39
        - code93
        - code-93
        - codabar
        - ean8
        - ean-8
        - ean13
        - ean-13
        - upca
        - upc-a
        - upce
        - upc-e
        - itf
        - interleaved2of5
        - itf14
        - itf-14
        - gtin8
        - gtin-8
        - gtin12
        - gtin-12
        - gtin13
        - gtin-13
        - gtin14
        - gtin-14
        - isbn
        - isbn-13
        - sscc
        - sscc-18
        - msi
        - msi-plessey
        - msi10
        - msi-10
        - msi11
        - msi-11
        - msi1010
        - msi-1010
        - msi1110
        - msi-1110
        - upus10
        - s10
        - uspsimb
        - usps-imb
        - intelligent-mail
        - upcacomposite
        - upca-composite
        - upcecomposite
        - upce-composite

    BarcodeStyle:
      type: object
      additionalProperties: false
      properties:
        color: { type: string }
        fill: { $ref: '#/components/schemas/FillStyle' }

    BarcodeText:
      type: object
      properties:
        enabled: { type: boolean }
        content: { type: string }
        position: { type: string, enum: [top, bottom] }
        offset: { type: number }
        style: { $ref: '#/components/schemas/TextStyle' }

    ImageElement:
      allOf:
        - $ref: '#/components/schemas/PositionedElementBase'
        - type: object
          required: [type, layout, width, height]
          properties:
            type:
              type: string
              const: image
            width: { type: number }
            height: { type: number }
            rotation:
              type: number
              description: Degrees clockwise. Image supports arbitrary finite numeric angles.
            asset:
              type: string
              description: Hosted asset key. Not a local file path.
            format: { type: string, enum: [jpg, jpeg, png, svg, webp] }
            source: { $ref: '#/components/schemas/ImageSource' }
      description: |
        Image element. Provide exactly one source: top-level `asset` shorthand
        with optional `format`, or explicit `source`. Data URI strings are not
        supported; base64 payloads must be raw base64 only.

    ImageSource:
      oneOf:
        - $ref: '#/components/schemas/ImageAssetSource'
        - $ref: '#/components/schemas/ImageBase64Source'

    ImageAssetSource:
      type: object
      required: [kind, key]
      properties:
        kind:
          type: string
          const: asset
        key: { type: string }
        format: { type: string, enum: [jpg, jpeg, png, svg, webp] }

    ImageBase64Source:
      type: object
      required: [kind, format, payload]
      properties:
        kind:
          type: string
          const: base64
        format: { type: string, enum: [jpg, jpeg, png, svg, webp] }
        payload:
          type: string
          description: Raw base64 content without a `data:image/...;base64,` prefix.

    LineElement:
      allOf:
        - $ref: '#/components/schemas/GeometryElementBase'
        - type: object
          required: [type, x1, y1, x2, y2]
          properties:
            type:
              type: string
              const: line
            x1: { type: number }
            y1: { type: number }
            x2: { type: number }
            y2: { type: number }
            stroke: { $ref: '#/components/schemas/StrokeStyle' }
            marker: { $ref: '#/components/schemas/MarkerStyle' }

    RectElement:
      allOf:
        - $ref: '#/components/schemas/PositionedElementBase'
        - type: object
          required: [type, layout, width, height]
          properties:
            type:
              type: string
              const: rect
            width: { type: number }
            height: { type: number }
            rotation:
              type: integer
              description: Degrees clockwise. Stable rectangle output should use 0, 90, 180, or 270.
            fill: { $ref: '#/components/schemas/FillStyle' }
            stroke: { $ref: '#/components/schemas/StrokeStyle' }
            corner_radius: { type: number }

    CircleElement:
      allOf:
        - $ref: '#/components/schemas/GeometryElementBase'
        - type: object
          required: [type, cx, cy, r]
          properties:
            type:
              type: string
              const: circle
            cx: { type: number }
            cy: { type: number }
            r: { type: number }
            fill: { $ref: '#/components/schemas/FillStyle' }
            stroke: { $ref: '#/components/schemas/StrokeStyle' }

    EllipseElement:
      allOf:
        - $ref: '#/components/schemas/GeometryElementBase'
        - type: object
          required: [type, cx, cy, rx, ry]
          properties:
            type:
              type: string
              const: ellipse
            cx: { type: number }
            cy: { type: number }
            rx: { type: number }
            ry: { type: number }
            rotation:
              type: integer
              description: Degrees clockwise.
            fill: { $ref: '#/components/schemas/FillStyle' }
            stroke: { $ref: '#/components/schemas/StrokeStyle' }

    PolygonElement:
      allOf:
        - $ref: '#/components/schemas/GeometryElementBase'
        - type: object
          required: [type, points]
          properties:
            type:
              type: string
              const: polygon
            points:
              type: array
              minItems: 3
              items:
                type: object
                required: [x, y]
                properties:
                  x: { type: number, description: Geometry-only point coordinate; not an element placement field. }
                  y: { type: number, description: Geometry-only point coordinate; not an element placement field. }
            fill: { $ref: '#/components/schemas/FillStyle' }
            stroke: { $ref: '#/components/schemas/StrokeStyle' }

    PathElement:
      type: object
      required: [type, layout, width, height, view_box, d]
      properties:
        type:
          type: string
          const: path
        layout: { $ref: '#/components/schemas/ElementLayout' }
        width:
          type: number
          exclusiveMinimum: 0
          description: Rendered width in page-space millimetres.
        height:
          type: number
          exclusiveMinimum: 0
          description: Rendered height in page-space millimetres.
        rotation:
          type: number
          default: 0
          description: Degrees clockwise. Path supports arbitrary finite numeric angles.
        comment: { type: string }
        view_box: { $ref: '#/components/schemas/PathViewBox' }
        d:
          type: string
          minLength: 1
          description: |
            SVG path data. Supports `M/L/H/V/Q/C/S/T/A/Z` plus lowercase
            relative variants. It must start with `M/m`, expand to at most
            1024 normalized segments, and contain at least one drawing segment.
        fill: { $ref: '#/components/schemas/FillStyle' }
        stroke:
          allOf:
            - $ref: '#/components/schemas/StrokeStyle'
          description: |
            `stroke.width` is in page-space millimetres and is not scaled by
            `view_box`. `stroke.compound` is not supported for path.
        link: { $ref: '#/components/schemas/LinkSpec' }
      description: |
        Controlled native vector outline. It accepts SVG path data in `d`, not
        SVG DOM, `<path>` XML, CSS style, transform, or external resources.
        Coordinates and control points must stay within `view_box`; enlarge
        `view_box` when control points need to extend farther.

    PathViewBox:
      type: object
      required: [x, y, width, height]
      properties:
        x: { type: number, description: Geometry-only local coordinate-space origin; not an element placement field. }
        y: { type: number, description: Geometry-only local coordinate-space origin; not an element placement field. }
        width: { type: number, exclusiveMinimum: 0 }
        height: { type: number, exclusiveMinimum: 0 }

    LinkElement:
      type: object
      required: [type, layout, width, height, target]
      properties:
        type:
          type: string
          const: link
        layout: { $ref: '#/components/schemas/ElementLayout' }
        width: { type: number }
        height: { type: number }
        target: { $ref: '#/components/schemas/LinkTarget' }
        alt: { type: string }
        padding: { type: number, minimum: 0 }
        border: { $ref: '#/components/schemas/LinkBorderStyle' }
        comment: { type: string }

    TableElement:
      type: object
      required: [type, layout, columns, rows]
      properties:
        type:
          type: string
          const: table
        layout: { $ref: '#/components/schemas/TableElementLayout' }
        comment: { type: string }
        width: { type: number }
        columns:
          type: array
          minItems: 1
          items: { $ref: '#/components/schemas/TableColumn' }
        rows:
          type: array
          items:
            type: object
            additionalProperties: { $ref: '#/components/schemas/TableCellValue' }
        cell: { $ref: '#/components/schemas/TableCellStyle' }
        header: { $ref: '#/components/schemas/TableHeaderConfig' }
        row_header: { $ref: '#/components/schemas/TableZoneConfig' }
        body: { $ref: '#/components/schemas/TableBodyConfig' }
        grid: { $ref: '#/components/schemas/TableGridConfig' }
        pagination: { $ref: '#/components/schemas/TablePaginationConfig' }
      description: |
        Public table schema. Legacy fields `data`, `theme`,
        `row_headers`, `corner`, and `style_overrides` are not public inputs.

    TableColumn:
      type: object
      required: [key, width]
      properties:
        key: { type: string }
        header: { $ref: '#/components/schemas/TableScalarOrBlockText' }
        width: { $ref: '#/components/schemas/TableColumnWidth' }
        role: { type: string, enum: [data, row_header], default: data }
        cell: { $ref: '#/components/schemas/TableCellStyle' }
        header_cell: { $ref: '#/components/schemas/TableCellStyle' }

    TableColumnWidth:
      oneOf:
        - type: object
          required: [mode, value]
          properties:
            mode: { type: string, const: fixed }
            value: { type: number }
        - type: object
          required: [mode, value]
          properties:
            mode: { type: string, const: percent }
            value: { type: number, minimum: 0, maximum: 100 }
        - type: object
          required: [mode]
          properties:
            mode: { type: string, const: auto }
        - type: object
          required: [mode]
          properties:
            mode: { type: string, const: fit_content }

    TableCellValue:
      oneOf:
        - $ref: '#/components/schemas/TableScalarCellValue'
        - $ref: '#/components/schemas/TableComplexCell'

    TableScalarCellValue:
      oneOf:
        - type: string
        - type: number
        - type: boolean
        - type: "null"

    TableScalarOrBlockText:
      oneOf:
        - $ref: '#/components/schemas/TableScalarCellValue'
        - $ref: '#/components/schemas/BlockTextContent'

    TableComplexCell:
      type: object
      properties:
        content: { $ref: '#/components/schemas/TableScalarOrBlockText' }
        image: { $ref: '#/components/schemas/TableCellImage' }
        barcode: { $ref: '#/components/schemas/TableCellBarcode' }
        row_span: { type: integer, minimum: 1 }
        col_span: { type: integer, minimum: 1 }
        style: { $ref: '#/components/schemas/TableCellStyle' }
        link: { $ref: '#/components/schemas/LinkSpec' }
      description: At most one of `content`, `image`, or `barcode` may be set.

    TableCellImage:
      type: object
      required: [width, height]
      properties:
        width: { type: number }
        height: { type: number }
        asset: { type: string }
        format: { type: string, enum: [jpg, jpeg, png, svg, webp] }
        source: { $ref: '#/components/schemas/ImageSource' }
        rotation:
          type: integer
          enum: [0]
          default: 0

    TableCellBarcode:
      type: object
      required: [format, content, width, height]
      properties:
        format: { $ref: '#/components/schemas/BarcodeFormat' }
        content: { type: string }
        width: { type: number }
        height: { type: number }
        style: { $ref: '#/components/schemas/BarcodeStyle' }
        options:
          type: object
          additionalProperties: true
        barcode_text: { $ref: '#/components/schemas/BarcodeText' }
        rotation:
          type: integer
          enum: [0]
          default: 0

    TableCellStyle:
      type: object
      properties:
        padding: { $ref: '#/components/schemas/TablePadding' }
        text: { $ref: '#/components/schemas/TextStyle' }
        fill: { $ref: '#/components/schemas/FillStyle' }
        content_offset_x: { type: number }
        content_offset_y: { type: number }
        borders: { $ref: '#/components/schemas/TableBorders' }

    TablePadding:
      type: object
      properties:
        x: { type: number, description: Geometry-only horizontal padding shorthand; not an element placement field. }
        y: { type: number, description: Geometry-only vertical padding shorthand; not an element placement field. }

    TableBorders:
      type: object
      properties:
        top: { $ref: '#/components/schemas/OptionalStrokeStyle' }
        right: { $ref: '#/components/schemas/OptionalStrokeStyle' }
        bottom: { $ref: '#/components/schemas/OptionalStrokeStyle' }
        left: { $ref: '#/components/schemas/OptionalStrokeStyle' }
        diagonal_tl_br: { $ref: '#/components/schemas/OptionalStrokeStyle' }
        diagonal_bl_tr: { $ref: '#/components/schemas/OptionalStrokeStyle' }

    OptionalStrokeStyle:
      oneOf:
        - type: boolean
          const: false
        - $ref: '#/components/schemas/StrokeStyle'

    TableHeaderConfig:
      type: object
      properties:
        show: { type: boolean, default: true }
        repeat_on_page_break: { type: boolean, default: true }
        rows:
          type: array
          items: { $ref: '#/components/schemas/TableHeaderRow' }
        cell: { $ref: '#/components/schemas/TableCellStyle' }

    TableHeaderRow:
      type: object
      required: [cells]
      properties:
        cells:
          type: array
          items:
            type: object
            properties:
              content: { $ref: '#/components/schemas/TableScalarOrBlockText' }
              row_span: { type: integer, minimum: 1 }
              col_span: { type: integer, minimum: 1 }
              style: { $ref: '#/components/schemas/TableCellStyle' }
              link: { $ref: '#/components/schemas/LinkSpec' }

    TableZoneConfig:
      type: object
      properties:
        cell: { $ref: '#/components/schemas/TableCellStyle' }

    TableBodyConfig:
      type: object
      properties:
        cell: { $ref: '#/components/schemas/TableCellStyle' }
        alternate_fill: { $ref: '#/components/schemas/FillStyle' }

    TableGridConfig:
      type: object
      properties:
        top: { $ref: '#/components/schemas/OptionalStrokeStyle' }
        right: { $ref: '#/components/schemas/OptionalStrokeStyle' }
        bottom: { $ref: '#/components/schemas/OptionalStrokeStyle' }
        left: { $ref: '#/components/schemas/OptionalStrokeStyle' }
        horizontal: { $ref: '#/components/schemas/OptionalStrokeStyle' }
        vertical: { $ref: '#/components/schemas/OptionalStrokeStyle' }

    TablePaginationConfig:
      type: object
      description: |
        Minimum table row heights and pagination controls. If a body table
        effectively has layout.flow=false, row_min_height is required. If that
        table's header is visible, header_min_height is also required. Body
        table pagination is measured per page fragment: visible headers consume
        measured header height, and rows fit only while used height plus
        measured row height stays inside available fragment height.
      properties:
        keep_spans_together: { type: boolean, default: true }
        row_min_height: { type: number }
        header_min_height: { type: number }

    ContainerElement:
      type: object
      required: [type, layout, width, height, elements]
      additionalProperties: false
      properties:
        type:
          type: string
          const: container
        layout: { $ref: '#/components/schemas/ContainerElementLayout' }
        width: { type: number, exclusiveMinimum: 0 }
        height: { type: number, exclusiveMinimum: 0 }
        comment: { type: string, description: Free-form note. Not rendered. }
        link: { $ref: '#/components/schemas/LinkSpec' }
        fill:
          allOf:
            - $ref: '#/components/schemas/FillStyle'
          description: Draws the outer-box background only when declared.
        stroke:
          allOf:
            - $ref: '#/components/schemas/StrokeStyle'
          description: Draws the outer-box outline only when declared.
        corner_radius: { type: number }
        defaults:
          allOf:
            - $ref: '#/components/schemas/Defaults'
          description: Applies only to descendant items, not to the container's own fill or stroke.
        elements:
          type: array
          items: { $ref: '#/components/schemas/ContainerItem' }
      description: |
        Geometric grouping element for cards, address blocks, status badges,
        totals blocks, bordered groups, clipped panels, and nested groups.
        Child coordinates are relative to the content box after
        `layout.children.padding`.
        Container does not support `table` children and is not an
        HTML/CSS layout surface.

    ContainerItem:
      oneOf:
        - $ref: '#/components/schemas/TextElement'
        - $ref: '#/components/schemas/BarcodeElement'
        - $ref: '#/components/schemas/ImageElement'
        - $ref: '#/components/schemas/LineElement'
        - $ref: '#/components/schemas/RectElement'
        - $ref: '#/components/schemas/CircleElement'
        - $ref: '#/components/schemas/EllipseElement'
        - $ref: '#/components/schemas/PolygonElement'
        - $ref: '#/components/schemas/PathElement'
        - $ref: '#/components/schemas/LinkElement'
        - $ref: '#/components/schemas/ContainerElement'
      description: Container descendants. `table` is not allowed.
      discriminator:
        propertyName: type

    ContainerLayout:
      oneOf:
        - $ref: '#/components/schemas/ContainerAbsoluteLayout'
        - $ref: '#/components/schemas/ContainerLinearLayout'
      description: |
        Omitted layout or `{}` means `{ "type": "absolute" }`. Linear-only
        fields are valid only when `type` is `linear`.

    ContainerAbsoluteLayout:
      type: object
      additionalProperties: false
      properties:
        type:
          type: string
          const: absolute
          default: absolute

    ContainerLinearLayout:
      type: object
      required: [type]
      additionalProperties: false
      properties:
        type:
          type: string
          const: linear
        axis:
          type: string
          enum: [horizontal, vertical]
          default: vertical
        gap:
          type: number
          minimum: 0
          default: 0
        main_align:
          type: string
          enum: [start, center, end, space_between]
          default: start
        cross_align:
          type: string
          enum: [start, center, end, stretch]
          default: start
        wrap:
          type: boolean
          default: false

    ContainerOverflow:
      type: string
      enum: [visible, clip, paginate]
      default: visible
      description: |
        `visible` draws children normally, `clip` clips children to the
        container outer box, and `paginate` creates continuation fragments
        across pages. `paginate` is allowed only for body-flow containers in
        `pages[].elements` or nested inside another body-flow container. In a
        paginated container, descendant text splits only when that text item
        explicitly uses `frame.overflow: "paginate"`; otherwise child items
        move whole to continuation fragments.

    Anchor:
      type: object
      description: Align horizontally to a reference edge instead of an absolute left value.
      required: [reference]
      properties:
        reference:
          type: string
          enum: [page_left, page_right, content_left, content_right]
        offset: { type: number, default: 0 }

    LinkSpec:
      type: object
      description: Hyperlink attached to an element or used as a standalone hotspot.
      required: [target]
      properties:
        target: { $ref: '#/components/schemas/LinkTarget' }
        alt: { type: string }
        padding: { type: number, minimum: 0 }
        border: { $ref: '#/components/schemas/LinkBorderStyle' }

    LinkTarget:
      oneOf:
        - $ref: '#/components/schemas/UrlLinkTarget'
        - $ref: '#/components/schemas/PageLinkTarget'

    UrlLinkTarget:
      type: object
      required: [type, url]
      properties:
        type:
          type: string
          const: url
        url:
          type: string
          description: Only `http://`, `https://`, `mailto:`, and `tel:` URLs are supported.

    PageLinkTarget:
      type: object
      required: [type, page]
      properties:
        type:
          type: string
          const: page
        page:
          type: integer
          minimum: 1
          description: One-based page number within the explicitly provided `pages` array.
        left: { type: number }
        top: { type: number }

    LinkBorderStyle:
      type: object
      properties:
        color: { type: string, description: Hex color. }
        width: { type: number, minimum: 0, description: Border width in mm; 0 means not drawn. }

    TextStyle:
      type: object
      properties:
        font_family: { type: string, examples: [NotoSans-Regular, NotoSansCJK-Regular] }
        font_mode:
          type: string
          enum: [strict, prefer]
          description: |
            - `strict` — declared family must cover all glyphs.
            - `prefer` — try declared family first, then fall back through the bundled set.
            Cannot appear without a same-level `font_family`.
        font_size: { type: number }
        font_weight: { type: string, enum: [normal, medium, semibold, bold] }
        font_style: { type: string, enum: [normal, italic] }
        color: { type: string, examples: ["#111827"] }
        opacity: { type: number, minimum: 0, maximum: 1 }
        letter_spacing: { type: number }
        highlight: { $ref: '#/components/schemas/TextHighlight' }
        line_height: { type: number }
        width: { type: number }
        height: { type: number }
        text_align: { type: string, enum: [left, center, right, justify] }
        vertical_align: { type: string, enum: [top, middle, bottom] }
        direction: { type: string, enum: [auto, ltr, rtl] }
        text_overflow: { type: string, enum: [visible, clip, ellipsis] }
        shrink_to_fit: { type: boolean }
        min_font_size: { type: number }
        wrap_policy: { type: string, enum: [normal, keep_together, no_wrap] }
        script: { type: string, enum: [normal, superscript, subscript] }
        decoration: { $ref: '#/components/schemas/TextDecoration' }
        link_style: { $ref: '#/components/schemas/TextLinkStyle' }

    InlineTextStyle:
      type: object
      properties:
        font_family: { type: string, examples: [NotoSans-Regular, NotoSansCJK-Regular] }
        font_mode: { type: string, enum: [strict, prefer] }
        font_size: { type: number }
        font_weight: { type: string, enum: [normal, medium, semibold, bold] }
        font_style: { type: string, enum: [normal, italic] }
        color: { type: string, examples: ["#111827"] }
        opacity: { type: number, minimum: 0, maximum: 1 }
        letter_spacing: { type: number }
        script: { type: string, enum: [normal, superscript, subscript] }
        highlight: { $ref: '#/components/schemas/TextHighlight' }
        decoration: { $ref: '#/components/schemas/TextDecoration' }
        link_style: { $ref: '#/components/schemas/TextLinkStyle' }

    TextHighlight:
      type: object
      additionalProperties: false
      properties:
        color: { type: string }
        opacity: { type: number, minimum: 0, maximum: 1 }
        corner_radius: { $ref: '#/components/schemas/LengthValue' }
        padding: { $ref: '#/components/schemas/TextHighlightPadding' }

    TextHighlightPadding:
      type: object
      additionalProperties: false
      properties:
        inline: { $ref: '#/components/schemas/LengthValue' }
        block: { $ref: '#/components/schemas/LengthValue' }
        inline_start: { $ref: '#/components/schemas/LengthValue' }
        inline_end: { $ref: '#/components/schemas/LengthValue' }
        block_start: { $ref: '#/components/schemas/LengthValue' }
        block_end: { $ref: '#/components/schemas/LengthValue' }

    LengthValue:
      type: object
      additionalProperties: false
      properties:
        value: { type: number }
        unit:
          type: string
          enum: [em, pt, mm]
          default: em

    TextDecoration:
      type: object
      additionalProperties: false
      properties:
        lines:
          type: array
          items:
            type: string
            enum: [underline, double_underline, line_through, overline]
        color: { type: string }
        thickness: { $ref: '#/components/schemas/LengthValue' }
        offset: { $ref: '#/components/schemas/LengthValue' }

    TextLinkStyle:
      type: object
      additionalProperties: false
      properties:
        appearance:
          type: string
          enum: [inherit, none, underline, color, underline_and_color]
        color: { type: string }

    StrokeStyle:
      type: object
      properties:
        color: { type: string }
        width: { type: number, description: Stroke width (mm). }
        opacity: { type: number, minimum: 0, maximum: 1 }
        cap: { type: string, enum: [butt, round, square] }
        join: { type: string, enum: [miter, round, bevel] }
        miter_limit: { type: number }
        dash: { $ref: '#/components/schemas/DashStyle' }
        compound: { $ref: '#/components/schemas/CompoundStrokeStyle' }

    DashStyle:
      type: object
      properties:
        preset: { type: string, enum: [solid, dashed, dotted, custom] }
        pattern:
          type: array
          items: { type: number }
        phase: { type: number }

    CompoundStrokeStyle:
      type: object
      required: [kind]
      properties:
        kind:
          type: string
          const: double
        gap:
          type: number
          description: Gap between the two strokes in mm.

    FillStyle:
      type: object
      properties:
        color: { type: string }
        opacity: { type: number, minimum: 0, maximum: 1, default: 0 }
        rule: { type: string, enum: [nonzero, even_odd] }

    MarkerStyle:
      type: object
      properties:
        start: { type: string, enum: [none, arrow, open_arrow, circle, bar] }
        end: { type: string, enum: [none, arrow, open_arrow, circle, bar] }
        size: { type: number }

    TemplateRenderRequest:
      type: object
      required: [template_id, data]
      additionalProperties: false
      properties:
        template_id:
          type: string
          description: |
            Stable per-template identifier. Treat as opaque. Some IDs are
            short codes (`k4n82q`); others are semantic slugs
            (`invoice`, `shipping_label`). Both are valid.
        data:
          type: array
          minItems: 1
          maxItems: 10
          description: |
            Each item rendered against the template; resulting pages are
            concatenated. Platform max **10 items per request** — exceeding
            returns `API-002`.
          items:
            type: object
            additionalProperties: true
            description: |
              Template-defined fields. Unknown fields are silently dropped
              today, but callers should not rely on this. Empty or
              whitespace-only strings are treated as missing for required
              fields. Dotted paths like `shipper.name` are supported.
        output: { $ref: '#/components/schemas/OutputSettings' }

    EInvoiceRenderRequest:
      allOf:
        - $ref: '#/components/schemas/DocumentRequest'
        - type: object
          description: |
            Same shape as `DocumentRequest` but `settings.e_invoice` is required
            and `settings.profile` must be `pdfa-3b` (or omitted; defaults to it).
          required: [settings]
          properties:
            settings:
              allOf:
                - $ref: '#/components/schemas/Settings'
                - type: object
                  required: [e_invoice]
                  properties:
                    profile:
                      type: string
                      enum: [pdfa-3b]
                      description: Fixed at `pdfa-3b` for e-invoice. Any other value returns `API-002`.

    EInvoiceSettings:
      type: object
      required: [standard, profile, document_type, xml]
      properties:
        standard:
          type: string
          enum: [factur_x, zugferd]
          description: Envelope standard. Determines embedded filename and XMP namespace.
        profile:
          type: string
          enum: [en16931]
          description: Semantic profile. Currently `en16931` only.
        document_type:
          type: string
          enum: [invoice]
          description: Currently invoice only.
        xml: { $ref: '#/components/schemas/EInvoiceXmlPayload' }
        validation: { $ref: '#/components/schemas/EInvoiceValidationSettings' }
        delivery: { $ref: '#/components/schemas/EInvoiceDeliverySettings' }
        report: { $ref: '#/components/schemas/EInvoiceReportSettings' }
        retention: { $ref: '#/components/schemas/EInvoiceRetentionSettings' }
        data_residency: { $ref: '#/components/schemas/EInvoiceDataResidencySettings' }
        job_id:
          type: string
          description: Idempotency ID for object delivery. UUID v7 recommended.

    EInvoiceXmlPayload:
      type: object
      required: [format, content]
      properties:
        format: { type: string, enum: [cii] }
        encoding: { type: string, enum: [utf8], default: utf8 }
        content:
          type: string
          description: UTF-8 XML content. Non-empty. **Max 2 MiB.**

    EInvoiceValidationSettings:
      type: object
      description: |
        - `level = strict` requires `delivery.mode = object`.
        - `execution = async` requires `level = strict` **and**
          `delivery.mode = object`.
      properties:
        level: { type: string, enum: [basic, strict], default: basic }
        execution: { type: string, enum: [sync, async], default: sync }
        fail_on_warnings: { type: boolean, default: false }

    EInvoiceDeliverySettings:
      type: object
      properties:
        mode:
          type: string
          enum: [inline_pdf, object]
          default: inline_pdf
          description: |
            - `inline_pdf` → PDF bytes returned inline; no job is created.
            - `object` → JSON job descriptor; artifacts retrievable via the job API until they expire.
        url_ttl_seconds:
          type: integer
          minimum: 1
          maximum: 900
          default: 900
          description: TTL of artifact download URLs (seconds).

    EInvoiceReportSettings:
      type: object
      properties:
        enabled:
          type: boolean
          default: false
          description: |
            When `true`, produces a `report.json` artifact alongside the
            PDF/XML. Requires `delivery.mode = object`.

    EInvoiceRetentionSettings:
      type: object
      properties:
        ttl_hours: { type: integer, minimum: 1, maximum: 23, default: 23 }
        store_request:
          type: boolean
          default: false
          description: When `true`, the original request body is stored as an artifact.

    EInvoiceDataResidencySettings:
      type: object
      description: |
        - `auto` resolves residency from `country` (`DE` → `eu`, `US` → `global`).
        - `eu` / `global` force the corresponding residency.
        - `strict = true` makes ambiguous or missing country a hard error rather than a fallback.
      properties:
        mode: { type: string, enum: [auto, eu, global], default: auto }
        country: { type: string, description: ISO 3166-1 alpha-2 country code. }
        strict: { type: boolean, default: false }

    EInvoiceCapabilities:
      type: object
      properties:
        schema_version: { type: integer, examples: [1] }
        package: { type: string, examples: [pdfa3_embedded_xml] }
        pdfa_profile: { type: string, examples: [pdfa-3b] }
        standards:
          type: array
          items:
            type: object
            properties:
              standard: { type: string, enum: [factur_x, zugferd] }
              standard_id: { type: string }
              name: { type: string }
              profile: { type: string, enum: [en16931] }
              document_type: { type: string, enum: [invoice] }
              xml_format: { type: string, enum: [cii] }
              embedded_file_name: { type: string }
        delivery_modes:
          type: array
          items: { type: string, enum: [inline_pdf, object] }
        data_residency_modes:
          type: array
          items: { type: string, enum: [auto, eu, global] }

    EInvoiceJob:
      type: object
      description: |
        Object-mode job descriptor. Returned both by the render endpoint
        (`delivery.mode = object`) and by `GET /api/v1/e-invoice/jobs/{job_id}`.
      properties:
        job_id: { type: string }
        status: { type: string, enum: [pending, completed, failed] }
        delivery: { type: string, enum: [object] }
        data_residency: { type: string, enum: [eu, global] }
        created_at_ms: { type: string, description: Unix milliseconds as a decimal string. }
        expires_at_ms: { type: string }
        artifacts:
          type: object
          properties:
            pdf: { $ref: '#/components/schemas/EInvoiceArtifact' }
            xml: { $ref: '#/components/schemas/EInvoiceArtifact' }
            report: { $ref: '#/components/schemas/EInvoiceArtifact' }
            request: { $ref: '#/components/schemas/EInvoiceArtifact' }
        validation:
          type: object
          properties:
            level: { type: string, enum: [basic, strict] }
            execution: { type: string, enum: [sync, async] }
            status:
              type: string
              enum: [pending, completed, failed]
              description: |
                `completed` means the report was produced — it does **not** mean
                the invoice is valid. Read `report.valid` inside the report
                artifact for that.
            report_requested: { type: boolean }

    EInvoiceArtifact:
      type: object
      properties:
        available: { type: boolean }
        content_type: { type: string }
        bytes: { type: integer }
        expires_at_ms: { type: string }
        url: { type: string, format: uri, description: Per-artifact download URL. Authenticate fetches with the same Bearer token. }
