Guide
Run batch rendering with webhook-first completion and status-based recovery
Use this guide to implement the batch webhook workflow from first create through callback reconciliation and retained-output recovery. The examples start with the smallest successful batch, then move to the production version, then show the advanced mixed-outcome recovery path.
Implementation path
- Create one batch with
webhookUrland persist returned identifiers. - Handle callbacks idempotently, then reconcile against the status route.
- Recover retained output through zip or item retrieval when delivery is delayed or outcomes are mixed.
Primary flow
Create once, store jobId, then use callbacks to trigger status reconciliation.
Source of truth
Webhook events are terminal signals; GET /v1/pdf/batch/:id is the canonical state check.
Recovery posture
If callback delivery is delayed, duplicated, or missed, recover with the stored jobId and status polling.
Prerequisites
Prepare credentials, callback handling, and persistence before your first batch create
Batch-capable API key
required
Use X-API-Key. When you set webhookUrl, batch callbacks require a Professional or Business plan.
Safe webhook target
required
Use a valid absolute HTTPS URL. Public docs should treat private or local webhook targets as unsupported.
Durable batch state store
required
Persist jobId, your item IDs, current status, and callback delivery records for recovery and auditing.
- Batch requests must include at least two items, so the smallest example in this guide uses two documents.
- Design item IDs to be deterministic and unique within the batch so status reconciliation and item downloads stay predictable.
- Plan for a fast callback acknowledgment path that verifies signature headers and defers slow downstream work.
Example Ladder
Build the webhook workflow from smallest success path to mixed-outcome recovery
Easiest path
Start with one small batch of two HTML items. This proves the core webhook loop: create, persist the accepted envelope, receive one terminal event, then reconcile on the status route.
curl -X POST "https://api.solidrelay.io/v1/pdf/batch" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Idempotency-Key: batch-receipts-2026-04-10" \
-H "Content-Type: application/json" \
-d '{
"webhookUrl": "https://your-app.example.com/webhooks/solidrelay",
"items": [
{ "id": "receipt_1001", "html": "<h1>Receipt 1001</h1>" },
{ "id": "receipt_1002", "html": "<h1>Receipt 1002</h1>" }
]
}'{
"jobId": "4f93c86f-3b9f-4bd8-9f8e-bba2f8dd760d",
"status": "queued",
"itemCount": 2,
"totalCount": 2,
"statusUrl": "/v1/pdf/batch/4f93c86f-3b9f-4bd8-9f8e-bba2f8dd760d",
"estimatedCompletionTime": "2026-04-10T15:13:00.000Z",
"expiresAt": "2026-04-11T15:10:00.000Z"
}{
"event": "batch.completed",
"jobId": "4f93c86f-3b9f-4bd8-9f8e-bba2f8dd760d",
"status": "completed",
"statusUrl": "https://api.solidrelay.io/v1/pdf/batch/4f93c86f-3b9f-4bd8-9f8e-bba2f8dd760d",
"zipUrl": "https://api.solidrelay.io/v1/pdf/batch/4f93c86f-3b9f-4bd8-9f8e-bba2f8dd760d/zip",
"completedAt": "2026-04-10T15:12:23.000Z"
}- Persist
jobId,statusUrl, andexpiresAtbefore waiting for callbacks. - Treat the webhook payload as the signal to reconcile, not as the only source of truth.
Realistic path
This is the production version most teams should ship first: deterministic item IDs, mixed input modes, metadata, signature verification, status reconciliation, and retained-output recovery.
curl -X POST "https://api.solidrelay.io/v1/pdf/batch" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Idempotency-Key: batch-invoices-2026-04-05" \
-H "Content-Type: application/json" \
-d '{
"webhookUrl": "https://your-app.example.com/webhooks/solidrelay",
"expiresInHours": 24,
"notes": "April invoice run",
"metadata": { "project": "acct-recon", "requestedBy": "ops" },
"items": [
{
"id": "invoice_1001",
"templateId": "2a2bc604-feb7-41b5-8e2c-2e2784ecb67d",
"data": { "invoiceNumber": "INV-1001" },
"filename": "invoice-1001.pdf"
},
{
"id": "invoice_1002",
"url": "https://example.com/invoices/1002"
}
]
}'curl "https://api.solidrelay.io/v1/pdf/batch/4f93c86f-3b9f-4bd8-9f8e-bba2f8dd760d" \
-H "X-API-Key: YOUR_API_KEY"curl -L "https://api.solidrelay.io/v1/pdf/batch/4f93c86f-3b9f-4bd8-9f8e-bba2f8dd760d/zip" \
-H "X-API-Key: YOUR_API_KEY" \
-o invoices-2026-04-05.zip
curl -L "https://api.solidrelay.io/v1/pdf/batch/4f93c86f-3b9f-4bd8-9f8e-bba2f8dd760d/invoice_1002" \
-H "X-API-Key: YOUR_API_KEY" \
-o invoice-1002.pdf- Verify
X-SolidRelay-Signature-V2,X-SolidRelay-Timestamp,X-SolidRelay-Delivery-Id, andX-SolidRelay-Key-Idbefore processing the callback. - Acknowledge the callback quickly with
2xx, then do heavier downstream work asynchronously. - Keep create payloads deterministic so network retries can safely reuse the same
Idempotency-Key.
Advanced path
The advanced edge is not a bigger create payload. It is the recovery logic you need when the callback says the batch is terminal but the item results are mixed.
{
"jobId": "4f93c86f-3b9f-4bd8-9f8e-bba2f8dd760d",
"status": "completed",
"createdAt": "2026-04-10T15:10:00.000Z",
"updatedAt": "2026-04-10T15:12:23.000Z",
"completedAt": "2026-04-10T15:12:23.000Z",
"finishedAt": "2026-04-10T15:12:23.000Z",
"expiresAt": "2026-04-11T15:10:00.000Z",
"zipUrl": "/v1/pdf/batch/4f93c86f-3b9f-4bd8-9f8e-bba2f8dd760d/zip",
"manifestAvailable": false,
"manifestDownloadUrlEndpoint": "/v1/pdf/batch/4f93c86f-3b9f-4bd8-9f8e-bba2f8dd760d/manifest/download-url",
"progress": {
"total": 2,
"completed": 1,
"failed": 1,
"pending": 0,
"throttled": 0,
"percentComplete": 100
},
"estimatedCompletionTime": null,
"job": {
"id": "4f93c86f-3b9f-4bd8-9f8e-bba2f8dd760d",
"status": "completed",
"accessExpired": false,
"totalCount": 2,
"completedCount": 1,
"failedCount": 1,
"createdAt": "2026-04-10T15:10:00.000Z",
"updatedAt": "2026-04-10T15:12:23.000Z",
"completedAt": "2026-04-10T15:12:23.000Z",
"finishedAt": "2026-04-10T15:12:23.000Z",
"expiresAt": "2026-04-11T15:10:00.000Z",
"notes": "April invoice run",
"metadata": { "project": "acct-recon", "requestedBy": "ops" }
},
"items": [
{
"itemId": "invoice_1001",
"status": "completed",
"outputAvailable": true,
"downloadUrlEndpoint": "/v1/pdf/batch/4f93c86f-3b9f-4bd8-9f8e-bba2f8dd760d/items/invoice_1001/download-url",
"outputExpiresAt": "2026-04-11T15:10:00.000Z",
"errorMessage": null,
"retryAttempt": null,
"error": null,
"pdfSize": 245789,
"pages": 2,
"processingTimeMs": 812
},
{
"itemId": "invoice_1002",
"status": "failed",
"outputAvailable": false,
"downloadUrlEndpoint": "/v1/pdf/batch/4f93c86f-3b9f-4bd8-9f8e-bba2f8dd760d/items/invoice_1002/download-url",
"outputExpiresAt": null,
"errorMessage": "PDF generation timed out",
"retryAttempt": 2,
"error": {
"code": "RENDER_TIMEOUT",
"message": "PDF generation timed out"
},
"pdfSize": 0,
"pages": 0,
"processingTimeMs": 0
}
]
}- A terminal webhook can still lead to a status payload where some items succeeded and some failed; always inspect
items[]. GET /v1/pdf/batch/:id/zipcan still recover the completed files when at least one item succeeded.GET /v1/pdf/batch/:id/:itemIdworks only for completed items; failed items need application-specific recovery instead of download retries.
Recovery Path
Recover safely when webhook delivery fails or your handler is unavailable
Webhook delivery reduces polling load, but it does not replace your fallback control loop.
- If no callback arrives in your expected window, poll
GET /v1/pdf/batch/:idusing the storedjobId. - Use a
200status payload as your source of truth for terminal state, progress, and item outcomes. - If status returns
404after a long delay, treat the batch as expired or no longer visible and move to retention-aware recovery handling. - If zip or item retrieval returns
410 Gone, the retention window has ended and unchanged download retries will not recover the files. - Run a periodic reconciliation sweep for recently active batch IDs so missed callbacks do not become silent failures.
This recovery path is intentionally summarized instead of embedding a full webhook worker. Keep the exact route contracts on the batch reference page and the shared retryability rules onthe errors page.
Operating Rules
Keep batch webhook integrations stable under retries, duplicates, and queue pressure
Never treat callback arrival as final truth
required
Always reconcile with status before marking business workflows complete.
Design webhook handlers as idempotent
required
Expect delivery retries and duplicate notifications; dedupe by delivery id and job id.
Keep item IDs deterministic
strongly recommended
Stable item IDs make status reconciliation, item downloads, and operator debugging much easier.
Separate route errors from item failures
required
Handle overall API failures with shared retry logic, then inspect items[].error and items[].retryAttempt for item-level outcomes.
Retry create with same key and same body
recommended
For network ambiguity or temporary service pressure, keep the request body unchanged when reusing the same Idempotency-Key.
Next Steps
Use the reference and concept pages for exact contracts and failure semantics
Reference
Batch rendering API
Use exact request fields, webhook contract details, status model, retrieval routes, and status codes.
Concept
Idempotency
Align create retry behavior with stable Idempotency-Key usage so network retries do not create duplicate batches.
Errors
Shared error and retryability model
Apply consistent retry policy across create, callback reconciliation, and output retrieval.