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

  1. Create one batch with webhookUrl and persist returned identifiers.
  2. Handle callbacks idempotently, then reconcile against the status route.
  3. 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.

Minimal batch with webhookUrl
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>" }
    ]
  }'
Persist this 202 envelope
{
  "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"
}
Terminal webhook signal
{
  "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, and expiresAt before 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.

Production-oriented batch create
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"
      }
    ]
  }'
Reconcile against status
curl "https://api.solidrelay.io/v1/pdf/batch/4f93c86f-3b9f-4bd8-9f8e-bba2f8dd760d" \
  -H "X-API-Key: YOUR_API_KEY"
Recover output through zip or item route
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, and X-SolidRelay-Key-Id before 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.

Mixed-outcome status reconcile
{
  "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/zip can still recover the completed files when at least one item succeeded.
  • GET /v1/pdf/batch/:id/:itemId works 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/:id using the stored jobId.
  • Use a 200 status payload as your source of truth for terminal state, progress, and item outcomes.
  • If status returns 404 after 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.

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