Render many documents at once
A batch creates up to 100 PDFs with one request, such as a month’s invoices or every ticket of a booking. You can download each document on its own, or all of them as one combined PDF.
Each item is an ordinary Renders API render with its own ID, state, PDF, webhook events and retention. The batch is created whole or not at all: if any item is invalid, nothing is created. The Reports API has no batches.
Before you start
Section titled “Before you start”- An API key with
render:createandrender:read, and curl or a client library set up as in the quickstart. - The version ID of a published template; see render a specific template version.
1. Create the batch
Section titled “1. Create the batch”Send the renders as items to POST /v1/renders/batch. An item can be anything a single
POST /v1/renders request takes (a template version, your own HTML or a
preview), except waitMs, and one batch can mix them. Each item can name its own
storage destination:
curl -X POST "$PDF_URL/v1/renders/batch" \ -H "x-api-key: $API_KEY" \ -H "content-type: application/json" \ -H "Idempotency-Key: invoices-2026-09" \ -d '{ "items": [ { "templateVersionId": "c7e0b9a4-1f2d-4c3b-9a8e-5d6f7a8b9c0d", "data": { "invoice": { "number": "INV-2041" } } }, { "templateVersionId": "c7e0b9a4-1f2d-4c3b-9a8e-5d6f7a8b9c0d", "data": { "invoice": { "number": "INV-2042" } } } ] }'using Typefold.Client;
var client = new TypefoldClient( new Uri(Environment.GetEnvironmentVariable("PDF_URL")!), Environment.GetEnvironmentVariable("API_KEY")!);
RenderBatch batch = await client.CreateBatchAsync( new[] { new TemplateVersionRenderRequest { TemplateVersionId = "c7e0b9a4-1f2d-4c3b-9a8e-5d6f7a8b9c0d", Data = new { invoice = new { number = "INV-2041" } }, }, new TemplateVersionRenderRequest { TemplateVersionId = "c7e0b9a4-1f2d-4c3b-9a8e-5d6f7a8b9c0d", Data = new { invoice = new { number = "INV-2042" } }, }, }, idempotencyKey: "invoices-2026-09");
Console.WriteLine(batch.Id);import { TypefoldClient } from "@typefold/client";
const client = new TypefoldClient({ baseUrl: process.env.PDF_URL!, apiKey: process.env.API_KEY!,});
const batch = await client.createBatch( [ { templateVersionId: "c7e0b9a4-1f2d-4c3b-9a8e-5d6f7a8b9c0d", data: { invoice: { number: "INV-2041" } }, }, { templateVersionId: "c7e0b9a4-1f2d-4c3b-9a8e-5d6f7a8b9c0d", data: { invoice: { number: "INV-2042" } }, }, ], { idempotencyKey: "invoices-2026-09" },);
console.log(batch.id);import { TypefoldClient } from "@typefold/client";
const client = new TypefoldClient({ baseUrl: process.env.PDF_URL, apiKey: process.env.API_KEY,});
const batch = await client.createBatch( [ { templateVersionId: "c7e0b9a4-1f2d-4c3b-9a8e-5d6f7a8b9c0d", data: { invoice: { number: "INV-2041" } }, }, { templateVersionId: "c7e0b9a4-1f2d-4c3b-9a8e-5d6f7a8b9c0d", data: { invoice: { number: "INV-2042" } }, }, ], { idempotencyKey: "invoices-2026-09" },);
console.log(batch.id);import os
from typefold import TypefoldClient, TemplateVersionRenderRequest
client = TypefoldClient(os.environ["PDF_URL"], os.environ["API_KEY"])
batch = client.create_batch( [ TemplateVersionRenderRequest( template_version_id="c7e0b9a4-1f2d-4c3b-9a8e-5d6f7a8b9c0d", data={"invoice": {"number": "INV-2041"}}, ), TemplateVersionRenderRequest( template_version_id="c7e0b9a4-1f2d-4c3b-9a8e-5d6f7a8b9c0d", data={"invoice": {"number": "INV-2042"}}, ), ], idempotency_key="invoices-2026-09",)
print(batch.id)The request answers 202 at once with the batch summary; it never waits for the PDFs. Keep its
id:
{ "id": "4d3c2b1a-0f9e-4d8c-b7a6-5f4e3d2c1b0a", "createdAt": "2026-10-08T09:30:00.120Z", "total": 2, "counts": { "queued": 2, "processing": 0, "succeeded": 0, "failed": 0, "cancelled": 0 }, "items": [ { "index": 0, "id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "state": "queued", "templateVersionId": "c7e0b9a4-1f2d-4c3b-9a8e-5d6f7a8b9c0d", "test": false, "createdAt": "2026-10-08T09:30:00.120Z", "updatedAt": "2026-10-08T09:30:00.120Z" }, { "index": 1, "id": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e", "state": "queued", "templateVersionId": "c7e0b9a4-1f2d-4c3b-9a8e-5d6f7a8b9c0d", "test": false, "createdAt": "2026-10-08T09:30:00.120Z", "updatedAt": "2026-10-08T09:30:00.120Z" } ]}itemsare in the order you sent them.indexis the position in your request, from 0, andidis that item’s render ID.templateVersionIdappears only on template version items.- The summary has no download links or error details. Read the item’s render for those.
totalcounts the items still kept. Items deleted by retention leave the batch.- All fields:
createRenderBatchin the API reference.
The Idempotency-Key makes it safe to send the batch again after a network error: the same key
and body return the first batch’s current summary instead of a second batch. Batch keys are
separate from single-render keys; the rest of the rules are under
idempotency keys.
2. Wait for the items to finish
Section titled “2. Wait for the items to finish”If you use webhooks, each item sends its own event when it ends; there is no event for the batch as a whole. Match the event’s render ID to the item IDs from step 1.
Otherwise, read the summary every few seconds until counts.queued and counts.processing are
both 0. The clients’ wait method does this for you. It gives up with RENDER_TIMEOUT after 10
minutes unless you pass a longer timeout:
curl "$PDF_URL/v1/renders/batch/4d3c2b1a-0f9e-4d8c-b7a6-5f4e3d2c1b0a" \ -H "x-api-key: $API_KEY"using Typefold.Client;
var client = new TypefoldClient( new Uri(Environment.GetEnvironmentVariable("PDF_URL")!), Environment.GetEnvironmentVariable("API_KEY")!);
RenderBatch batch = await client.GetBatchAsync("4d3c2b1a-0f9e-4d8c-b7a6-5f4e3d2c1b0a");batch = await client.WaitForBatchAsync(batch);
Console.WriteLine($"{batch.Counts.Succeeded} succeeded, {batch.Counts.Failed} failed");import { TypefoldClient } from "@typefold/client";
const client = new TypefoldClient({ baseUrl: process.env.PDF_URL!, apiKey: process.env.API_KEY!,});
let batch = await client.getBatch("4d3c2b1a-0f9e-4d8c-b7a6-5f4e3d2c1b0a");batch = await client.waitForBatch(batch);
console.log(`${batch.counts.succeeded} succeeded, ${batch.counts.failed} failed`);import { TypefoldClient } from "@typefold/client";
const client = new TypefoldClient({ baseUrl: process.env.PDF_URL, apiKey: process.env.API_KEY,});
let batch = await client.getBatch("4d3c2b1a-0f9e-4d8c-b7a6-5f4e3d2c1b0a");batch = await client.waitForBatch(batch);
console.log(`${batch.counts.succeeded} succeeded, ${batch.counts.failed} failed`);import os
from typefold import TypefoldClient
client = TypefoldClient(os.environ["PDF_URL"], os.environ["API_KEY"])
batch = client.get_batch("4d3c2b1a-0f9e-4d8c-b7a6-5f4e3d2c1b0a")batch = client.wait_for_batch(batch)
print(f"{batch.counts.succeeded} succeeded, {batch.counts.failed} failed")One failed item does not stop the others. A limit hit while rendering, such as the page count, fails only that item.
3. Download each document
Section titled “3. Download each document”Each item is a single render, so download the ones that succeeded by their render ID. To see why an item failed, read its render the same way:
Read each item by its render id and download it as a single render; see
download the PDF.
using Typefold.Client;
var client = new TypefoldClient( new Uri(Environment.GetEnvironmentVariable("PDF_URL")!), Environment.GetEnvironmentVariable("API_KEY")!);
RenderBatch batch = await client.GetBatchAsync("4d3c2b1a-0f9e-4d8c-b7a6-5f4e3d2c1b0a");
foreach (var item in batch.Items.Where(item => item.State == RenderState.Succeeded)){ Render render = await client.GetRenderAsync(item.Id); await File.WriteAllBytesAsync($"invoice-{item.Index}.pdf", await client.DownloadAsync(render));}import { writeFile } from "node:fs/promises";import { TypefoldClient, RenderState } from "@typefold/client";
const client = new TypefoldClient({ baseUrl: process.env.PDF_URL!, apiKey: process.env.API_KEY!,});
const batch = await client.getBatch("4d3c2b1a-0f9e-4d8c-b7a6-5f4e3d2c1b0a");
for (const item of batch.items) { if (item.state === RenderState.Succeeded) { const render = await client.getRender(item.id); await writeFile(`invoice-${item.index}.pdf`, await client.download(render)); }}import { writeFile } from "node:fs/promises";import { TypefoldClient, RenderState } from "@typefold/client";
const client = new TypefoldClient({ baseUrl: process.env.PDF_URL, apiKey: process.env.API_KEY,});
const batch = await client.getBatch("4d3c2b1a-0f9e-4d8c-b7a6-5f4e3d2c1b0a");
for (const item of batch.items) { if (item.state === RenderState.Succeeded) { const render = await client.getRender(item.id); await writeFile(`invoice-${item.index}.pdf`, await client.download(render)); }}import osfrom pathlib import Path
from typefold import TypefoldClient, RenderState
client = TypefoldClient(os.environ["PDF_URL"], os.environ["API_KEY"])
batch = client.get_batch("4d3c2b1a-0f9e-4d8c-b7a6-5f4e3d2c1b0a")
for item in batch.items: if item.state == RenderState.SUCCEEDED: render = client.get_render(item.id) Path(f"invoice-{item.index}.pdf").write_bytes(client.download(render))A batch has no cancel call of its own. To cancel an item, cancel its render by ID; see cancel a render.
Download one combined PDF
Section titled “Download one combined PDF”To give someone every document in one file, for example all the tickets of a booking, download the combined PDF once every item has succeeded:
curl "$PDF_URL/v1/renders/batch/4d3c2b1a-0f9e-4d8c-b7a6-5f4e3d2c1b0a/pdf" \ -H "x-api-key: $API_KEY" \ -o booking.pdfusing Typefold.Client;
var client = new TypefoldClient( new Uri(Environment.GetEnvironmentVariable("PDF_URL")!), Environment.GetEnvironmentVariable("API_KEY")!);
byte[] pdf = await client.DownloadBatchAsync("4d3c2b1a-0f9e-4d8c-b7a6-5f4e3d2c1b0a");
await File.WriteAllBytesAsync("booking.pdf", pdf);import { writeFile } from "node:fs/promises";import { TypefoldClient } from "@typefold/client";
const client = new TypefoldClient({ baseUrl: process.env.PDF_URL!, apiKey: process.env.API_KEY!,});
const pdf = await client.downloadBatch("4d3c2b1a-0f9e-4d8c-b7a6-5f4e3d2c1b0a");
await writeFile("booking.pdf", pdf);import { writeFile } from "node:fs/promises";import { TypefoldClient } from "@typefold/client";
const client = new TypefoldClient({ baseUrl: process.env.PDF_URL, apiKey: process.env.API_KEY,});
const pdf = await client.downloadBatch("4d3c2b1a-0f9e-4d8c-b7a6-5f4e3d2c1b0a");
await writeFile("booking.pdf", pdf);import osfrom pathlib import Path
from typefold import TypefoldClient
client = TypefoldClient(os.environ["PDF_URL"], os.environ["API_KEY"])
pdf = client.download_batch("4d3c2b1a-0f9e-4d8c-b7a6-5f4e3d2c1b0a")
Path("booking.pdf").write_bytes(pdf)Pages come in item order, and each page keeps its own paper size, fonts, images and links. The file’s title, author, subject, keywords and creator come from the first item. Bookmarks and files attached to items are left out.
The file is put together on each request and not stored, so download it while the items are still kept (see retention). When it can’t be made, the error says why:
| Situation | Answer |
|---|---|
| An item is still queued or processing | 409 CONFLICT, retryable: true. Ask again later. |
| An item failed or was cancelled | 409 CONFLICT, retryable: false, naming the first one. |
| An item is an image | 400 BAD_REQUEST, naming the first one. |
| An item is PDF/A or PDF/UA | 400 UNSUPPORTED_FEATURE. Only plain PDFs can be joined. |
| An item has a password or is signed | 400 UNSUPPORTED_FEATURE. Only plain PDFs can be joined. |
| An item kept no copy | 409 CONFLICT, naming the first one. Its PDF is only in your bucket. |
| The items’ PDFs are too large together | 400 LIMIT_EXCEEDED. Split into smaller batches. |
If the batch is refused
Section titled “If the batch is refused”Nothing was created. Fix the cause, then send the batch again:
- The message starts with
Batch item 7:: item 7 (counting from 0) can’t be used, for example because its template version doesn’t exist. Fix that item. 429 RATE_LIMITED: the batch doesn’t fit your workspace’s render allowances right now. Wait the seconds inRetry-After.400 LIMIT_EXCEEDED: the batch can never fit. Either it has more items than your workspace’s Outstanding renders or Renders per minute (Studio: Manage → Render limits; 10 outstanding by default), or its items’ input is too large together. Split it into smaller batches.- Items with
"test": trueare test renders: they are admitted against the test allowance of 10, apart from the batch’s other items. 413 LIMIT_EXCEEDED: the request body is too large. Split it into smaller batches.
Items that render a published template version use far less of a batch’s input limit than items with your own HTML, because their fonts and images are not copied into each item. Prefer them for large runs. Every size and count is in limits.
Next steps
Section titled “Next steps”- Get notified with webhooks: one event per item.
- HTTP API: error codes and what to do about them.
- API reference: every batch field.