Skip to content

Render a batch

note:: The polling method will be omitted or changed, the current information is not fully correct

::note

One request, many documents, one response. The whole difficulty of batching isn’t sending it — it’s reading the answer, because a batch can partially succeed. This guide covers the correlation rule that makes partial failure safe to handle.

  • A published template version and its id.
  • An API key. A test key caps a batch at 10 documents per request; a production key has no fixed cap.
  • Decide whether you can wait. A synchronous batch holds the HTTP request open until every document has finished. For hundreds or thousands of documents, use an async job instead.
  1. Pass a list of data objects instead of one.

    Same endpoint, same template, same fields per document — just an array. On the wire it’s the identical documents array a single render uses, with more entries.

  2. Get back a result you iterate, not a single document.

    Every SDK returns an iterable batch result whose items line up with your input list, one slot per submitted document.

  3. Correlate by index, never by position in the output.

    A document that fails leaves its slot empty rather than shifting everything after it. The SDKs place each rendered document at the slot its own documentIndex reports, so result[3] is always the outcome of your fourth input.

  4. Check each item’s ok, and read issues on the ones that failed.

    A failed slot always carries at least one issue explaining why — the SDKs synthesise a “not rendered” issue if the server didn’t attribute one.

  5. Read the envelope-level counts for the summary.

    requested_count, rendered_count, missing_count and status describe the batch as a whole. missing_count is everything that didn’t render, whatever the reason.

Terminal window
curl -X POST "https://pagr-prd-api-public.azurewebsites.net/v1/render/8bec66ff-6f3d-4c1e-9a2b-1f0e5d7c4a90" \
-H "Authorization: Bearer pagr_prod_xxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"documents": [
{ "Title": "Acme Q3 Invoice", "Amount": 42 },
{ "Title": "Acme Q4 Invoice", "Amount": 58 },
{ "Title": "Broken Invoice" }
],
"includeDocument": true
}'
// → 200
{
"status": "partial",
"requestedCount": 3,
"renderedCount": 2,
"missingCount": 1,
"documents": [
{ "documentIndex": 0, "documentName": "Acme Q3 Invoice", },
{ "documentIndex": 1, "documentName": "Acme Q4 Invoice", }
],
"issues": [
{ "documentIndex": 2, "type": "MissingBinding", "severity": "Error", }
]
}

Note that documents has two entries for three inputs. Match on documentIndex, not array position.

This is the one thing to get right. The API returns rendered documents and a flat issue list; neither is guaranteed to be the same length as your input.

  • Each rendered document carries its own documentIndex — its zero-based position in the array you submitted.
  • Each issue carries a documentIndex too, or null for a batch-wide issue that applies to every document.

The SDKs use those indices to rebuild a one-slot-per-input list, so you never guess:

Your input documents in response SDK item
[0] Q3 Invoice documentIndex: 0 result[0].ok == true
[1] Q4 Invoice documentIndex: 1 result[1].ok == true
[2] Broken (absent) result[2].ok == false, issues populated

Every SDK’s batch result exposes the same three convenience views, so you rarely need to write the loop by hand:

SDK Successes Failures Just the documents Write them all
Python .succeeded .failed .documents .save_all(dir)
TypeScript .succeeded .failed .documents .saveAll(dir)
Java .getSucceeded() .getFailed() .getDocuments() .saveAll(dir)
C# .Succeeded .Failed .Documents .SaveAllAsync(dir)
Ruby .succeeded .failed .documents .save_all(dir)
C++ .succeeded() .failed() .documents() .save_all(dir)

save_all writes only the items that actually carry inline bytes, so it’s a no-op unless you rendered with includeDocument: true.

The envelope’s status and the result’s ok answer different questions:

Means
status: "ok" Every requested document rendered.
status: "partial" Some rendered, some didn’t.
status: "failed" Nothing rendered.
status: "insufficient_credit" Credit ran out; whatever rendered before that is kept.
result.ok Everything you requested rendered and credit sufficed. Derived client-side, not a wire field.

result.ok is always false for a credit-stopped batch, even when every document that was attempted succeeded — documents you asked for were never attempted, so the batch didn’t do what you asked. (Python and C++ derive ok from the items themselves rather than from missing_count, so a slot that came back empty is never reported as success just because the server’s count disagreed; the other SDKs check missing_count. The two agree in practice.)

  • Test keys cap the batch at 10. An 11-document batch on a pagr_test_ key is rejected up front with 400 ValidationError — nothing renders, nothing is charged. Production keys have no fixed cap, but see the next point.
  • One request, one rate-limit permit. A 500-document batch costs the same render permit as a single render, which makes batching the cheapest way to stay under the render limit.
  • Per-document limits don’t relax in a batch. Each document still gets 50 MB of JSON, 32 levels of nesting, and 60 seconds of render time.
  • Raise the timeout, don’t split the batch blindly. The client default is 30 seconds — easily too short for a large synchronous batch. Pass a per-call timeout first; if the batch is big enough to need minutes, switch to an async job.
  • Never retry a timed-out batch. The SDKs don’t retry writes because the API has no idempotency keys: a batch whose response was lost may have rendered and charged in full. Poll Documents to find out what actually landed.
  • Accept: application/pdf doesn’t work on a batch. A raw PDF body can only represent one document; asking for it returns 406 NotAcceptable before anything renders. Use includeDocument: true and read the bytes per item.
  • Validate first if inputs are user-supplied. One Error in a 500-document batch costs you 499 successful renders and a confusing report. See Validate before rendering.