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.
curl -X POST "https://pagr-prd-api-public.azurewebsites.net/v1/render/8bec66ff-6f3d-4c1e-9a2b-1f0e5d7c4a90/async" \
-H "Authorization: Bearer pagr_prod_xxxxxxxx" \
-H "Content-Type: application/json" \
{ "Title": "Acme Q3 Invoice", "Amount": 42 },
{ "Title": "Acme Q4 Invoice", "Amount": 58 }
"callbackUrl": "https://your-app.example/pagr/callback?token=s3cr3t"
# → 202 { "jobId": "…", "requestedCount": 2, "state": "queued" }
# 2. Poll (or wait for the webhook)
curl "https://pagr-prd-api-public.azurewebsites.net/v1/render/jobs/<jobId>" \
-H "Authorization: Bearer pagr_prod_xxxxxxxx"
from pagr import PagrApiClient
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:
import { PagrApiClient } from 'pagr' ;
const client = new PagrApiClient ( 'pagr_prod_xxxxxxxx' );
const job = await client. enqueueBatchRender (
'8bec66ff-6f3d-4c1e-9a2b-1f0e5d7c4a90' ,
{ Title: 'Acme Q3 Invoice' , Amount: 42 },
{ Title: 'Acme Q4 Invoice' , Amount: 58 },
'https://your-app.example/pagr/callback?token=s3cr3t' ,
console. log (job.jobId, job.requestedCount, job.state); // 'queued'
const status = await client. waitForJob (job.jobId, { pollIntervalMs: 2000 , timeoutMs: 300_000 });
console. log (status.state, status.status, `${ status . renderedCount }/${ status . requestedCount }` );
import org.example.PagrApiClient;
import org.example.models.RenderJob;
import org.example.models.RenderJobStatus;
import java.time.Duration;
try (PagrApiClient client = new PagrApiClient ( "pagr_prod_xxxxxxxx" )) {
RenderJob job = client. enqueueBatchRender (
UUID. fromString ( "8bec66ff-6f3d-4c1e-9a2b-1f0e5d7c4a90" ),
"{ \" Title \" : \" Acme Q3 Invoice \" , \" Amount \" : 42}" ,
"{ \" Title \" : \" Acme Q4 Invoice \" , \" Amount \" : 58}" ),
"https://your-app.example/pagr/callback?token=s3cr3t" );
System.out. println (job. getJobId () + " — " + job. getState ());
RenderJobStatus status = client. waitForJob (
job. getJobId (), Duration. ofSeconds ( 2 ), Duration. ofMinutes ( 5 ));
System.out. println (status. getState () + " / " + status. getStatus ()
+ " — " + status. getRenderedCount () + "/" + status. getRequestedCount ());
using var client = new PagrApiClient ( "pagr_prod_xxxxxxxx" );
var job = await client. EnqueueBatchRenderAsync (
Guid. Parse ( "8bec66ff-6f3d-4c1e-9a2b-1f0e5d7c4a90" ),
new { Title = "Acme Q3 Invoice" , Amount = 42 },
new { Title = "Acme Q4 Invoice" , Amount = 58 },
callbackUrl : "https://your-app.example/pagr/callback?token=s3cr3t" );
Console. WriteLine ( $" { job . JobId } — { job . RequestedCount } doc(s) — { job . State } " );
var status = await client. WaitForJobAsync (
job.JobId, pollInterval : TimeSpan. FromSeconds ( 2 ), timeout : TimeSpan. FromMinutes ( 5 ));
Console. WriteLine ( $" { status . State } / { status . Status } — { status . RenderedCount } / { status . RequestedCount } " );
client = Pagr :: Client . new ( "pagr_prod_xxxxxxxx" )
job = client. enqueue_batch_render (
"8bec66ff-6f3d-4c1e-9a2b-1f0e5d7c4a90" ,
{ "Title" => "Acme Q3 Invoice" , "Amount" => 42 },
{ "Title" => "Acme Q4 Invoice" , "Amount" => 58 },
"https://your-app.example/pagr/callback?token=s3cr3t" ,
puts " #{job. job_id } — #{job. requested_count } doc(s) — #{job. state } "
status = client. wait_for_job (job. job_id , poll_interval: 2.0 , timeout: 300 )
puts " #{status. state } / #{status. status } — #{status. rendered_count } / #{status. requested_count } "
#include "pagr/PagrApiClient.hpp"
pagr :: PagrApiClient client ( "pagr_prod_xxxxxxxx" );
std ::vector < std ::string > documents = {
R"({"Title": "Acme Q3 Invoice", "Amount": 42})" ,
R"({"Title": "Acme Q4 Invoice", "Amount": 58})" ,
auto job = client. enqueue_batch_render (
std :: string ( "8bec66ff-6f3d-4c1e-9a2b-1f0e5d7c4a90" ),
"https://your-app.example/pagr/callback?token=s3cr3t" );
std ::cout << job.job_id << " — " << job.requested_count << " doc(s) \n " ;
auto status = client. wait_for_job (
job.job_id, std :: chrono :: seconds ( 2 ), std :: chrono :: minutes ( 5 ));
std ::cout << status.rendered_count << "/" << status.requested_count << " \n " ;
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.
includeDocument
boolean
false
When true, each progress webhook carries its document’s PDF inline as Base64.
{ "Title" : "Acme Q3 Invoice" , "Amount" : 42 },
{ "Title" : "Acme Q4 Invoice" , "Amount" : 58 }
"callbackUrl" : "https://your-app.example/pagr/callback?token=s3cr3t" ,
Payload limits are checked at enqueue time
Document size and nesting depth are validated before the job is accepted, so
an accepted job never fails later on payload size. A rejected enqueue costs you
nothing.
202 Accepted:
"jobId" : "6b1c9f42-0d3e-4a58-b7c1-9e2f8a4d5c30" ,
"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)
"missingCount" : 1 , // requestedCount − renderedCount
{ "type" : "MissingBinding" , "severity" : "Error" , "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.
state and status answer different questions
state is did the job finish running . status is how did the documents turn
out . A job can be state: "completed" and status: "partial" at the same
time — it ran to completion, but not every document rendered. Always read both.
The issue list is capped, the counts are not
At most 100 issues are persisted per job, so a job with thousands of failing
documents returns a truncated issues array. renderedCount,
requestedCount and missingCount stay exact — trust the counts, treat
issues as a diagnostic sample.
TODO: NOt clear in documentation. Current explanation= if you do a batch async it will give you a RenderCompletion Dto, this lists all the issues. But only 100 are saved (to prevent row bloating in db), so a polling with job id afterwards can only list 100 issues.
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.