Guides · Batch API

Batch generation walkthrough.

Generate hundreds of documents from one Word template and one spreadsheet. This guide covers the whole job: prepare the data, send the batch, poll the job, download the ZIP, and read skippedRecords.

Overview

Before You Start

The API reference documents each endpoint on its own. This page runs them in order.

  • A DOCX template uploaded to BulkRender, and its templateId.
  • An API key, sent as X-API-Key.
  • Enough credits. DOCX costs 1 credit per document, PDF costs 2.
Step 1

Prepare the Spreadsheet

One row per document. One column per template variable. Column headers must match the variable names in your template exactly, including case.

invoice_no customer due_date amount_due
INV-1001 Alice Ltd 14 Mar 2026 £1,240.00
INV-1002 Bob Ltd 21 Mar 2026 £980.50

Four rules save most of the debugging time:

  1. Pre-compute every total. BulkRender fills values, it never calculates them. Subtotals, tax, and grand totals must already sit in a column.
  2. Pre-format currency and dates as text. Write £1,240.00 and 14 Mar 2026, not a raw number or a date serial. The engine inserts what you send, as text.
  3. Clean spreadsheet error values first. Cells holding #N/A, #REF!, #VALUE! and similar cause the row to be skipped. See step 6.
  4. Repeating tables need nested data. A line-items table in your template maps to an array inside the record, so build it in JSON. CSV upload in the batch UI is flat and cannot carry arrays.

Check the row count against your plan limit before you send.

Plan Max records per batch
Pro 100
Enterprise 500
Custom 1,000

The ZIP itself is capped at 50 MB, and each document at 10 MB. Split very large runs into several batches.

Step 2

Convert Rows to Records

Each spreadsheet row becomes one object in the records array. A record with a repeating table looks like this:

record.json
{
  "invoice_no": "INV-1001",
  "customer": "Alice Ltd",
  "due_date": "14 Mar 2026",
  "amount_due": "£1,240.00",
  "items": [
    { "desc": "Design retainer", "qty": "1", "line_total": "£1,000.00" },
    { "desc": "Hosting", "qty": "4", "line_total": "£240.00" }
  ]
}

In the template, items is a loop: wrap the repeating table row in {{#items}} and {{/items}}, then use the item's own fields inside it.

Step 3

Send the Batch

$ Terminal
curl -X POST https://api.bulkrender.com/api/documents/generate-batch \
  -H "Content-Type: application/json" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Idempotency-Key: invoices-2026-03-14-run1" \
  -d '{
    "templateId": "c5f2e8a1-...",
    "records": [ ... ],
    "outputFormat": "pdf"
  }'

What comes back depends on size.

Records Status Meaning
10 or fewer 201 Created Done already. The body carries batchId, count, and a ZIP url. Skip to step 5.
More than 10 202 Accepted Queued. The body carries a jobId and a starting progress. Go to step 4.

A 202 body:

{
  "jobId": "9f31c0b2-...",
  "status": "processing",
  "progress": { "completed": 0, "total": 50, "failed": 0 }
}

Always send an Idempotency-Key from automated pipelines

A retry with the same key returns the first result instead of generating and charging twice.

Step 4

Poll the Job

$ Terminal
curl https://api.bulkrender.com/api/documents/batch-jobs/9f31c0b2-... \
  -H "X-API-Key: YOUR_API_KEY"
{
  "status": "success",
  "data": {
    "jobId": "9f31c0b2-...",
    "status": "processing",
    "progress": { "completed": 32, "total": 50, "failed": 0 },
    "createdAt": "2026-03-14T09:12:04.881Z",
    "completedAt": null
  }
}

data.status moves through pending, processing, then completed or failed.

  • completed: the response gains a url, the signed ZIP link.
  • failed: read error_message on the response and fix the cause before resubmitting.

Poll every 3 to 5 seconds. Batches run with a 5 minute timeout and up to 5 documents in flight at once, so a 500 record PDF job takes minutes, not seconds. Do not poll in a tight loop.

The endpoint also accepts a synchronous batchId, so one polling routine handles both sizes. For a sync batch it returns "status": "completed" with "completedSynchronously": true.

Step 5

Download the ZIP

The url is a signed link, valid for 1 hour by default and 24 hours at most.

$ Terminal
curl -L -o invoices.zip "https://download.bulkrender.com/...batch-9f31c0b2.zip?X-Amz-Expires=3600&..."

If the link expires, poll the job again for a fresh one. Generated documents auto-expire after 3 days by default, so archive the ZIP on your side.

Step 6

Read skippedRecords

BulkRender validates rows before generating. A row holding a spreadsheet error value is skipped, never generated, and never charged.

When any row is skipped, the initial 201 or 202 response includes two extra fields:

Field Meaning
originalCount How many records you sent.
skippedRecords One entry per dropped row: { "index": 2, "reason": "..." }
{
  "jobId": "9f31c0b2-...",
  "status": "processing",
  "originalCount": 50,
  "progress": { "completed": 0, "total": 48, "failed": 0 },
  "skippedRecords": [
    { "index": 2, "reason": "Cell contains spreadsheet error value #N/A" },
    { "index": 17, "reason": "Cell contains spreadsheet error value #REF!" }
  ]
}

Three things to know:

  1. index is the position in the array you sent, counted from 0. Map it back to your spreadsheet row before you report the error to a user.
  2. These fields appear on the first response only. Later job polls do not repeat them, so log the first response.
  3. If every record fails validation, the request returns 422 with the same skippedRecords list. Nothing generates and nothing is charged.

Retrying corrected rows: use a new Idempotency-Key

A partial-skip 201 is cached like any other success, so reusing the old key returns the stale result instead of regenerating. 422 responses are never cached.

Troubleshooting

Common Problems

Symptom Cause and fix
400 Bad Request Missing templateId or records, payload over 1 MB, or batch over your plan limit. Split the run or trim the payload.
403 Forbidden Not enough credits, or the key's role is too low. Top up credits, or use a key with generate rights.
422 on the whole batch Every row failed validation. Clean the error values and resubmit with a new key.
Blanks in the output A column header does not match the template variable. Match names exactly, including case.
Totals empty or wrong The template expects a computed value. Pre-compute totals in the data.
Dates or currency look raw Values were sent as numbers. Pre-format them as strings.
Retry returned the old ZIP The Idempotency-Key was reused. Use a new key.

Ready to start?

Create an account, upload your first template, and generate your first document in under five minutes.