Async + webhooks end-to-end
Generate in the background, get notified by webhook, verify the signature, and store the PDF on your side.
This guide runs the full async flow: enqueue a generation, receive a
generation.completed webhook, verify its signature, and download the PDF
before retention purges it. You'll need a webhook-capable plan (Growth or
Enterprise) and a publicly reachable HTTPS endpoint to receive the webhook.
1. Register a webhook endpoint
In the dashboard, go to Settings → Webhooks and add your endpoint URL (must
be https). The dashboard shows a secret once — copy it now; you need it
to verify deliveries. The endpoint subscribes to generation.completed and
generation.failed.
2. Enqueue an async generation
curl -X POST https://api.renduo.dev/v1/generate \
-H "Authorization: Bearer $RENDUO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"templateId": "5f0c2e3a-…",
"mode": "async",
"payload": { "title": "Invoice 2026-001", "customer": "Acme Corp" }
}'The response is immediate:
{
"jobId": "8e7d9c4f-…",
"status": "queued"
}Save jobId as a fallback — you can poll
GET /v1/generate/:jobId if the webhook ever goes missing.
3. Receive and verify the webhook
When the job completes, Renduo delivers a signed POST to your endpoint:
Renduo-Signature: t=1785600000000,v1=9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
Content-Type: application/json{
"event": "generation.completed",
"generationId": "8e7d9c4f-…",
"templateId": "5f0c2e3a-…",
"templateVersionId": "…",
"environmentId": "…",
"status": "completed",
"createdAt": "2026-08-03T12:00:00Z",
"pdfUrl": "https://…signed…",
"pdfUrlExpiresAt": "2026-08-04T12:00:00Z",
"durationMs": 950,
"timings": { "…": "…" }
}Always verify the signature before trusting the body. The header format is
t=<unix_ms>,v1=<hmac_sha256> over <timestamp>.<raw body>:
// server.ts — example Express route
import { createHmac, timingSafeEqual } from 'node:crypto'
const SECRET = process.env.RENDUO_WEBHOOK_SECRET
app.post('/renduo-webhook', (req, res) => {
const header = req.headers['renduo-signature']
if (!verifySignature(SECRET, JSON.stringify(req.body), header)) {
return res.status(401).end()
}
const { event, generationId, pdfUrl, pdfUrlExpiresAt } = req.body
if (event === 'generation.completed' && pdfUrl) {
// Fire-and-forget the download; return 2xx fast so Renduo marks delivered.
void downloadAndStore(generationId, pdfUrl)
}
res.status(200).end()
})
function verifySignature(secret, body, header) {
const [tPart, v1Part] = String(header).split(',')
const timestamp = Number(tPart.split('=')[1])
const signature = v1Part.split('=')[1]
if (Math.abs(Date.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)
}
async function downloadAndStore(generationId, pdfUrl) {
const res = await fetch(pdfUrl)
const bytes = Buffer.from(await res.arrayBuffer())
await writeFile(`./pdfs/${generationId}.pdf`, bytes)
}Return 2xx to mark the delivery successful. Anything else (or a timeout) triggers an exponential-backoff retry: 5 attempts over ~30 minutes. Three separate events exhausting their retries disable the endpoint automatically.
4. Store the PDF on your side
The pdfUrl is a signed URL that expires (24 hours, and it is re-minted on
each poll of GET /v1/generate/:jobId). Your async PDF is only kept in R2 for
your plan's retention window (24 h Free / 30 d Starter / 90 d Growth), then
purged. So the moment you receive the webhook:
Download the PDF and store it in your own storage. Renduo's retention is a download guarantee, not archival custody — your real backup is regeneration from your immutable template version + the same payload.
Polling as a fallback
If you can't run a webhook receiver yet, poll the job status instead:
curl -H "Authorization: Bearer $RENDUO_API_KEY" \
https://api.renduo.dev/v1/generate/8e7d9c4f-…{
"jobId": "8e7d9c4f-…",
"status": "completed",
"pdfUrl": "https://…signed…",
"durationMs": 950
}Poll until status is completed or failed/timeout, then download and
store.
See also
- Async, retention & webhooks — the concepts behind this guide.
- Backend examples — complete receivers in Node and Python.
- API reference — Generations