Render API
Base URL: https://composepdf.com
OpenAPI specification
Section titled “OpenAPI specification”The endpoints on this page are published as an OpenAPI 3.1 document at
https://composepdf.com/v1/openapi.json — readable without a key. Point a
generator at it for a typed client, load it into an API tool, or hand it to an
agent that needs to call the API without reading prose.
The specification describes the envelope: paths, authentication, headers and
errors. What a particular template expects in data is the separate,
per-template data contract.
Authentication
Section titled “Authentication”Send your API key on every request, either way:
-H "x-api-key: $COMPOSEPDF_API_KEY" # primary-H "Authorization: Bearer $COMPOSEPDF_API_KEY" # also acceptedKeys are issued under Settings → API keys and can be scoped to a
workspace. Unknown keys, revoked keys and out-of-scope resources all answer
404 — the API does not confirm what exists outside your scope.
Endpoints
Section titled “Endpoints”| Method | Path | Purpose |
|---|---|---|
GET |
/v1/openapi.json |
This surface as an OpenAPI 3.1 document (no key) |
GET |
/v1/health |
Liveness probe |
GET |
/v1/canvases |
List published canvases in the key’s scope |
POST |
/v1/canvases/{canvasId}/pdf |
Render the published version to PDF |
GET |
/v1/canvases/{canvasId}/schema |
The template’s data contract |
GET |
/v1/renders |
List recent renders |
GET |
/v1/renders/{renderId} |
Status of a render (async jobs included) |
GET |
/v1/renders/{renderId}/pdf |
Download a stored render |
GET |
/v1/usage |
Current-period usage for the key’s organization |
Rendering
Section titled “Rendering”POST /v1/canvases/{canvasId}/pdfcontent-type: application/json
{ "data": { … } }The published version is rendered with data and the response body is the
PDF. Rendering always uses the frozen published version — drafts are never
reachable through the API.
Request body
Section titled “Request body”| Field | Type | Notes |
|---|---|---|
data |
object | The payload. Validated against the template’s declared variables before layout. |
dataList |
array | Mutually exclusive with data — see below. |
options.store |
boolean | Keep the PDF so it can be downloaded later via GET /v1/renders/{id}/pdf. |
options.password |
string | Encrypt the output (only for templates that declare an encryption policy). The password is never stored. |
options.createdAt |
string | The document’s creation date, ISO 8601 in UTC ("2026-08-13T09:30:00Z"). Omitted, files are dated 2000-01-01T00:00:00Z — the engine never reads a clock, so the same input gives the same bytes. |
Many records, one file
Section titled “Many records, one file”Two ways to produce a collated, multi-document PDF:
dataList— an array of payloads (up to 100 per request). Each entry is validated and rendered as its own document; the documents are collated into one file. Contract violations name the record:dataList[3].customer.name.- Record templates — a template that declares a record source (for
example one invoice per entry of
customers) decides the count itself:POSTa singledataobject and the template fans out. The record count is reported in thex-rspdf-recordsresponse header.
Page counts are recorded per render and reported as pages by
GET /v1/renders/{renderId}. The monthly quota (GET /v1/usage) counts
records, not pages — a 100-record dataList spends 100.
Asynchronous rendering
Section titled “Asynchronous rendering”Synchronous requests are bounded by a ~25 second render budget. For heavy jobs, request async processing:
POST /v1/canvases/{canvasId}/pdfPrefer: respond-asyncYou get 202 Accepted with a job id. Poll GET /v1/renders/{id} until the
status is terminal, then download from GET /v1/renders/{id}/pdf.
- Send an
Idempotency-Keyheader to make retries safe — a replayed key whose bytes already exist answers200immediately. - The key alone decides the replay; your
datais not part of it. Present a key whose bytes exist, for the same canvas and with the same API key, and you get those bytes back (X-Rspdf-Replayed: 1) — even if you changed the payload.409is reserved for a key reused across a different canvas or a different API key. So reprinting a document with corrected values needs a key that changes with it: leave the header off, or derive it from something that is new each time you print. Keys fit calls you must not pay for twice (one retry of one issued record), not calls you will run again with new values. options.passwordis rejected (400) on async jobs: encryption secrets are never stored, and an async job would have to store them.
Errors
Section titled “Errors”| Status | Meaning |
|---|---|
400 |
Payload violates the data contract. The body lists each violating path. |
402 |
The organization’s plan does not allow this call (for example, the free plan has no API access). |
404 |
Unknown canvas, no published version, or out of the key’s scope. |
413 |
Request too large for the plan’s per-minute budget, dataList beyond 100 records, or output beyond the size cap — retrying will not help. |
429 |
Rate limited. Honor Retry-After. |
Rate and concurrency limits are per organization and scale with the plan.
Actual consumption is visible under Settings → Usage and via GET /v1/usage.