Skip to content

Many documents, one file

Statements, payslips, certificates, per-customer invoices: the same form, once per person. Compose PDF renders those in a single call and returns a single file, with each record laid out as its own document.

There are two ways to say how many documents you want, and which one to use depends on who owns the list.

Send dataList — an array of payloads — instead of data. Each entry is validated and rendered as its own document, and the documents are collated in order:

Terminal window
curl -X POST https://composepdf.com/v1/canvases/{canvasId}/pdf \
-H "x-api-key: $COMPOSEPDF_API_KEY" \
-H "content-type: application/json" \
-d '{ "dataList": [
{ "customer": { "name": "Acme Inc." }, "items": [ … ] },
{ "customer": { "name": "Brightline LLC" }, "items": [ … ] }
] }' \
--output statements.pdf

A contract violation names the record it came from, so a bad row in position four reads dataList[3].customer.name rather than “invalid request”.

Sometimes the payload is naturally one object with a list inside it, and each entry of that list should become a document. Say so in the studio: under Documents to print, choose One per row of your list instead of One, from all of this data.

Now a single data object fans out. Each document sees its own row plus everything outside the list — a period, a company address, a notice that appears on all of them. Where a name appears in both, the row wins.

This choice belongs to the template and is frozen when you publish, which is what keeps the studio preview, the browser export and the API in agreement about how many documents a payload produces.

One PDF. Each record is paginated independently, so page numbers are per-document — the second invoice starts again at page 1, and 3 / 4 means the third page of that invoice. The response reports how many records were rendered in x-rspdf-records; the total page count is recorded server-side and read back as pages from GET /v1/renders/{renderId}.

  • Up to 100 records per request with dataList. Beyond that the request is refused rather than run — split it.
  • data and dataList are mutually exclusive.
  • An empty list is refused. A zero-page PDF is not a useful answer, so an empty array is an error rather than an empty file.
  • Long jobs should be asynchronous. Synchronous requests are bounded by a render budget of roughly 25 seconds; add Prefer: respond-async to get a job id instead, and download the result when it is ready. See Render API.
  • Encryption is not available on async jobs, because the password would have to be stored while the job waits.