Skip to content

Render a batch asynchronously

For batches you don’t want to hold an HTTP request open for. The call returns immediately with a jobId; Pagr renders in the background and POSTs a webhook as each document finishes, plus one final callback when the job ends. If you can’t host a public endpoint, poll the job-status endpoint instead — it exposes the same information.

POST /v1/render/{templateId}/async
POST /v1/render/{templateId}/versions/{version}/async
GET /v1/render/jobs/{jobId}

The two POST forms enqueue against the latest published version or a specific one. The GET form polls a job’s status and is scoped to the calling organisation — another tenant’s job returns 404.

import asyncio
from pagr import PagrApiClient
async def main():
async with PagrApiClient("pagr_prod_xxxxxxxx") as client:
job = await client.enqueue_batch_render(
"8bec66ff-6f3d-4c1e-9a2b-1f0e5d7c4a90",
[
{"Title": "Acme Q3 Invoice", "Amount": 42},
{"Title": "Acme Q4 Invoice", "Amount": 58},
],
callback_url="https://your-app.example/pagr/callback?token=s3cr3t",
)
print(job.job_id, job.requested_count, job.state) # state == QUEUED
# Poll until terminal. wait_for_job wraps the "while not status.done" loop.
status = await client.wait_for_job(job.job_id, poll_interval=2.0, timeout=300)
print(status.state, status.status,
f"{status.rendered_count}/{status.requested_count}")
for issue in status.issues:
print(issue)
asyncio.run(main())
Parameter Type Required Description
templateId string (UUID) Yes The template to render.
version integer Only for the specific-version form The template version number to render.
Parameter Type Default Description
persist boolean true Whether the rendered documents are stored.
language string — Language variant to render, for multilingual templates.
Field Type Default Description
documents array of objects — (required) One object per document. Same 50 MB / 32-level limits per document as a synchronous render; a test key caps the array at 10 entries.
callbackUrl string — (required) A publicly reachable URL Pagr POSTs the webhooks to. Each POST is signed with your organisation’s webhook secret in an X-Pagr-Signature header — verify it before acting on the payload.
includeDocument boolean false When true, each progress webhook carries its document’s PDF inline as Base64.
{
"documents": [
{ "Title": "Acme Q3 Invoice", "Amount": 42 },
{ "Title": "Acme Q4 Invoice", "Amount": 58 }
],
"callbackUrl": "https://your-app.example/pagr/callback?token=s3cr3t",
"includeDocument": false
}

202 Accepted:

{
"jobId": "6b1c9f42-0d3e-4a58-b7c1-9e2f8a4d5c30",
"requestedCount": 2,
"state": "queued" // always "queued" here — poll for progression
}
Field Type Description
jobId string (UUID) Use it to poll the job or correlate webhooks.
requestedCount number Documents submitted with the job.
state string Always queued on this response.
GET /v1/render/jobs/{jobId}
Parameter Type Required Description
jobId string (UUID), path Yes The job returned by the enqueue call.

Call it on an interval — every couple of seconds is fine — and stop once state is a terminal value. Response: 200:

{
"jobId": "6b1c9f42-0d3e-4a58-b7c1-9e2f8a4d5c30",
"state": "completed", // lifecycle: pending | completed | failed
"status": "partial", // outcome: ok | partial | failed | insufficient_credit (null while pending)
"renderedCount": 1,
"requestedCount": 2,
"missingCount": 1, // requestedCount − renderedCount
"issues": [
{ "type": "MissingBinding", "severity": "Warning", "description": "…", "documentIndex": 1 }
],
"startedAt": "2026-07-24T09:45:58Z",
"completedAt": "2026-07-24T09:46:04Z", // null while pending
"failureReason": null // set only when state == "failed"
}
Field Type Description
jobId string (UUID) The job.
state string The job lifecycle: pending (queued or rendering), completed (finished; documents were produced, including partial and credit-stopped runs), or failed (produced nothing).
status string or null The render outcome, same vocabulary as the sync envelope: ok, partial, failed, insufficient_credit. null while state is pending.
renderedCount number Documents produced so far.
requestedCount number Documents submitted.
missingCount number requestedCount − renderedCount.
issues array Per-document RenderIssue objects, each with its documentIndex.
startedAt string (ISO 8601) When the job started.
completedAt string (ISO 8601) or null When it reached a terminal state; null while pending.
failureReason string or null Human-readable reason, set only when state is failed.
SDK Enqueue Poll once Wait for terminal
Python enqueue_batch_render(template_id, json_data_sets, callback_url, …) get_job_status(job_id) wait_for_job(job_id, poll_interval=2.0, timeout=None)
TypeScript enqueueBatchRender(templateId, dataSets, callbackUrl, options?) getJobStatus(jobId) waitForJob(jobId, options?)
Java enqueueBatchRender(templateId, dataSets, callbackUrl[, RenderOptions]) getJobStatus(jobId) waitForJob(jobId[, pollInterval, timeout])
C# EnqueueBatchRenderAsync(templateId, dataSets, callbackUrl, …) GetJobStatusAsync(jobId) WaitForJobAsync(jobId, pollInterval, timeout, …)
Ruby enqueue_batch_render(template_id, json_data_sets, callback_url, …) job_status(job_id) wait_for_job(job_id, poll_interval:, timeout:)
C++ enqueue_batch_render(template_id, data_sets, callback_url, RenderOptions) get_job_status(job_id) wait_for_job(job_id, poll_interval, timeout)

Every SDK’s wait_for_job treats an unrecognised state as terminal (fail-open), so a state a newer server introduces can never trap the loop in an infinite wait.

Status code When
400 ValidationError A test key submitted more than 10 documents, or an unknown language.
404 EntityNotFound No job with this id exists for your organisation.
413 PayloadTooLarge A document exceeds 50 MB.
503 QueueFull The async render queue is at capacity — back off and retry.

See Errors for the full table.