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.
Before you start
Section titled “Before you start”- 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.
How it works
Section titled “How it works”-
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
documentsarray a single render uses, with more entries. -
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.
-
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
documentIndexreports, soresult[3]is always the outcome of your fourth input. -
Check each item’s
ok, and readissueson 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.
-
Read the envelope-level counts for the summary.
requested_count,rendered_count,missing_countandstatusdescribe the batch as a whole.missing_countis everything that didn’t render, whatever the reason.
Full example
Section titled “Full example”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.
import asynciofrom pagr import PagrApiClient
TEMPLATE_ID = "8bec66ff-6f3d-4c1e-9a2b-1f0e5d7c4a90"DOCUMENTS = [ {"Title": "Acme Q3 Invoice", "Amount": 42}, {"Title": "Acme Q4 Invoice", "Amount": 58}, {"Title": "Broken Invoice"}, # missing Amount]
async def main(): async with PagrApiClient("pagr_prod_xxxxxxxx") as client: result = await client.render_batch( TEMPLATE_ID, DOCUMENTS, include_document=True, )
print(f"{result.status}: {result.rendered_count}/{result.requested_count}" f" rendered, {result.missing_count} missing")
for item in result: # iterable over BatchItem if item.ok: print(f"[{item.index}] {item.document.document_name}") else: print(f"[{item.index}] FAILED — {item.input}") for issue in item.issues: print(" ", issue)
written = result.save_all("out/") # only items with inline bytes print(f"Wrote {len(written)} file(s)")
if result.insufficient_credit: print("Batch cut short — out of page credit")
asyncio.run(main())import { PagrApiClient } from 'pagr';
const TEMPLATE_ID = '8bec66ff-6f3d-4c1e-9a2b-1f0e5d7c4a90';const DOCUMENTS = [ { Title: 'Acme Q3 Invoice', Amount: 42 }, { Title: 'Acme Q4 Invoice', Amount: 58 }, { Title: 'Broken Invoice' }, // missing Amount];
const client = new PagrApiClient('pagr_prod_xxxxxxxx');
const result = await client.renderBatch(TEMPLATE_ID, DOCUMENTS, { includeDocument: true,});
console.log(`${result.status}: ${result.renderedCount}/${result.requestedCount}` + ` rendered, ${result.missingCount} missing`);
for (const item of result) { // iterable over BatchItem if (item.ok) { console.log(`[${item.index}] ${item.document!.documentName}`); } else { console.log(`[${item.index}] FAILED`); for (const issue of item.issues) console.log(' ', issue.description); }}
const written = await result.saveAll('./out');console.log(`Wrote ${written.length} file(s)`);
if (result.insufficientCredit) console.log('Batch cut short — out of page credit');import org.example.PagrApiClient;import org.example.RenderOptions;import org.example.models.*;import java.util.*;
UUID templateId = UUID.fromString("8bec66ff-6f3d-4c1e-9a2b-1f0e5d7c4a90");List<Map<String, Object>> documents = List.of( Map.of("Title", "Acme Q3 Invoice", "Amount", 42), Map.of("Title", "Acme Q4 Invoice", "Amount", 58), Map.of("Title", "Broken Invoice")); // missing Amount
try (PagrApiClient client = new PagrApiClient("pagr_prod_xxxxxxxx")) {
BatchRenderResult result = client.renderBatch(templateId, documents, RenderOptions.builder().includeDocument(true).build());
System.out.printf("%s: %d/%d rendered, %d missing%n", result.getStatus(), result.getRenderedCount(), result.getRequestedCount(), result.getMissingCount());
for (BatchItem item : result) { // Iterable over BatchItem if (item.isOk()) { System.out.printf("[%d] %s%n", item.getIndex(), item.getDocument().getDocumentName()); } else { System.out.printf("[%d] FAILED%n", item.getIndex()); item.getIssues().forEach(i -> System.out.println(" " + i)); } }
if (result.isInsufficientCredit()) { System.out.println("Batch cut short — out of page credit"); }}using Pagr.Sdk;
var templateId = Guid.Parse("8bec66ff-6f3d-4c1e-9a2b-1f0e5d7c4a90");var documents = new[]{ new { Title = "Acme Q3 Invoice", Amount = 42 }, new { Title = "Acme Q4 Invoice", Amount = 58 }, new { Title = "Broken Invoice", Amount = (int?)null }, // missing Amount};
using var client = new PagrApiClient("pagr_prod_xxxxxxxx");
var result = await client.RenderBatchAsync(templateId, documents, includeDocument: true);
Console.WriteLine($"{result.Status}: {result.RenderedCount}/{result.RequestedCount}" + $" rendered, {result.MissingCount} missing");
foreach (var item in result) // IReadOnlyList<BatchItem>{ if (item.Ok) Console.WriteLine($"[{item.Index}] {item.Document!.DocumentName}"); else { Console.WriteLine($"[{item.Index}] FAILED"); foreach (var issue in item.Issues) Console.WriteLine($" {issue}"); }}
if (result.InsufficientCredit) Console.WriteLine("Batch cut short — out of page credit");require "pagr"
TEMPLATE_ID = "8bec66ff-6f3d-4c1e-9a2b-1f0e5d7c4a90"DOCUMENTS = [ { "Title" => "Acme Q3 Invoice", "Amount" => 42 }, { "Title" => "Acme Q4 Invoice", "Amount" => 58 }, { "Title" => "Broken Invoice" }, # missing Amount]
client = Pagr::Client.new("pagr_prod_xxxxxxxx")
result = client.render_batch(TEMPLATE_ID, DOCUMENTS, include_document: true)
puts "#{result.status}: #{result.rendered_count}/#{result.requested_count} " \ "rendered, #{result.missing_count} missing"
result.each do |item| # Enumerable over BatchItem if item.ok? puts "[#{item.index}] #{item.document.document_name}" else puts "[#{item.index}] FAILED" item.issues.each { |issue| puts " #{issue}" } endend
written = result.save_all("out")puts "Wrote #{written.size} file(s)"
puts "Batch cut short — out of page credit" if result.insufficient_credit?#include "pagr/PagrApiClient.hpp"
const std::string kTemplateId = "8bec66ff-6f3d-4c1e-9a2b-1f0e5d7c4a90";
std::vector<std::string> documents = { R"({"Title": "Acme Q3 Invoice", "Amount": 42})", R"({"Title": "Acme Q4 Invoice", "Amount": 58})", R"({"Title": "Broken Invoice"})", // missing Amount};
pagr::PagrApiClient client("pagr_prod_xxxxxxxx");
const auto result = client.render_batch( kTemplateId, documents, {.include_document = true});
std::cout << result.status() << ": " << result.rendered_count() << "/" << result.requested_count() << " rendered\n";
for (const auto& item : result) { if (item.ok()) { std::cout << "[" << item.index << "] " << item.document->document_name << "\n"; } else { std::cout << "[" << item.index << "] FAILED\n"; for (const auto& issue : item.issues) { std::cout << " " << issue.description << "\n"; } }}
if (result.insufficient_credit()) { std::cout << "Batch cut short — out of page credit\n";}The correlation rule
Section titled “The correlation rule”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
documentIndextoo, ornullfor 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 |
Filtering the outcome
Section titled “Filtering the outcome”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.
Reading status vs. ok
Section titled “Reading status vs. ok”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.)
Other considerations
Section titled “Other considerations”- Test keys cap the batch at 10. An 11-document batch on a
pagr_test_key is rejected up front with400 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/pdfdoesn’t work on a batch. A raw PDF body can only represent one document; asking for it returns406 NotAcceptablebefore anything renders. UseincludeDocument: trueand read the bytes per item.- Validate first if inputs are user-supplied. One
Errorin a 500-document batch costs you 499 successful renders and a confusing report. See Validate before rendering.
Related articles
Section titled “Related articles”- Render a document — the endpoint, with the full envelope reference.
- Run renders in the background — when a batch is too big to wait for.
- Validate before rendering — reject bad inputs before they cost you.
- Choose a rendering shape — how batching compares to the other three shapes.
