Renduo
Guides

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

On this page