Skip to content

Render API

Base URL: https://composepdf.com

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.

Send your API key on every request, either way:

Terminal window
-H "x-api-key: $COMPOSEPDF_API_KEY" # primary
-H "Authorization: Bearer $COMPOSEPDF_API_KEY" # also accepted

Keys 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.

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
Terminal window
POST /v1/canvases/{canvasId}/pdf
content-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.

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.

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: POST a single data object and the template fans out. The record count is reported in the x-rspdf-records response 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.

Synchronous requests are bounded by a ~25 second render budget. For heavy jobs, request async processing:

Terminal window
POST /v1/canvases/{canvasId}/pdf
Prefer: respond-async

You 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-Key header to make retries safe — a replayed key whose bytes already exist answers 200 immediately.
  • The key alone decides the replay; your data is 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. 409 is 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.password is rejected (400) on async jobs: encryption secrets are never stored, and an async job would have to store them.
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.