Skip to content
CastPDF
API reference

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.

Request
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); // ..., queued
import 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"])
202
{
  "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
}
FieldDescription
template_idstring (UUID)RequiredThe template every item uses.
template_versioninteger, 1 or moreDefault: the pinned version, or else the current one. The version is fixed when the batch is created, so editing the template meanwhile changes nothing.
itemsarrayOne 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.
spreadsheetobjectA 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 charactersFile 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, marginsAs for POST /v1/pdf, for every item.
deliverobjectDefaults 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.
metadataobjectYour data for the whole batch (at most 20 keys and 8 KB), returned with it.
testbooleantrue makes a watermarked, uncounted test batch with a live key. Test keys always make test batches.
HeaderDescription
Idempotency-KeyOptional. 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.

CodeWhen
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

FieldDescription
idstring (UUID)The batch's id.
statusstringqueued, processing, then completed once every item succeeded or failed, or canceled when at least one item was canceled.
livemodebooleanfalse for a test batch.
sourcestringapi (items) or spreadsheet.
template_id, template_versionstring, integerThe template and the version every item uses.
total, queued, processing, succeeded, failed, canceledintegerItem counts.
created_at, completed_atstring, string or nullWhen the batch was created and finished.
zipobject or nullOnce 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 nullThe batch's metadata.

GET /v1/batches

Your batches, newest first. Test keys see test batches only.

curl
curl "https://api.castpdf.com/v1/batches?limit=20" \
  -H "Authorization: Bearer YOUR_API_KEY"
Query parameterDescription
limit1 to 100. Default 20.
beforeThe 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.

Request
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"])
200
{
  "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
}
CodeWhen
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
curl "https://api.castpdf.com/v1/batches/6a2f9c1e-3b4d-4e5f-8a9b-0c1d2e3f4a5b/items?limit=100" \
  -H "Authorization: Bearer YOUR_API_KEY"
200
{
  "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 parameterDescription
limit1 to 100. Default 20.
afterThe previous page's next_after.
FieldDescription
indexintegerThe item's position: 0 for the first item or spreadsheet row.
idstring (UUID)The item's document id, for GET /v1/pdf/:id.
statusstringqueued, processing, succeeded, failed or canceled.
filenamestringThe file name in the ZIP (before repeated names get -2, -3).
error_code, error_messagestring or nullWhy a failed item failed.
document_urlstring or nullA 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
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
curl "https://api.castpdf.com/v1/batches/6a2f9c1e-3b4d-4e5f-8a9b-0c1d2e3f4a5b/zip?exp=1791627309&sig=Zr5c..." \
  --output certificates.zip

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

CodeWhen
forbidden (403)The signature does not match
file_expired (410)The link or the ZIP expired
Updated Something unclear? [email protected], a real person replies.