Batches
Make one PDF per item or spreadsheet row from one template in the background, follow the batch's progress, cancel it and download every PDF as a ZIP.
On this page
A batch makes one PDF per item from one template, in the background, and collects them in a ZIP. Each PDF is counted like any other live document (test keys and test batches are watermarked and not counted). This page is the reference for the six batch endpoints; the batches guide walks through a complete run, from a list or a spreadsheet to the ZIP.
POST /v1/batches
Creates a batch and queues its items. Send exactly one of items or spreadsheet. The body may be up to 15 MB.
curl https://api.castpdf.com/v1/batches \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: certificates-2026-10" \
-d '{
"template_id": "8d1f2c3b-4a5e-4f60-9b7a-1c2d3e4f5a6b",
"filename_pattern": "certificate-{{ name }}",
"deliver": { "email": { "subject": "Your certificate" } },
"items": [
{ "data": { "name": "Ada Lovelace" }, "deliver": { "email": { "to": "[email protected]" } } },
{ "data": { "name": "Grace Hopper" } }
]
}'const res = await fetch('https://api.castpdf.com/v1/batches', {
method: 'POST',
headers: {
Authorization: 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json',
'Idempotency-Key': 'certificates-2026-10',
},
body: JSON.stringify({
template_id: '8d1f2c3b-4a5e-4f60-9b7a-1c2d3e4f5a6b',
filename_pattern: 'certificate-{{ name }}',
deliver: { email: { subject: 'Your certificate' } },
items: [
{ data: { name: 'Ada Lovelace' }, deliver: { email: { to: '[email protected]' } } },
{ data: { name: 'Grace Hopper' } },
],
}),
});
const batch = await res.json();
console.log(batch.id, batch.status); // ..., queuedimport requests
res = requests.post(
"https://api.castpdf.com/v1/batches",
headers={"Authorization": "Bearer YOUR_API_KEY", "Idempotency-Key": "certificates-2026-10"},
json={
"template_id": "8d1f2c3b-4a5e-4f60-9b7a-1c2d3e4f5a6b",
"filename_pattern": "certificate-{{ name }}",
"deliver": {"email": {"subject": "Your certificate"}},
"items": [
{"data": {"name": "Ada Lovelace"}, "deliver": {"email": {"to": "[email protected]"}}},
{"data": {"name": "Grace Hopper"}},
],
},
timeout=60,
)
batch = res.json()
print(batch["id"], batch["status"]){
"id": "6a2f9c1e-3b4d-4e5f-8a9b-0c1d2e3f4a5b",
"status": "queued",
"livemode": true,
"source": "api",
"template_id": "8d1f2c3b-4a5e-4f60-9b7a-1c2d3e4f5a6b",
"template_version": 4,
"total": 2,
"queued": 2,
"processing": 0,
"succeeded": 0,
"failed": 0,
"canceled": 0,
"created_at": "2026-10-03T10:15:02.123Z",
"completed_at": null,
"zip": null,
"metadata": null
}| Field | Description |
|---|---|
template_idstring (UUID)Required | The template every item uses. |
template_versioninteger, 1 or more | Default: the pinned version, or else the current one. The version is fixed when the batch is created, so editing the template meanwhile changes nothing. |
itemsarray | One object per PDF: data (the Liquid data; without it the template's sample data), filename (up to 100 characters), metadata (as on POST /v1/pdf) and deliver, all optional. |
spreadsheetobject | A CSV or XLSX file instead of items: filename and content_base64 (required), and optionally mapping (template field to column) and email_column (the column holding each row's email address). See POST /v1/spreadsheets/parse. |
filename_patternstring, up to 200 characters | File names from each item's data, with {{ field }} placeholders: "certificate-{{ name }}". An item's own filename wins. Default: the template's name and the item's number. Names that repeat in the ZIP get -2, -3. |
mode, format, orientation, margins | As for POST /v1/pdf, for every item. |
deliverobject | Defaults for every item's deliver.email (for example subject and message); an item's own fields win. Only items with a to are emailed. See email delivery. |
metadataobject | Your data for the whole batch (at most 20 keys and 8 KB), returned with it. |
testboolean | true makes a watermarked, uncounted test batch with a live key. Test keys always make test batches. |
| Header | Description |
|---|---|
Idempotency-Key | Optional. The same key and body within 24 hours return the first batch instead of creating a second one. See idempotency. |
Items per batch, per plan: Free 25, Starter 500, Growth 1,000, Pro 2,500 and Scale 5,000. Batches do not accept e_invoice.
The answer is 202 with the batch object and a Location header pointing at GET /v1/batches/:id. When the batch finishes, a batch.completed webhook is sent with the batch object; each item also sends pdf.created or pdf.failed. Items run with your workspace's concurrent render slots and are retried like background PDFs. A batch keeps going through restarts of our servers.
| Code | When |
|---|---|
invalid_request (400) | Validation failed, both or neither of items and spreadsheet, metadata too large, a spreadsheet that cannot be read or mapped, or more items than your plan allows (details: { "limit": "batch_items", "max": N }) |
template_not_found (404) | The template or the version does not exist |
payload_too_large (413) | The body is over 15 MB |
email_not_verified (403), email_limit_reached (429) | Items with deliver.email; see email delivery |
idempotency_conflict (409), idempotency_mismatch (422) | A request with the same key is still running, or the key was used with another body |
rate_limited (429) | Too many background PDFs waiting (details.limit: "open_jobs"), too many test background PDFs in 24 hours ("test_jobs_per_day"), or the key's rate limit |
Items that fail (for example plan_limit_reached when your documents run out) do not fail the request: they show their error in the items.
The batch object
| Field | Description |
|---|---|
idstring (UUID) | The batch's id. |
statusstring | queued, processing, then completed once every item succeeded or failed, or canceled when at least one item was canceled. |
livemodeboolean | false for a test batch. |
sourcestring | api (items) or spreadsheet. |
template_id, template_versionstring, integer | The template and the version every item uses. |
total, queued, processing, succeeded, failed, canceledinteger | Item counts. |
created_at, completed_atstring, string or null | When the batch was created and finished. |
zipobject or null | Once finished: { "url", "expires_at", "bytes" }, a signed link to a ZIP of the PDFs that succeeded, kept for your plan's retention period (test batches: 1 day). null before that, when nothing succeeded, and once the ZIP expired. |
metadataobject or null | The batch's metadata. |
GET /v1/batches
Your batches, newest first. Test keys see test batches only.
curl "https://api.castpdf.com/v1/batches?limit=20" \
-H "Authorization: Bearer YOUR_API_KEY"| Query parameter | Description |
|---|---|
limit | 1 to 100. Default 20. |
before | The previous page's next_before. |
200 with { "data": [batch, ...], "next_before": "..." }, each item a batch object; next_before is null on the last page.
GET /v1/batches/:id
Returns one batch object. Poll it, or wait for the batch.completed webhook.
curl https://api.castpdf.com/v1/batches/6a2f9c1e-3b4d-4e5f-8a9b-0c1d2e3f4a5b \
-H "Authorization: Bearer YOUR_API_KEY"const res = await fetch('https://api.castpdf.com/v1/batches/6a2f9c1e-3b4d-4e5f-8a9b-0c1d2e3f4a5b', {
headers: { Authorization: 'Bearer YOUR_API_KEY' },
});
const batch = await res.json();
if (batch.zip) console.log(batch.zip.url);import requests
res = requests.get(
"https://api.castpdf.com/v1/batches/6a2f9c1e-3b4d-4e5f-8a9b-0c1d2e3f4a5b",
headers={"Authorization": "Bearer YOUR_API_KEY"},
timeout=30,
)
batch = res.json()
if batch["zip"]:
print(batch["zip"]["url"]){
"id": "6a2f9c1e-3b4d-4e5f-8a9b-0c1d2e3f4a5b",
"status": "completed",
"livemode": true,
"source": "api",
"template_id": "8d1f2c3b-4a5e-4f60-9b7a-1c2d3e4f5a6b",
"template_version": 4,
"total": 2,
"queued": 0,
"processing": 0,
"succeeded": 1,
"failed": 1,
"canceled": 0,
"created_at": "2026-10-03T10:15:02.123Z",
"completed_at": "2026-10-03T10:15:09.480Z",
"zip": {
"url": "https://api.castpdf.com/v1/batches/6a2f9c1e-3b4d-4e5f-8a9b-0c1d2e3f4a5b/zip?exp=1791627309&sig=Zr5c...",
"expires_at": "2026-10-10T10:15:09.480Z",
"bytes": 51870
},
"metadata": null
}| Code | When |
|---|---|
batch_not_found (404) | No such batch in your workspace, or a live batch read with a test key |
GET /v1/batches/:id/items
The items of a batch in their order, one page at a time.
curl "https://api.castpdf.com/v1/batches/6a2f9c1e-3b4d-4e5f-8a9b-0c1d2e3f4a5b/items?limit=100" \
-H "Authorization: Bearer YOUR_API_KEY"{
"data": [
{
"index": 0,
"id": "0b6c1d2e-3f4a-4b5c-8d6e-7f8091a2b3c4",
"status": "succeeded",
"filename": "certificate-Ada-Lovelace.pdf",
"error_code": null,
"error_message": null,
"document_url": "https://api.castpdf.com/v1/files/0b6c1d2e-3f4a-4b5c-8d6e-7f8091a2b3c4?exp=1791627305&sig=Pa7e..."
},
{
"index": 1,
"id": "1c7d2e3f-4a5b-4c6d-9e7f-8091a2b3c4d5",
"status": "failed",
"filename": "certificate-Grace-Hopper.pdf",
"error_code": "render_timeout",
"error_message": "Rendering took longer than 30 seconds.",
"document_url": null
}
],
"next_after": null
}| Query parameter | Description |
|---|---|
limit | 1 to 100. Default 20. |
after | The previous page's next_after. |
| Field | Description |
|---|---|
indexinteger | The item's position: 0 for the first item or spreadsheet row. |
idstring (UUID) | The item's document id, for GET /v1/pdf/:id. |
statusstring | queued, processing, succeeded, failed or canceled. |
filenamestring | The file name in the ZIP (before repeated names get -2, -3). |
error_code, error_messagestring or null | Why a failed item failed. |
document_urlstring or null | A signed link to the item's PDF while it is stored. |
next_after is null on the last page. Errors: batch_not_found (404).
POST /v1/batches/:id/cancel
Cancels the items that have not started; items already rendering finish. The batch then ends as canceled (or completed if nothing was left to cancel), with a ZIP of what succeeded. Calling it again, or on a finished batch, changes nothing.
curl -X POST https://api.castpdf.com/v1/batches/6a2f9c1e-3b4d-4e5f-8a9b-0c1d2e3f4a5b/cancel \
-H "Authorization: Bearer YOUR_API_KEY"200 with the batch object, its canceled count updated. Canceled items are not counted. Errors: batch_not_found (404).
GET /v1/batches/:id/zip
Downloads a batch's ZIP. Like file links, you never build this URL: it is zip.url of the batch, signed with exp and sig, and needs no API key.
curl "https://api.castpdf.com/v1/batches/6a2f9c1e-3b4d-4e5f-8a9b-0c1d2e3f4a5b/zip?exp=1791627309&sig=Zr5c..." \
--output certificates.zip200 with content-type: application/zip, content-disposition: attachment; filename="batch-<id>.zip", content-length, cache-control: private, max-age=300 and x-content-type-options: nosniff.
| Code | When |
|---|---|
forbidden (403) | The signature does not match |
file_expired (410) | The link or the ZIP expired |