Renduo

Async, retention & webhooks

The two generation modes, how long async PDFs are stored, and how webhooks notify you.

Generation has two modes — sync (default, on-demand) and async (high-volume, background). Async PDFs are stored briefly in R2, so the recommended pattern is download and store on your side; webhooks are how you automate that.

Sync mode

mode: "sync" waits for the render and returns the PDF as base64 directly in the response. It is ephemeral: Renduo does not persist the payload, the PDF, or a generation record. There is a hard timeout (10 s), after which the request fails with GENERATION_TIMEOUT.

Best for on-demand generation where a user is waiting on screen — invoices generated on checkout, receipts, and anything low-volume.

Async mode

mode: "async" enqueues a job and returns immediately:

{
  "jobId": "3f8a2c1e-…",
  "status": "queued"
}

The worker renders in the background, persists the PDF to R2, and (if you have webhooks configured) notifies you. You can poll for the result:

GET /v1/generate/:jobId
{
  "jobId": "3f8a2c1e-…",
  "status": "completed",
  "pdfUrl": "https://…signed…",
  "durationMs": 950,
  "timings": { "": "…" }
}

pdfUrl is a short-lived signed URL to the stored PDF.

Best for high-volume or background generation — batch runs, nightly reporting, anything where nobody is waiting synchronously.

Choosing a mode

SyncAsync
Latencyblocks until donefire-and-forget + notify
Returnsbase64 PDFjobId, then a signed URL
Persistenceephemeral — nothing storedPDF in R2 for the retention window
Best foron-demand, user waitinghigh volume, background

Both modes cost the same and count the same toward your plan.

Retention

Async PDFs are stored in R2 only for your plan's retention window, then deleted by a scheduled sweep:

PlanAsync retention
Free24 hours
Starter30 days
Growth90 days
Enterprisecustom / BYOS

The generation record stays (marked expired); only the binary is purged. Because the window is short, the recommended practice is:

Download the PDF and store it on your side. Your template version is immutable, so you can always regenerate the exact same document from the same payload later — that is your real backup.

Webhooks

Webhooks notify you when an async generation finishes — the piece that lets you automate download and store instead of polling.

Events

EventFires when
generation.completedAn async job finished successfully.
generation.failedAn async job failed or timed out.

Sync generations never fire webhooks — the PDF is in the response, there is nothing to notify.

Payload

{
  "event": "generation.completed",
  "generationId": "3f8a2c1e-…",
  "templateId": "…",
  "templateVersionId": "…",
  "environmentId": "…",
  "status": "completed",
  "createdAt": "2026-08-03T12:00:00Z",
  "pdfUrl": "https://…signed…",
  "pdfUrlExpiresAt": "2026-08-04T12:00:00Z",
  "durationMs": 950,
  "timings": { "": "…" }
}

The payload never contains the document's data — only identifiers and metadata.

Signature verification

Deliveries are signed with HMAC-SHA256 using the endpoint's secret, sent in the Renduo-Signature header:

Renduo-Signature: t=<unix_ms>,v1=<hex_hmac_sha256>

The signed value is <timestamp>.<raw body> — the timestamp is inside the signature, so replaying an old delivery fails the freshness check (5-minute tolerance). Always verify the signature before trusting the body:

import { createHmac, timingSafeEqual } from 'crypto'

const SECRET = process.env.RENDUO_WEBHOOK_SECRET

function verify(secret: string, body: string, header: string, now = Date.now()) {
  const [tPart, v1Part] = header.split(',')
  const timestamp = Number(tPart.split('=')[1])
  const signature = v1Part.split('=')[1]
  if (Math.abs(now - timestamp) > 5 * 60 * 1000) return false // replay

  const expected = createHmac('sha256', secret)
    .update(`${timestamp}.${body}`)
    .digest('hex')
  const a = Buffer.from(expected, 'hex')
  const b = Buffer.from(signature, 'hex')
  return a.length === b.length && timingSafeEqual(a, b)
}

Delivery & retries

Delivery is https-only, does not follow redirects, and is SSRF-guarded (Renduo will not reach private/link-local addresses — it will not probe your internal network). Failed deliveries retry with exponential backoff (5 attempts over ~30 minutes). An endpoint that exhausts retries on 3 separate events is automatically disabled and flagged in the dashboard.

Availability

Webhooks and the priority queue are Growth features (and Enterprise). On lower plans the dashboard shows them as existing-but-locked.

See also

On this page