openapi: 3.1.0
info:
  title: eCourtDate Document Generation API
  version: 1.0.0
  description: |
    Generate court-ready PDF documents: reusable, versioned **templates** with
    merge tags, or one-off **documents** from blank pages or HTML/CSS, plus
    image assets, batch generation, and a per-account audit trail.

    ## Authentication

    Every request needs an API key in the `x-api-key` header. Create and
    manage your keys in the **eCourtDate Console → APIs** page. Each key
    carries scopes that gate what it may call:

    | Scope | Grants |
    |---|---|
    | `templates:read` | Browse templates and versions, render previews |
    | `templates:write` | Create, edit, publish templates; manage assets |
    | `generate` | Generate documents, poll jobs, fetch outputs |
    | `audit:read` | Read the audit trail |

    Write operations also require an `X-On-Behalf-Of` header naming the person
    the call is made for (an email or user id, 1–128 printable characters); it
    is recorded in the audit trail.

    ## Quickstart

    Generate a one-page PDF with no setup at all:

    ```bash
    curl -o hello.pdf "https://api.pdfs.ecourtdate.com/v1/documents" \
      -H "x-api-key: $API_KEY" \
      -H "Content-Type: application/json" \
      -H "Accept: application/pdf" \
      -d '{
        "page": { "size": "letter" },
        "fields": [
          { "type": "text", "content": "# Notice\n\nYour hearing is on **August 15, 2026**.",
            "content_format": "markdown", "x": 72, "y": 72, "width": 468, "height": 200 }
        ]
      }'
    ```

    ## Conventions

    - **Coordinates** are PDF points (1/72 inch) with a **top-left origin, Y
      down**, the same space a visual editor uses. `x: 72, y: 72` is one
      inch from the top-left corner of the page.
    - **Errors** are RFC 9457 `application/problem+json` with a stable
      machine-readable `error_code` and a `request_id` to quote when
      contacting support.
    - **Warnings** never fail a document: a value that cannot be rendered is
      skipped and reported, in `warnings` (JSON responses) or in the
      `X-Warning-Count` / `X-Warnings` headers (binary responses). Pass
      `on_warning: "error"` to fail instead (`422 GENERATION_WARNINGS`).
    - **Idempotency**: send an `Idempotency-Key` header (1–128 printable
      characters) on POSTs. Retrying with the same key returns the original
      result with `Idempotent-Replay: true`; reusing a key with a different
      body is `409 IDEMPOTENCY_KEY_REUSED`. Generation replays return the
      originally generated document; a retried timeout can never produce a
      duplicate.
    - **Pagination**: list endpoints take `limit` (1–100, default 25) and an
      opaque `cursor`; responses carry `next_cursor` (null on the last page).

    ## Limits and lifecycles

    | Limit | Value |
    |---|---|
    | Fields per document | 200 |
    | Pages per document | 100 |
    | Base PDF upload | 20 MB |
    | Asset upload (PNG/JPEG) | 2 MB |
    | HTML document content | 1 MB |
    | HTML field content | 64 KB |
    | Batch items per job | 200 |
    | Download URL validity | 1 hour (re-sign any time via `GET /v1/outputs/{outputId}`) |
    | Generated output retention | ~20 hours |
    | Job status retention | 48 hours |
    | Base-PDF upload window | 15 minutes |
servers:
  - url: https://api.pdfs.ecourtdate.com
    description: Production
tags:
  - name: Templates
    description: Reusable document templates and their lifecycle.
  - name: Versions
    description: >-
      Immutable template versions: draft → ready → published → superseded.
      Editing never silently changes production output; generation follows
      the published version unless a version is pinned.
  - name: Generation
    description: Render documents from templates with merge data.
  - name: Documents
    description: One-off documents from blank pages or HTML/CSS, no template required.
  - name: Assets
    description: Your account's images (logos, seals, signatures) referenced by image fields.
  - name: Batch
    description: Asynchronous many-document jobs with per-item status.
  - name: Outputs
    description: Re-sign download URLs for generated documents.
  - name: Audit
    description: Your account's audit trail.
security:
  - apiKey: []
paths:
  /v1/templates:
    post:
      operationId: createTemplate
      tags: [Templates]
      summary: Create a template
      x-scope: templates:write
      description: >-
        Creates an empty template shell (metadata only). Add content by
        creating a version. Requires scope `templates:write`.
      parameters:
        - $ref: '#/components/parameters/OnBehalfOf'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string, maxLength: 200 }
                description: { type: string, maxLength: 2000 }
                defaults:
                  type: object
                  description: Default document settings (e.g. typography) merged into every version.
              required: [name]
              additionalProperties: false
            example:
              name: Hearing Notice
              description: Standard notice of hearing, English
      responses:
        '201':
          description: Created. `Location` points at the new template.
          headers:
            Location: { $ref: '#/components/headers/Location' }
          content:
            application/json:
              schema:
                type: object
                properties:
                  template: { $ref: '#/components/schemas/Template' }
              example:
                template:
                  id: 5f0c2a4e-8f13-4b2e-9d64-1c7a2b9e0f11
                  name: Hearing Notice
                  description: Standard notice of hearing, English
                  status: active
                  defaults: {}
                  latest_version: 0
                  published_version: null
                  created_at: '2026-08-01T17:20:04.211Z'
                  created_by: clerk@example.gov
                  updated_at: '2026-08-01T17:20:04.211Z'
                  updated_by: clerk@example.gov
        default: { $ref: '#/components/responses/Problem' }
    get:
      operationId: listTemplates
      tags: [Templates]
      summary: List templates
      x-scope: templates:read
      description: Newest first. Requires scope `templates:read`.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
        - name: status
          in: query
          schema: { type: string, enum: [active, archived, all], default: active }
        - name: q
          in: query
          schema: { type: string, maxLength: 100 }
          description: >-
            Case-insensitive name search over the newest 500 templates.
            Search results are not cursor-paginated.
      responses:
        '200':
          description: A page of templates.
          content:
            application/json:
              schema:
                type: object
                properties:
                  templates:
                    type: array
                    items: { $ref: '#/components/schemas/Template' }
                  next_cursor: { type: ['string', 'null'] }
              example:
                templates:
                  - id: 5f0c2a4e-8f13-4b2e-9d64-1c7a2b9e0f11
                    name: Hearing Notice
                    description: Standard notice of hearing, English
                    status: active
                    defaults: {}
                    latest_version: 1
                    published_version: 1
                    created_at: '2026-08-01T17:20:04.211Z'
                    created_by: clerk@example.gov
                    updated_at: '2026-08-01T17:24:41.930Z'
                    updated_by: clerk@example.gov
                next_cursor: null
        default: { $ref: '#/components/responses/Problem' }
  /v1/templates/{templateId}:
    parameters:
      - $ref: '#/components/parameters/TemplateId'
    get:
      operationId: getTemplate
      tags: [Templates]
      summary: Get a template
      x-scope: templates:read
      description: Requires scope `templates:read`.
      responses:
        '200':
          description: The template.
          content:
            application/json:
              schema:
                type: object
                properties:
                  template: { $ref: '#/components/schemas/Template' }
        default: { $ref: '#/components/responses/Problem' }
    patch:
      operationId: patchTemplate
      tags: [Templates]
      summary: Update template metadata
      x-scope: templates:write
      description: >-
        Updates `name`, `description`, and/or `defaults`. Content changes go
        through versions. Requires scope `templates:write`.
      parameters:
        - $ref: '#/components/parameters/OnBehalfOf'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string, maxLength: 200 }
                description: { type: string, maxLength: 2000 }
                defaults: { type: object }
              additionalProperties: false
            example:
              description: Standard notice of hearing, English and Spanish
      responses:
        '200':
          description: The updated template.
          content:
            application/json:
              schema:
                type: object
                properties:
                  template: { $ref: '#/components/schemas/Template' }
        default: { $ref: '#/components/responses/Problem' }
    delete:
      operationId: archiveTemplate
      tags: [Templates]
      summary: Archive a template
      x-scope: templates:write
      description: >-
        Soft delete: the template disappears from default listings and can no
        longer generate documents. Requires scope `templates:write`.
      parameters:
        - $ref: '#/components/parameters/OnBehalfOf'
      responses:
        '204': { description: Archived. }
        default: { $ref: '#/components/responses/Problem' }
  /v1/templates/{templateId}/copy:
    parameters:
      - $ref: '#/components/parameters/TemplateId'
    post:
      operationId: copyTemplate
      tags: [Templates]
      summary: Copy a template
      x-scope: templates:write
      description: >-
        Clones the published (or, if none, the latest finalized) version into
        a new template. `409 NOTHING_TO_COPY` when the source has no usable
        version. Requires scope `templates:write`.
      parameters:
        - $ref: '#/components/parameters/OnBehalfOf'
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '201':
          description: The new template. `Location` points at it.
          headers:
            Location: { $ref: '#/components/headers/Location' }
          content:
            application/json:
              schema:
                type: object
                properties:
                  template: { $ref: '#/components/schemas/Template' }
        default: { $ref: '#/components/responses/Problem' }
  /v1/templates/{templateId}/versions:
    parameters:
      - $ref: '#/components/parameters/TemplateId'
    post:
      operationId: createVersion
      tags: [Versions]
      summary: Create a version
      x-scope: templates:write
      description: >-
        Appends an immutable version. Supply the base PDF inline
        (`base_pdf_base64`) for an immediately-ready version, or omit it to
        receive one-time S3 upload instructions: upload the PDF there, then
        call finalize. A version with no base PDF and no upload renders on
        blank pages. Declared `merge_tags` refine the schema derived from the
        fields' merge tags. Requires scope `templates:write`.
      parameters:
        - $ref: '#/components/parameters/OnBehalfOf'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                fields:
                  type: array
                  maxItems: 200
                  items: { $ref: '#/components/schemas/Field' }
                defaults: { type: object }
                merge_tags:
                  type: array
                  items: { $ref: '#/components/schemas/MergeTag' }
                base_pdf_base64:
                  type: string
                  description: Base PDF, standard base64 (max 20 MB decoded).
                options: { $ref: '#/components/schemas/DocumentOptions' }
              additionalProperties: false
            example:
              fields:
                - type: text
                  merge_tag: client_name
                  x: 90
                  y: 200
                  width: 300
                  height: 20
                - type: text
                  merge_tag: hearing_date
                  x: 90
                  y: 230
                  width: 300
                  height: 20
                - type: checkbox
                  label: interpreter_needed
                  x: 90
                  y: 260
                  width: 16
                  height: 16
              merge_tags:
                - tag: client_name
                  type: string
                  required: true
                  example: Jordan Smith
                - tag: hearing_date
                  type: date
                  required: true
                  example: '2026-08-15'
              options:
                viewer: { print_scaling: none }
      responses:
        '201':
          description: >-
            The version, plus `upload` instructions when no inline PDF was
            supplied (`null` otherwise). `Location` points at the version.
          headers:
            Location: { $ref: '#/components/headers/Location' }
          content:
            application/json:
              schema:
                type: object
                properties:
                  version: { $ref: '#/components/schemas/TemplateVersion' }
                  upload:
                    oneOf:
                      - { $ref: '#/components/schemas/UploadInstructions' }
                      - type: 'null'
                  warnings:
                    type: array
                    items: { $ref: '#/components/schemas/Warning' }
              example:
                version:
                  template_id: 5f0c2a4e-8f13-4b2e-9d64-1c7a2b9e0f11
                  version: 1
                  status: draft
                  fields:
                    - { type: text, merge_tag: client_name, x: 90, y: 200, width: 300, height: 20 }
                  merge_tags:
                    - { tag: client_name, type: string, required: true, example: Jordan Smith }
                  defaults: {}
                  options: { viewer: { print_scaling: none } }
                  page_count: null
                  bytes: null
                  sha256: null
                  pages: []
                  acro_fields: []
                  created_at: '2026-08-01T17:22:10.049Z'
                  created_by: clerk@example.gov
                  finalized_at: null
                  published_at: null
                upload:
                  url: https://uploads.example.com/bucket
                  fields: { key: '…', Policy: '…', X-Amz-Signature: '…' }
                  key: tenants/agency/uploads/…
                  expires_in: 900
                  note: POST the base PDF here, then call finalize on this version
                warnings: []
        default: { $ref: '#/components/responses/Problem' }
    get:
      operationId: listVersions
      tags: [Versions]
      summary: List versions
      x-scope: templates:read
      description: Newest first. Requires scope `templates:read`.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of versions.
          content:
            application/json:
              schema:
                type: object
                properties:
                  versions:
                    type: array
                    items: { $ref: '#/components/schemas/TemplateVersion' }
                  next_cursor: { type: ['string', 'null'] }
        default: { $ref: '#/components/responses/Problem' }
  /v1/templates/{templateId}/versions/{version}:
    parameters:
      - $ref: '#/components/parameters/TemplateId'
      - $ref: '#/components/parameters/VersionNumber'
    get:
      operationId: getVersion
      tags: [Versions]
      summary: Get a version
      x-scope: templates:read
      description: >-
        Includes the merge-tag schema, per-page geometry, and the base PDF's
        form-field inventory (`acro_fields`) once finalized. Requires scope
        `templates:read`.
      responses:
        '200':
          description: The version.
          content:
            application/json:
              schema:
                type: object
                properties:
                  version: { $ref: '#/components/schemas/TemplateVersion' }
        default: { $ref: '#/components/responses/Problem' }
  /v1/templates/{templateId}/versions/{version}/finalize:
    parameters:
      - $ref: '#/components/parameters/TemplateId'
      - $ref: '#/components/parameters/VersionNumber'
    post:
      operationId: finalizeVersion
      tags: [Versions]
      summary: Finalize a version
      x-scope: templates:write
      description: >-
        Validates the uploaded base PDF and marks the version `ready`.
        Idempotent once ready. `409 UPLOAD_NOT_FOUND` when the PDF was never
        uploaded; `422 INVALID_PDF` / `BASE_PDF_TOO_LARGE` / `TOO_MANY_PAGES`
        when it cannot be used. Requires scope `templates:write`.
      parameters:
        - $ref: '#/components/parameters/OnBehalfOf'
      responses:
        '200':
          description: The finalized version.
          content:
            application/json:
              schema:
                type: object
                properties:
                  version: { $ref: '#/components/schemas/TemplateVersion' }
                  warnings:
                    type: array
                    items: { $ref: '#/components/schemas/Warning' }
        default: { $ref: '#/components/responses/Problem' }
  /v1/templates/{templateId}/versions/{version}/publish:
    parameters:
      - $ref: '#/components/parameters/TemplateId'
      - $ref: '#/components/parameters/VersionNumber'
    post:
      operationId: publishVersion
      tags: [Versions]
      summary: Publish a version
      x-scope: templates:write
      description: >-
        Points the template's `published_version` at this version; the
        previously published version becomes `superseded`. Republishing a
        superseded version is how you roll back. Generation resolves the
        published version unless a request pins one; edits never silently
        change production output. Requires scope `templates:write`.
      parameters:
        - $ref: '#/components/parameters/OnBehalfOf'
      responses:
        '200':
          description: The template and version after publishing.
          content:
            application/json:
              schema:
                type: object
                properties:
                  template: { $ref: '#/components/schemas/Template' }
                  version: { $ref: '#/components/schemas/TemplateVersion' }
        default: { $ref: '#/components/responses/Problem' }
  /v1/templates/{templateId}/versions/{version}/preview:
    parameters:
      - $ref: '#/components/parameters/TemplateId'
      - $ref: '#/components/parameters/VersionNumber'
    post:
      operationId: previewVersion
      tags: [Versions]
      summary: Render a preview
      x-scope: templates:read
      description: >-
        Renders the version with whatever sample data is supplied (missing
        merge values are tolerated), watermarked by default. Requires scope
        `templates:read` only, so editor and browse UIs can preview without
        generation rights.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                data: { type: object }
                name: { type: string }
                flatten: { type: boolean }
                watermark: { type: boolean, default: true }
              additionalProperties: false
            example:
              data: { client_name: Jordan Smith, hearing_date: '2026-08-15' }
      responses:
        '200':
          description: The preview PDF.
          content:
            application/pdf:
              schema: { type: string, format: binary }
        default: { $ref: '#/components/responses/Problem' }
  /v1/templates/{templateId}/generate:
    parameters:
      - $ref: '#/components/parameters/TemplateId'
    post:
      operationId: generateFromTemplate
      tags: [Generation]
      summary: Generate a document from a template
      x-scope: generate
      description: >-
        Renders the published version (or a pinned `version`) with the given
        merge `data`. Strict validation (the default) rejects missing
        required tags (`422 MISSING_MERGE_TAGS`), type mismatches
        (`422 INVALID_MERGE_VALUES`), and undeclared tags
        (`422 UNKNOWN_MERGE_TAGS`); `data_validation: "lenient"` renders
        what it can and reports the rest as warnings. Requires scope
        `generate`.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                data:
                  type: object
                  description: Merge-tag values, validated against the version's merge-tag schema.
                version:
                  type: integer
                  description: Pin a version (default = the published version).
                name:
                  type: string
                  description: Output filename; a single `.pdf` suffix is ensured.
                flatten:
                  type: boolean
                  description: Flatten all form fields into static page content.
                data_validation: { type: string, enum: [strict, lenient], default: strict }
                on_warning: { type: string, enum: [ignore, error], default: ignore }
                dry_run:
                  type: boolean
                  description: Validate the data without rendering.
                file_format: { type: string, enum: [pdf], default: pdf }
                response:
                  type: object
                  properties:
                    mode:
                      type: string
                      enum: [binary, json]
                      default: binary
                      description: binary returns the PDF bytes; json uploads it and returns a download URL.
                  additionalProperties: false
                options: { $ref: '#/components/schemas/DocumentOptions' }
              additionalProperties: false
            examples:
              binary:
                summary: Binary response (the default)
                value:
                  data: { client_name: Jordan Smith, hearing_date: '2026-08-15' }
                  name: hearing-notice-smith
              json:
                summary: JSON response with a download URL
                value:
                  data: { client_name: Jordan Smith, hearing_date: '2026-08-15' }
                  response: { mode: json }
              dry_run:
                summary: Validate data without rendering
                value:
                  data: { client_name: Jordan Smith }
                  dry_run: true
      responses:
        '200':
          description: >-
            Binary mode returns the PDF; json mode returns download details;
            `dry_run` returns the validation result.
          headers:
            X-Warning-Count: { $ref: '#/components/headers/XWarningCount' }
            X-Warnings: { $ref: '#/components/headers/XWarnings' }
            Idempotent-Replay: { $ref: '#/components/headers/IdempotentReplay' }
          content:
            application/pdf:
              schema: { type: string, format: binary }
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/GenerateResult'
                  - $ref: '#/components/schemas/DryRunResult'
              examples:
                json:
                  summary: json mode
                  value:
                    files:
                      - output_id: 4406d607-16bf-427c-808c-26a7fc26ab0c
                        url: https://outputs.example.com/…signed…
                        name: hearing-notice-smith.pdf
                        format: pdf
                        content_type: application/pdf
                        bytes: 24831
                        sha256: 0a33d20b72da3796f8e281d0861667de564a8215c6d37bb642bd99c023dd421b
                        page_count: 2
                        expires_in: 3600
                    template_id: 5f0c2a4e-8f13-4b2e-9d64-1c7a2b9e0f11
                    version: 1
                    warnings: []
                dry_run:
                  summary: dry_run mode
                  value:
                    valid: true
                    template_id: 5f0c2a4e-8f13-4b2e-9d64-1c7a2b9e0f11
                    version: 1
                    warnings: []
        default: { $ref: '#/components/responses/Problem' }
  /v1/documents:
    post:
      operationId: createDocument
      tags: [Documents]
      summary: Generate a document without a template
      x-scope: generate
      description: >-
        One-off generation. Without `html`, the server synthesizes blank
        pages of the requested geometry and draws `fields` on them. With
        `html`, the body is authored in HTML/CSS and rendered by an isolated
        browser engine with real CSS layout and pagination; the content
        determines the page count (`page.count` is rejected), `page.size` /
        `orientation` set the paper, and a JSON response is required
        (`422 HTML_REQUIRES_JSON_RESPONSE`). Fields then compose onto the
        rendered pages.


        HTML is sanitized to a safe allow-list before rendering: scripts,
        frames, forms, and anything that fetches are removed (reported as an
        `HTML_CONTENT_SANITIZED` warning); images must be `data:` URIs;
        stylesheets and `style` attributes pass through. The fonts
        `"Noto Sans"` (default), `"Noto Sans Hebrew"`, and `"Noto Sans SC"`
        are available by name. `html.tagged: true` produces an accessible
        tagged PDF (Section 508) and cannot be combined with `fields`,
        `options`, `typography`, `flatten`, or `unicode`
        (`422 HTML_TAGGED_CONFLICT`).


        `fill` fields are rejected, as there is no existing form to fill
        (`422 FILL_REQUIRES_TEMPLATE`); every other field type works exactly
        as in template versions. Requires scope `generate`.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                html: { $ref: '#/components/schemas/HtmlSource' }
                page: { $ref: '#/components/schemas/PageSpec' }
                fields:
                  type: array
                  maxItems: 200
                  items: { $ref: '#/components/schemas/Field' }
                typography: { $ref: '#/components/schemas/Typography' }
                unicode:
                  type: boolean
                  default: false
                  description: Render text with embedded Unicode fonts (Latin-extended, Greek, Cyrillic, CJK, RTL).
                flatten: { type: boolean, default: false }
                name: { type: string }
                on_warning: { type: string, enum: [ignore, error], default: ignore }
                file_format: { type: string, enum: [pdf], default: pdf }
                response:
                  type: object
                  properties:
                    mode: { type: string, enum: [binary, json], default: binary }
                  additionalProperties: false
                options: { $ref: '#/components/schemas/DocumentOptions' }
              additionalProperties: false
            examples:
              markdown_letter:
                summary: Blank page with a markdown body
                value:
                  page: { size: letter }
                  fields:
                    - type: text
                      content: "# Notice\n\nYour hearing is on **August 15, 2026**.\n\n- Bring ID\n- Arrive 15 minutes early"
                      content_format: markdown
                      x: 72
                      y: 72
                      width: 468
                      height: 400
              html_document:
                summary: Whole document authored in HTML/CSS
                value:
                  name: hearing-notice
                  page: { size: letter }
                  html:
                    content: "<h1>Notice of Hearing</h1><p>Your hearing is scheduled for <strong>August 15, 2026</strong> at 9:00 AM.</p>"
                  response: { mode: json }
              html_box:
                summary: An HTML box composed onto a blank page
                value:
                  page: { size: letter }
                  fields:
                    - type: html
                      content: "<p style='font-size:14px'>Dear {{client_name}},<br>your hearing is confirmed.</p>"
                      x: 72
                      y: 144
                      width: 468
                      height: 200
                  response: { mode: json }
      responses:
        '200':
          description: >-
            Binary mode returns the PDF; json mode returns download details
            with `template_id`/`version` null.
          headers:
            X-Warning-Count: { $ref: '#/components/headers/XWarningCount' }
            X-Warnings: { $ref: '#/components/headers/XWarnings' }
            Idempotent-Replay: { $ref: '#/components/headers/IdempotentReplay' }
          content:
            application/pdf:
              schema: { type: string, format: binary }
            application/json:
              schema: { $ref: '#/components/schemas/GenerateResult' }
              example:
                files:
                  - output_id: 4406d607-16bf-427c-808c-26a7fc26ab0c
                    url: https://outputs.example.com/…signed…
                    name: hearing-notice.pdf
                    format: pdf
                    content_type: application/pdf
                    bytes: 757
                    sha256: 0a33d20b72da3796f8e281d0861667de564a8215c6d37bb642bd99c023dd421b
                    page_count: 1
                    expires_in: 3600
                template_id: null
                version: null
                warnings: []
        default: { $ref: '#/components/responses/Problem' }
  /v1/templates/{templateId}/generate-batch:
    parameters:
      - $ref: '#/components/parameters/TemplateId'
    post:
      operationId: generateBatch
      tags: [Batch]
      summary: Generate many documents as one job
      x-scope: generate
      description: >-
        Validates every item's `data` up front (`422 BATCH_ITEMS_INVALID`
        lists per-item errors before anything is enqueued) and pins the
        resolved version at submit time, so a publish mid-job never splits a
        batch. Items render asynchronously; poll the job for status and
        download URLs. Requires scope `generate`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                items:
                  type: array
                  minItems: 1
                  maxItems: 200
                  items:
                    type: object
                    properties:
                      data: { type: object }
                      name: { type: string }
                    additionalProperties: false
                version: { type: integer, description: Pin a version (default = the published version). }
                options: { $ref: '#/components/schemas/DocumentOptions' }
                data_validation: { type: string, enum: [strict, lenient], default: strict }
              required: [items]
              additionalProperties: false
            example:
              items:
                - { data: { client_name: Jordan Smith, hearing_date: '2026-08-15' }, name: notice-smith }
                - { data: { client_name: Alex Rivera, hearing_date: '2026-08-15' }, name: notice-rivera }
      responses:
        '202':
          description: Accepted. `Location` points at the job.
          headers:
            Location: { $ref: '#/components/headers/Location' }
          content:
            application/json:
              schema:
                type: object
                properties:
                  job_id: { type: string, format: uuid }
                  status: { type: string, enum: [queued] }
                  total: { type: integer }
              example:
                job_id: 9d2c6a71-0b3f-4c58-8f2e-6a1d4b7c9e05
                status: queued
                total: 2
        default: { $ref: '#/components/responses/Problem' }
  /v1/jobs/{jobId}:
    parameters:
      - name: jobId
        in: path
        required: true
        schema: { type: string, format: uuid }
    get:
      operationId: getJob
      tags: [Batch]
      summary: Get job status and results
      x-scope: generate
      description: >-
        Per-item status plus freshly signed download URLs for every finished
        document. URLs are re-signed on every read, so polling always
        returns usable links. Jobs expire 48 hours after submission.
        Requires scope `generate`.
      responses:
        '200':
          description: The job.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Job' }
              example:
                job_id: 9d2c6a71-0b3f-4c58-8f2e-6a1d4b7c9e05
                status: completed
                template_id: 5f0c2a4e-8f13-4b2e-9d64-1c7a2b9e0f11
                version: 1
                total: 2
                pending: 0
                succeeded: 2
                failed: 0
                created_at: '2026-08-01T17:31:02.114Z'
                items:
                  - { index: 0, status: succeeded, output_id: 0f6a1c9d-2e47-4b3a-8d15-7c0b9e4f2a61 }
                  - { index: 1, status: succeeded, output_id: 6b1e8f30-9a5d-42c7-b3f4-0d2a7c8e5b19 }
                files:
                  - index: 0
                    output_id: 0f6a1c9d-2e47-4b3a-8d15-7c0b9e4f2a61
                    url: https://outputs.example.com/…signed…
                    name: notice-smith.pdf
                    bytes: 24831
                    sha256: 6f3f2b…
                    expires_in: 3600
                  - index: 1
                    output_id: 6b1e8f30-9a5d-42c7-b3f4-0d2a7c8e5b19
                    url: https://outputs.example.com/…signed…
                    name: notice-rivera.pdf
                    bytes: 24902
                    sha256: a1c04d…
                    expires_in: 3600
                next_cursor: null
        default: { $ref: '#/components/responses/Problem' }
    delete:
      operationId: cancelJob
      tags: [Batch]
      summary: Cancel a job
      x-scope: generate
      description: >-
        Stops items that have not rendered yet; already-rendered items keep
        their outputs. Idempotent. Requires scope `generate`.
      responses:
        '204': { description: Cancelled. }
        default: { $ref: '#/components/responses/Problem' }
  /v1/assets:
    post:
      operationId: createAsset
      tags: [Assets]
      summary: Upload an asset
      x-scope: templates:write
      description: >-
        Uploads an image (logo, seal, signature) for use in `image` fields.
        PNG or JPEG, max 2 MB, inline base64. Files are validated by content,
        not filename; a corrupt image fails here (`422 INVALID_IMAGE`), not
        on a rendered document. Requires scope `templates:write`.
      parameters:
        - $ref: '#/components/parameters/OnBehalfOf'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string, maxLength: 100 }
                content_type: { type: string, enum: [image/png, image/jpeg] }
                content_base64: { type: string }
              required: [name, content_type, content_base64]
              additionalProperties: false
            example:
              name: county-seal
              content_type: image/png
              content_base64: iVBORw0KGgoAAAANSUhEUgAA…
      responses:
        '201':
          description: Created. `Location` points at the asset.
          headers:
            Location: { $ref: '#/components/headers/Location' }
          content:
            application/json:
              schema:
                type: object
                properties:
                  asset: { $ref: '#/components/schemas/Asset' }
              example:
                asset:
                  asset_id: 2c9e5b7a-4d18-4f36-a2c0-8b61d9f3e754
                  name: county-seal
                  content_type: image/png
                  bytes: 48213
                  sha256: 91b2c4…
                  width: 400
                  height: 400
                  created_at: '2026-08-01T17:18:40.552Z'
                  created_by: clerk@example.gov
        default: { $ref: '#/components/responses/Problem' }
    get:
      operationId: listAssets
      tags: [Assets]
      summary: List assets
      x-scope: templates:read
      description: Requires scope `templates:read`.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of assets.
          content:
            application/json:
              schema:
                type: object
                properties:
                  assets:
                    type: array
                    items: { $ref: '#/components/schemas/Asset' }
                  next_cursor: { type: ['string', 'null'] }
        default: { $ref: '#/components/responses/Problem' }
  /v1/assets/{assetId}:
    parameters:
      - name: assetId
        in: path
        required: true
        schema: { type: string, format: uuid }
    get:
      operationId: getAsset
      tags: [Assets]
      summary: Get an asset
      x-scope: templates:read
      description: Metadata only. Requires scope `templates:read`.
      responses:
        '200':
          description: The asset.
          content:
            application/json:
              schema:
                type: object
                properties:
                  asset: { $ref: '#/components/schemas/Asset' }
        default: { $ref: '#/components/responses/Problem' }
    delete:
      operationId: deleteAsset
      tags: [Assets]
      summary: Delete an asset
      x-scope: templates:write
      description: >-
        Fields that still reference a deleted asset are skipped at generation
        with an `ASSET_NOT_FOUND` warning. Requires scope `templates:write`.
      parameters:
        - $ref: '#/components/parameters/OnBehalfOf'
      responses:
        '204': { description: Deleted. }
        default: { $ref: '#/components/responses/Problem' }
  /v1/outputs/{outputId}:
    parameters:
      - name: outputId
        in: path
        required: true
        schema: { type: string, format: uuid }
    get:
      operationId: getOutput
      tags: [Outputs]
      summary: Re-sign a download URL
      x-scope: generate
      description: >-
        Returns a freshly signed download URL for a previously generated
        document. Use it whenever an earlier URL has expired. Output records
        themselves expire ~20 hours after generation
        (`404 OUTPUT_NOT_FOUND`). Requires scope `generate`.
      responses:
        '200':
          description: The output with a fresh URL.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/OutputRecord' }
              example:
                output_id: 4406d607-16bf-427c-808c-26a7fc26ab0c
                url: https://outputs.example.com/…signed…
                name: hearing-notice.pdf
                format: pdf
                content_type: application/pdf
                bytes: 757
                sha256: 0a33d20b72da3796f8e281d0861667de564a8215c6d37bb642bd99c023dd421b
                page_count: 1
                template_id: null
                version: null
                created_at: '2026-08-01T17:28:53.402Z'
                expires_in: 3600
        default: { $ref: '#/components/responses/Problem' }
  /v1/audit:
    get:
      operationId: listAudit
      tags: [Audit]
      summary: Read the audit trail
      x-scope: audit:read
      description: >-
        Every write and generation is recorded with the acting user
        (`X-On-Behalf-Of`) and the API key used. Newest first. The `action`
        and `resource` filters apply within each fetched page, so a filtered
        page may hold fewer than `limit` entries while `next_cursor` is
        still set. Requires scope `audit:read`.
      parameters:
        - name: action
          in: query
          schema: { type: string }
          description: Exact match, e.g. `template.published`.
        - name: resource
          in: query
          schema: { type: string }
          description: Prefix match, e.g. `template/5f0c2a4e…`.
        - name: from
          in: query
          schema: { type: string }
          description: ISO 8601 timestamp or date prefix (inclusive).
        - name: to
          in: query
          schema: { type: string }
          description: ISO 8601 timestamp or date prefix (inclusive).
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of audit records.
          content:
            application/json:
              schema:
                type: object
                properties:
                  audits:
                    type: array
                    items: { $ref: '#/components/schemas/AuditRecord' }
                  next_cursor: { type: ['string', 'null'] }
              example:
                audits:
                  - action: template.published
                    resource: template/5f0c2a4e-8f13-4b2e-9d64-1c7a2b9e0f11
                    actor: clerk@example.gov
                    api_key_id: a1b2c3d4e5
                    at: '2026-08-01T17:24:41.930Z'
                    details: { version: 1 }
                next_cursor: null
        default: { $ref: '#/components/responses/Problem' }
components:
  securitySchemes:
    apiKey:
      type: apiKey
      in: header
      name: x-api-key
      description: >-
        Create and manage API keys in the eCourtDate Console → APIs page.
        Each key carries the scopes listed in the introduction.
  parameters:
    TemplateId:
      name: templateId
      in: path
      required: true
      schema: { type: string, format: uuid }
    VersionNumber:
      name: version
      in: path
      required: true
      schema: { type: integer, minimum: 1 }
    Limit:
      name: limit
      in: query
      schema: { type: integer, minimum: 1, maximum: 100, default: 25 }
    Cursor:
      name: cursor
      in: query
      schema: { type: string }
      description: Opaque pagination cursor from a previous response's `next_cursor`.
    OnBehalfOf:
      name: X-On-Behalf-Of
      in: header
      required: true
      schema: { type: string, minLength: 1, maxLength: 128 }
      description: >-
        The person this call is made for (an email or user id, printable
        characters). Required on all write operations; recorded in the audit
        trail. Missing on a write → `422 MISSING_ACTOR`.
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      schema: { type: string, minLength: 1, maxLength: 128 }
      description: >-
        Makes the request safely retryable: the same key returns the original
        result with `Idempotent-Replay: true`; the same key with a different
        body is `409 IDEMPOTENCY_KEY_REUSED`. Keys are remembered for 24
        hours. On generation endpoints a replay returns the originally
        generated document.
  headers:
    Location:
      description: URL of the created resource.
      schema: { type: string }
    XWarningCount:
      description: >-
        Total number of warn-and-skip events during rendering. Absent when
        there were none. May exceed the entries listed in `X-Warnings` when
        that list is truncated.
      schema: { type: string }
    XWarnings:
      description: >-
        JSON array of `{code, message}` warning objects (ASCII-escaped,
        capped at 4 KB). See the Warning schema for the stable codes.
      schema: { type: string }
    IdempotentReplay:
      description: Present (`true`) when this response is an idempotent replay of an earlier request.
      schema: { type: string }
  responses:
    Problem:
      description: >-
        Error. RFC 9457 `application/problem+json` with a stable
        `error_code` and a `request_id` to quote when contacting support.
      content:
        application/problem+json:
          schema: { $ref: '#/components/schemas/ProblemDetails' }
          example:
            type: about:blank
            title: Not Found
            status: 404
            detail: No such template
            error_code: TEMPLATE_NOT_FOUND
            request_id: eb0315c5-14ad-4250-a8aa-2c7bb41fa26d
  schemas:
    ProblemDetails:
      type: object
      properties:
        type: { type: string }
        title: { type: string }
        status: { type: integer }
        detail: { type: string }
        error_code:
          type: string
          description: >-
            Stable machine-readable code. Treat as an open set; new codes
            may be added. Current codes by status:
            **400** INVALID_JSON, INVALID_REQUEST, UNKNOWN_FIELD,
            INVALID_CURSOR, INVALID_IDEMPOTENCY_KEY, INVALID_STATUS_FILTER,
            INVALID_VERSION, INVALID_RESPONSE_MODE, UNSUPPORTED_FILE_FORMAT,
            INVALID_ACTOR ·
            **401** MISSING_API_KEY ·
            **403** KEY_NOT_PROVISIONED, INSUFFICIENT_SCOPE ·
            **404** ROUTE_NOT_FOUND, TEMPLATE_NOT_FOUND, VERSION_NOT_FOUND,
            ASSET_NOT_FOUND, JOB_NOT_FOUND, OUTPUT_NOT_FOUND ·
            **405** METHOD_NOT_ALLOWED ·
            **409** CONCURRENT_MODIFICATION, IDEMPOTENCY_KEY_REUSED,
            TEMPLATE_ARCHIVED, NO_PUBLISHED_VERSION, NOTHING_TO_COPY,
            UPLOAD_NOT_FOUND, VERSION_NOT_FINALIZED ·
            **413** HTML_TOO_LARGE ·
            **422** MISSING_ACTOR, UNSUPPORTED_FIELD_TYPE,
            MISSING_MERGE_TAGS, INVALID_MERGE_VALUES, UNKNOWN_MERGE_TAGS,
            INVALID_PDF, BASE_PDF_TOO_LARGE, TOO_MANY_PAGES, ASSET_TOO_LARGE,
            INVALID_IMAGE, GENERATION_WARNINGS, BATCH_ITEMS_INVALID,
            TABLE_OVERFLOW, FILL_REQUIRES_TEMPLATE, RENDER_FAILED,
            HTML_RENDER_FAILED, HTML_RENDER_TIMEOUT,
            HTML_REQUIRES_JSON_RESPONSE, HTML_TAGGED_CONFLICT,
            HTML_TOO_MANY_PAGES ·
            **500** INTERNAL_ERROR ·
            **503** SERVICE_UNCONFIGURED
        request_id: { type: ['string', 'null'] }
      required: [title, status, error_code]
    Warning:
      type: object
      description: >-
        A warn-and-skip event: the affected value was skipped, the document
        still rendered. Stable codes: TEXT_SUBSTITUTED,
        SYMBOL_NOT_ENCODABLE, APPEARANCES_STALE, FILL_UNKNOWN_FIELD,
        FILL_NOT_ENCODABLE, FILL_OPTION_MISMATCH, FILL_UNSUPPORTED_KIND,
        FILL_FAILED, ASSET_NOT_FOUND, FIELD_OUT_OF_BOUNDS, BARCODE_INVALID,
        TABLE_ROWS_TRUNCATED, UNICODE_MISSING_GLYPHS,
        DIRECTION_REQUIRES_UNICODE, HTML_CONTENT_SANITIZED,
        HTML_FIELD_TRUNCATED (open set; new codes may be added).
      properties:
        code: { type: string }
        message: { type: string }
      required: [code, message]
    Template:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        description: { type: string }
        status: { type: string, enum: [active, archived] }
        defaults: { type: object }
        latest_version: { type: integer }
        published_version: { type: ['integer', 'null'] }
        created_at: { type: string, format: date-time }
        created_by: { type: ['string', 'null'] }
        updated_at: { type: string, format: date-time }
        updated_by: { type: ['string', 'null'] }
    TemplateVersion:
      type: object
      properties:
        template_id: { type: string, format: uuid }
        version: { type: integer }
        status:
          type: string
          enum: [draft, ready, published, superseded]
          description: >-
            draft = awaiting its base PDF; ready = usable; published = the
            version generation resolves by default; superseded = previously
            published.
        fields:
          type: array
          items: { $ref: '#/components/schemas/Field' }
        merge_tags:
          type: array
          items: { $ref: '#/components/schemas/MergeTag' }
        defaults: { type: object }
        options: { $ref: '#/components/schemas/DocumentOptions' }
        page_count: { type: ['integer', 'null'] }
        bytes: { type: ['integer', 'null'] }
        sha256: { type: ['string', 'null'] }
        pages:
          type: array
          description: Per-page geometry in points (set at finalize), for editor placement.
          items:
            type: object
            properties:
              width: { type: number }
              height: { type: number }
              rotation: { type: integer }
        acro_fields:
          type: array
          description: >-
            The base PDF's interactive form-field inventory (targets for
            `fill` fields), set at finalize.
          items:
            type: object
            properties:
              name: { type: string }
              kind: { type: string, enum: [text, checkbox, dropdown, option_list, radio_group, other] }
              options: { type: array, items: { type: string } }
              max_len: { type: integer }
        created_at: { type: string, format: date-time }
        created_by: { type: ['string', 'null'] }
        finalized_at: { type: ['string', 'null'] }
        published_at: { type: ['string', 'null'] }
    MergeTag:
      type: object
      description: >-
        Declares one data key a version accepts. Tags referenced by fields
        are derived automatically; declarations refine them (type, required,
        examples) for discovery and validation.
      properties:
        tag: { type: string, pattern: '^[a-zA-Z0-9_.-]{1,64}$' }
        type:
          type: string
          enum: [string, number, date, boolean, image, array]
          default: string
          description: >-
            Open set; tolerate unknown future values. `image` values are
            asset ids (rendered by image fields); `array` values are row
            lists (rendered by table fields).
        required: { type: boolean, default: false }
        format: { type: string }
        example: { type: string }
        description: { type: string }
      required: [tag]
    Field:
      type: object
      description: >-
        One box drawn on a page. Position is absolute: PDF points, top-left
        origin, Y down.
      properties:
        type:
          type: string
          enum: [text, field, checkbox, symbol, fill, image, barcode, table, html]
          description: >-
            `text` draws content (plain or markdown); `field` creates a
            fillable text box; `checkbox` a checkable box; `symbol` one
            glyph (✓ ✗ ● ■ ★ …) centered in the box; `fill` sets the value
            of a form field that already exists in the base PDF; `image`
            draws an uploaded asset; `barcode` draws a QR or Code 128;
            `table` draws ruled line items; `html` renders the content as
            HTML/CSS at exactly the box size and composes it onto the page.
        label:
          type: string
          description: >-
            Name for created `field`/`checkbox` widgets; for `fill`, the name
            of the existing form field to set (see the version's
            `acro_fields`).
        content:
          type: string
          description: >-
            The drawn text (`text`), glyph (`symbol`), value (`fill`), or
            HTML source (`html`, ≤64 KB, sanitized like whole-document
            HTML; `{{merge_tag}}` tokens substitute from `data`,
            HTML-escaped; overflowing content truncates to the box with an
            `HTML_FIELD_TRUNCATED` warning).
        content_format:
          type: string
          enum: [text, markdown]
          default: text
          description: >-
            `markdown` renders `#`/`##`/`###` headings, **bold**, *italic*,
            links (with live link annotations), `-`/`1.` lists, and
            blank-line paragraphs; always word-wraps.
        merge_tag:
          type: string
          description: >-
            Bind this field's value to a data key at generation time:
            `text`/`fill` take their content from the data; `checkbox`
            checks on truthy values; `image` takes an asset id; `table`
            takes an array of rows.
        page:
          type: integer
          minimum: 1
          default: 1
          description: 1-based page number, clamped to the document's page count.
        x: { type: number }
        y: { type: number, description: 'PDF points from the top edge (Y down).' }
        width: { type: number, default: 120 }
        height: { type: number, default: 24 }
        typography: { $ref: '#/components/schemas/Typography' }
        asset_id:
          type: string
          format: uuid
          description: '`image` fields: the asset to draw (or bind via `merge_tag`).'
        fit:
          type: string
          enum: [contain, stretch]
          default: contain
          description: '`image` fields: `contain` preserves aspect ratio, centered in the box.'
        symbology:
          type: string
          enum: [qr, code128]
          description: >-
            `barcode` fields (required): QR (≤500 characters) or Code 128
            (printable ASCII, ≤80 characters). Vector-drawn with standard
            quiet zones; unencodable values warn `BARCODE_INVALID` and skip.
        columns:
          type: array
          maxItems: 10
          description: >-
            `table` fields (required): `[{key, label?, width? (points),
            align?: left|center|right}]`. Unsized columns share the
            remaining width; any label enables the bold header row.
          items:
            type: object
            properties:
              key: { type: string }
              label: { type: string }
              width: { type: number }
              align: { type: string, enum: [left, center, right] }
            required: [key]
        rows:
          type: array
          maxItems: 100
          description: >-
            `table` fields: static rows (flat objects keyed by column key),
            or bind rows via `merge_tag` with an array-typed tag. Cells are
            single-line, ellipsis-truncated.
          items: { type: object }
        row_height:
          type: number
          description: '`table` fields: row height in points (default 1.6 × font size).'
        overflow:
          type: string
          enum: [truncate, error]
          default: truncate
          description: >-
            `table` fields: rows beyond the box either truncate (with a
            `TABLE_ROWS_TRUNCATED` warning) or fail generation
            (`422 TABLE_OVERFLOW`).
      required: [type]
    Typography:
      type: object
      description: >-
        Merged per field: service defaults ← document typography ← field
        typography. Unrecognized values fall back to defaults; they never
        reject.
      properties:
        font_family:
          type: string
          description: >-
            Mapped to the nearest built-in font: helvetica/arial/verdana/
            trebuchet → Helvetica; times/georgia/palatino → Times Roman;
            courier → Courier.
        font_size: { type: number, minimum: 6, maximum: 72, default: 12 }
        font_style: { type: string, enum: [normal, bold, italic, bolditalic], default: normal }
        text_color: { type: string, default: '#000000' }
        line_height: { type: number, minimum: 0.5, maximum: 3, default: 1.2 }
        text_align:
          type: string
          enum: [left, center, right, justify]
          default: left
          description: '`justify` renders as `left`.'
        auto_size:
          type: boolean
          default: true
          description: Shrink text to fit the box.
        word_wrap: { type: boolean, default: false }
        direction:
          type: string
          enum: [ltr, rtl, auto]
          default: ltr
          description: >-
            Paragraph base direction (requires `unicode: true`): `rtl` and
            `auto` reorder mixed-direction text per the Unicode
            Bidirectional Algorithm; `rtl` defaults alignment to right.
    PageSpec:
      type: object
      description: Page geometry for documents generated without a base PDF.
      properties:
        size:
          description: '`letter` (default), `legal`, `a4`, or `{width, height}` in points (72–7200).'
          oneOf:
            - type: string
              enum: [letter, legal, a4]
            - type: object
              properties:
                width: { type: number, minimum: 72, maximum: 7200 }
                height: { type: number, minimum: 72, maximum: 7200 }
              required: [width, height]
        orientation: { type: string, enum: [portrait, landscape], default: portrait }
        count:
          type: integer
          minimum: 1
          maximum: 100
          default: 1
          description: Blank-page mode only; rejected with `html` (the content sets the page count).
    HtmlSource:
      type: object
      description: >-
        An HTML/CSS document body rendered with real browser layout and
        pagination. Sanitized to a safe allow-list; images must be `data:`
        URIs; external fetches never happen.
      properties:
        content:
          type: string
          description: >-
            The document HTML (≤1 MB). A fragment is wrapped in a page shell
            using the built-in Noto Sans fonts.
        header:
          type: string
          description: >-
            Print header template (≤10 KB). Elements with class
            `pageNumber`, `totalPages`, `date`, or `title` are substituted.
        footer: { type: string, description: 'Print footer template (≤10 KB), same substitutions.' }
        margins:
          type: object
          description: Page margins in points; default 36 on every side.
          properties:
            top: { type: number, minimum: 0, maximum: 288, default: 36 }
            right: { type: number, minimum: 0, maximum: 288, default: 36 }
            bottom: { type: number, minimum: 0, maximum: 288, default: 36 }
            left: { type: number, minimum: 0, maximum: 288, default: 36 }
          additionalProperties: false
        tagged:
          type: boolean
          default: false
          description: >-
            Emit an accessible tagged PDF (Section 508). Cannot be combined
            with `fields`, `options`, `typography`, `flatten`, or `unicode`.
        print_background: { type: boolean, default: true }
      required: [content]
      additionalProperties: false
    DocumentOptions:
      type: object
      description: >-
        Document-wide options. Stored on a version and overridable per
        request; a request section replaces the stored section wholesale.
        Watermarks and page numbers draw above field content; to place them
        above interactive form fields too, also set `flatten`.
      properties:
        metadata:
          type: object
          description: PDF document properties.
          properties:
            title: { type: string }
            author: { type: string }
            subject: { type: string }
            keywords: { type: array, items: { type: string } }
            language: { type: string, description: 'Document language, e.g. en-US (accessibility).' }
        viewer:
          type: object
          properties:
            print_scaling:
              type: string
              enum: [none, app_default]
              description: '`none` asks viewers to print at true size (court forms).'
            duplex: { type: string, enum: [simplex, duplex_flip_long_edge, duplex_flip_short_edge] }
        watermark:
          type: object
          description: A diagonal text stamp (DRAFT, COPY, VOID…) on every page.
          properties:
            text: { type: string, maxLength: 100 }
            opacity: { type: number, minimum: 0.05, maximum: 0.5, default: 0.15 }
            angle: { type: number, default: 45 }
            color: { type: string, default: '#999999' }
          required: [text]
        page_numbers:
          type: object
          properties:
            format: { type: string, default: 'Page {page} of {pages}' }
            position: { type: string, enum: [bottom_left, bottom_center, bottom_right], default: bottom_center }
            size: { type: number, minimum: 6, maximum: 14, default: 9 }
            start_at: { type: integer, default: 1 }
            color: { type: string, default: '#555555' }
      additionalProperties: false
    UploadInstructions:
      type: object
      description: >-
        One-time S3 upload for a version's base PDF: HTTP POST the file to
        `url` as multipart form data with every entry of `fields` included,
        then call finalize.
      properties:
        url: { type: string }
        fields:
          type: object
          description: Form fields that must accompany the upload, exactly as given.
        key: { type: string }
        expires_in: { type: integer, description: Seconds until the upload window closes. }
        note: { type: string }
    OutputFile:
      type: object
      description: One generated document with a signed download URL.
      properties:
        output_id: { type: string, format: uuid }
        url: { type: string }
        name: { type: string }
        format: { type: string }
        content_type: { type: string }
        bytes: { type: integer }
        sha256: { type: string }
        page_count: { type: ['integer', 'null'] }
        expires_in: { type: integer, description: URL validity in seconds. }
    GenerateResult:
      type: object
      description: JSON-mode generation result.
      properties:
        files:
          type: array
          items: { $ref: '#/components/schemas/OutputFile' }
        template_id: { type: ['string', 'null'] }
        version: { type: ['integer', 'null'] }
        warnings:
          type: array
          items: { $ref: '#/components/schemas/Warning' }
      required: [files]
    DryRunResult:
      type: object
      description: '`dry_run` validation result: nothing was rendered.'
      properties:
        valid: { type: boolean }
        template_id: { type: string, format: uuid }
        version: { type: integer }
        warnings:
          type: array
          items: { $ref: '#/components/schemas/Warning' }
      required: [valid]
    OutputRecord:
      type: object
      description: A generated output with a freshly signed URL.
      properties:
        output_id: { type: string, format: uuid }
        url: { type: string }
        name: { type: string }
        format: { type: string }
        content_type: { type: string }
        bytes: { type: integer }
        sha256: { type: string }
        page_count: { type: ['integer', 'null'] }
        template_id: { type: ['string', 'null'] }
        version: { type: ['integer', 'null'] }
        created_at: { type: string, format: date-time }
        expires_in: { type: integer }
    Job:
      type: object
      properties:
        job_id: { type: string, format: uuid }
        status:
          type: string
          enum: [queued, running, completed, completed_with_errors, cancelled]
        template_id: { type: string, format: uuid }
        version: { type: integer }
        total: { type: integer }
        pending: { type: integer }
        succeeded: { type: integer }
        failed: { type: integer }
        created_at: { type: string, format: date-time }
        items:
          type: array
          items: { $ref: '#/components/schemas/JobItem' }
        files:
          type: array
          description: Signed download URLs for every finished item (re-signed on each read).
          items: { $ref: '#/components/schemas/JobFile' }
        next_cursor: { type: ['string', 'null'] }
    JobItem:
      type: object
      properties:
        index: { type: integer }
        status: { type: string, enum: [queued, running, succeeded, failed, cancelled] }
        output_id: { type: string, format: uuid }
        error: { type: string }
      required: [index, status]
    JobFile:
      type: object
      properties:
        index: { type: integer }
        output_id: { type: string, format: uuid }
        url: { type: string }
        name: { type: string }
        bytes: { type: integer }
        sha256: { type: string }
        expires_in: { type: integer }
    Asset:
      type: object
      properties:
        asset_id: { type: string, format: uuid }
        name: { type: string }
        content_type: { type: string, enum: [image/png, image/jpeg] }
        bytes: { type: integer }
        sha256: { type: string }
        width: { type: ['integer', 'null'] }
        height: { type: ['integer', 'null'] }
        created_at: { type: string, format: date-time }
        created_by: { type: ['string', 'null'] }
    AuditRecord:
      type: object
      properties:
        action: { type: string, description: 'e.g. template.created, template.published, document.generated' }
        resource: { type: string }
        actor: { type: ['string', 'null'], description: The X-On-Behalf-Of value recorded with the action. }
        api_key_id: { type: string }
        at: { type: string, format: date-time }
        details: { type: object }
