---
title: "Render pages as ONE multi-page file: PDF or self-contained HTML"
method: POST
path: "/render/document"
---

# Render pages as ONE multi-page file: PDF or self-contained HTML

`POST /render/document`

## Request

- Base URL: `https://rendry.io/api/v1`
- URL: `https://rendry.io/api/v1/render/document`
- Auth: one of:
  - HTTP bearer
  - HTTP bearer
  - HTTP bearer
  - HTTP bearer

ASYNC. Pages = ordered presets (inline pages[] or a saved document_id). 2 credits include the first 20 final pages; add 1 credit for each additional begun 20-page block, plus the existing full-raster compute surcharge above the standard-page floor. The quote uses the final prepared page count. Charged once up front; failed jobs refund automatically. Up to 20 authored pages and 200 final pages, including table continuations. Account pending document limits refuse with 429 document_queue_full before charge. The caller decides how content splits into pages. Text stays searchable vector; layers outside the document profile (shadows, blurs, blend modes) are rasterized and reported in the job warnings.

## Headers

- `X-Rendry-Account` string

## Request body

- DocumentRequest
  - `document_id` string: Saved template from GET /documents. Either this or pages
  - `pages` DocumentPage[] (unresolved $ref)
  - `dynamic_data` object: Shared substitutions for pages without their own
  - `rows` object: Rows for every page's top-level tables bound to rows_key. A page's own rows win; an absent key retains sample rows, while an explicit empty array clears them. With row_height > 0, body rows paginate in order and the header repeats. Row/cell limits apply to each final page for supplied document rows. At most 20 authored pages and 200 final pages; dry_run quotes the exact row_ranges and cost before submission.
  - `dry_run` boolean: Count, do not draw: page count, price and the layers that will be rasterized. No job, no file, no credits spent
  - `format` 'pdf' | 'html' | 'html_embed': pdf (default) | html — one self-contained HTML file, pages stacked with print page breaks. html_embed is the same format under its older name
  - `webhook_url` string: POST-ed {job_id,status,result_url} once the document is done; the outcome of that call is reported as webhook on GET /render/status/{id}. Must be an http or https address on the public internet: an unparseable address, another scheme, an internal host name or a literal private, loopback or link-local IP is refused with 400 invalid_webhook_url before anything is priced, queued or charged.
  - `compliance` PDFCompliance: Opt-in independently validated PDF product. pdfa3b uses existing static pages. facturx requires the controlled structured invoice and cannot be combined with pages/document_id/master/dynamic_data/rows. Forms, flatten_form, annotations and hyperlinks are refused before charge. A 48 MiB final-file cap and compliance memory reserve apply. dry_run validates admission and price only; validation receipts appear only on completed status.
    - `mode` 'pdfa3b' | 'facturx', required
    - `version` '1.09.2': Required for facturx, forbidden for pdfa3b.
    - `profile` 'EN16931': Required for facturx, forbidden for pdfa3b; no profile downgrade.
    - `invoice` FacturXInvoice: Positive ordinary EUR sales invoice (type 380), C62 integer units and SEPA payment only. Cent arithmetic; half-up VAT per group, at most eight rates. No allowances, charges, exemptions, credit notes, custom layouts or arbitrary attachments. Text must be bounded UTF-8 without controls/template braces. Due date is on/after issue date. Shared table pagination/resource admission determines final page count and price.
      - `number` string, required
      - `currency` 'EUR', required
      - `seller` FacturXParty, required
        - `name` string, required
        - `street` string, required
        - `postcode` string, required
        - `city` string, required
        - `country` string, required
        - `vat_id` string, required
      - `buyer` FacturXParty, required
        - `name` string, required
        - `street` string, required
        - `postcode` string, required
        - `city` string, required
        - `country` string, required
        - `vat_id` string, required
      - `iban` string, required: Valid uppercase IBAN without spaces.
      - `lines` FacturXLine[], required
        - `description` string, required
        - `quantity` integer, required
        - `unit_price_cents` integer, required
        - `vat_rate_basis_points` integer, required
      - `issue_date` string, date, required
      - `delivery_date` string, date, required
      - `due_date` string, date, required
  - `rows_refs` RowsReferences: Request-only UTF-8 source names, 1..128 bytes. Across all scopes at most 40 declarations; all declarations including overridden validate before reads. Effective visible table sources fetch once per URL+format under SafeHTTP, 4194304 aggregate bytes and 25 seconds. JSON array objects must have only string values; CSV preserves exact quoted newlines and strings. At most 8000 rows/source and 200 fields/row. Errors never echo URLs or data. Stdio has a separate local path normalization budget; HTTP paths refuse.
  - `print` PDFPrint: Selected static CMYK document PDF using the named APTEC coated-offset ICC profile. Omission preserves ordinary RGB output. Opaque canvas required; no HTML, PDF/A/Factur-X or interactive forms. Raster/alpha/effect pages are composited then separated and warn about rasterization/loss of searchable text. Supported square/circle/line/ink/underline/strikeout appearances are server-painted; other annotations refuse before charge.
    - `mode` 'cmyk', required
    - `profile` 'aptec-offset-coated-linearctv-2025-v1'

## Response `200`

dry_run: true — the quote. status is dry_run; cost, page_count, warnings and losses say what the render would charge and what would be rasterized. No job, no file, nothing charged.

- AsyncRenderResponse
  - `format` string: Document output format.
  - `job_id` string
  - `status` string
  - `status_url` string
  - `message` string
  - `pages_used` string[]: preset_id of every page queued — determinism between calls.
  - `pages` integer: Output page count for document quotes and submissions.
  - `cost_credits` number: Displayed credits for the document. A dry run does not charge.
  - `credits_charged` number: What this call actually took, in credits, at the moment it was queued. Distinct from cost_credits, which a dry run reports for work that is NOT charged.
  - `warnings` string[]: dry_run: the same warnings the finished job would carry.
  - `losses` Loss[]: dry_run, machine-readable: every layer that will be rasterized.
    - `page` integer
    - `layer_id` string
    - `layer_type` string
    - `reasons` string[]: Why it goes raster: rotation, shadow, mask, plate_shape, image_fill and so on.
    - `whole_page` boolean: The whole page was rasterized (blend_mode/backdrop_blur) — single layers are not listed.
    - `loses_text` boolean: This fragment contained text and stopped being text.
  - `text_searchable` boolean: dry_run: true when every page's text comes out of the file as the words it was typed as, false when a line stays text and stops being WORDS (letter_spacing past size/20). ABSENT when the document carries no text at all, so a client promises nothing about a sheet of scans.
  - `error` string: On a refusal: the machine code (see Refusal); `message` is its sentence. On 402 it is the credit failure: see balance, required, packages_url.
  - `balance` number: 402 only: credits the account has.
  - `required` number: 402 only: credits this render needs.
  - `packages_url` string: 402 only: where to top up.
  - `diagnostics` VariableDiagnostic[]: On variables_unresolved: why the template's variables do not resolve for this call.
    - `code` 'invalid_name' | 'invalid_mode' | 'invalid_format' | 'invalid_format_value' | 'reserved_name' | 'unsupported_row_scope' | 'unsupported_reference_scope' | 'template_syntax' | 'formula_syntax' | 'unknown_reference' | 'cycle' | 'nonnumeric_operand' | 'division_by_zero' | 'numeric_limit' | 'source_limit' | 'node_limit' | 'depth_limit' | 'output_limit' | 'circular_layout_dependency', required: The stable codes of variable diagnostics (specs/007-nested-variables/contracts/language.md). A diagnostic names the variable, the code, a reason sub-code, the referenced name, a cycle path and a code-point span in the source — never a source string, a supplied value or a result. A schema problem refuses the save (invalid_name, invalid_mode, reserved_name, unsupported_row_scope, source_limit) and nothing is written; a definition that does not resolve from the saved values is saved and named in the save's warnings; at render every unresolvable definition refuses the request before any charge (400, nothing rendered, nothing charged) with the same codes.
    - `severity` 'error' | 'warning', required
    - `variable` string: The definition or supplied input the problem belongs to; empty for a context-wide limit.
    - `reference` string: The referenced name (unknown_reference, nonnumeric_operand).
    - `path` string[]: A cycle, its first name repeated at the end.
    - `reason` string: A stable sub-code: which syntax rule or which limit.
    - `span` object: Half-open range of Unicode code points in the definition's source.
      - `start` integer
      - `end` integer
  - `row_ranges` DocumentRowRange[]: Exact body-row placement from the prepared document: 1-based inclusive row ranges per final sheet, excluding the repeated header. Available in dry runs and submission replies. Empty tables use 0/0.
    - `authored_page` integer, required
    - `final_page` integer, required
    - `layer_id` string, required
    - `rows_key` string
    - `from_row` integer, required
    - `to_row` integer, required
  - `print` PDFPrint: Selected static CMYK document PDF using the named APTEC coated-offset ICC profile. Omission preserves ordinary RGB output. Opaque canvas required; no HTML, PDF/A/Factur-X or interactive forms. Raster/alpha/effect pages are composited then separated and warn about rasterization/loss of searchable text. Supported square/circle/line/ink/underline/strikeout appearances are server-painted; other annotations refuse before charge.
    - `mode` 'cmyk', required
    - `profile` 'aptec-offset-coated-linearctv-2025-v1'
  - `text_flows` TextFlowReceipt[]
    - `authored_page` integer, required
    - `layer_id` string, required
    - `final_page` integer, required
    - `source_from` integer, required
    - `source_to` integer, required
    - `box` TextFlowBox, required
      - `x` integer, required
      - `y` integer, required
      - `width` integer, required
      - `height` integer, required
    - `text` string, required
    - `wraps` integer[], required

## Other responses

- `202`: Accepted
- `400`: Validation error; nothing was charged. `variables_unresolved` names the `page` and carries `diagnostics`; a webhook_url the worker would refuse to call answers `error` `invalid_webhook_url` with `reason` beside it.
- `401`: Missing/invalid API key
- `402`: Insufficient credits
- `403`: `error` is `api_key_scope_insufficient` (a read-only key) or `api_key_preset_not_allowed` (a preset outside the key's allow-list; `preset_id` and `tool` ride beside it)
- `404`: `document_not_found`, or `preset_not_found` for a page's preset (with `page` and `preset_id`).
- `422`: Page over canvas limits or unsupported format

---

[API](https://skmtc.dev/rendry/apis/rendry-api.md) · [All operations](https://skmtc.dev/rendry/apis/rendry-api/llms.txt) · [OpenAPI document](https://skmtc.dev/rendry/apis/rendry-api/revisions/c4fb572c26c9?raw)
