Async job lifecycle
Renders run on a queue. By default, POST /v1/renders returns 202 Accepted with a poll URL — the actual Chromium / PhpWord work happens in a worker. For small renders that comfortably finish inside a request, ?sync=true blocks up to 15 seconds for an inline response.
State machine
queued → rendering → succeeded (terminal)
→ failed (terminal)
→ cancelled (terminal)Once a render is in a terminal state, the record is immutable.DELETE /v1/renders/{id} on a terminal render returns 409.
Async (default)
POST /v1/renders → 202 Accepted
{ {
"template_id": "01HX...", "id": "01HY...",
"formats": ["pdf"], "status": "queued",
"data": { ... } "poll_url": "/v1/renders/01HY..."
} }Poll until terminal:
GET /v1/renders/01HY... → 200 OK
{
"status": "rendering",
"duration_ms": null,
"outputs": []
}
GET /v1/renders/01HY... → 200 OK
{
"status": "succeeded",
"duration_ms": 873,
"outputs": [
{
"format": "pdf",
"url": "https://…?signature=…",
"expires_at": "…",
"bytes": 12480,
"sha256": "…"
}
]
}Polling cadence: every SDK ships a pollRenderhelper that backs off exponentially (500ms → 5s ceiling) and respects a wall-clock budget. Don't spam getRenderat 50ms intervals — that's how you trip rate limits.
Sync (?sync=true)
Add ?sync=true to block up to 15 seconds (configurable via DOCGEN_SYNC_RENDER_TIMEOUT) for a terminal state:
POST /v1/renders?sync=true → 200 OK (finished in time)
or 202 Accepted (timed out; still running)200 means the render reached a terminal state inside the deadline — even if that terminal state is failed. Check status before assuming success.
202 from ?sync=truemeans the render overshot the deadline; it's still running on the queue. Switch to polling from this point.
When to use sync vs async
| Use sync when… | Use async when… |
|---|---|
| Single-page render | Multi-page document, complex layout |
| HTML / DOCX only (PDF can be slow on cold-start) | PDF render (Chromium cold-start adds ~500ms) |
| User waiting in front of a screen | Background email / batch job |
| Latency budget < 5s | Latency budget > 30s, or you don't care |
Idempotency keys
Both modes support an Idempotency-Key header. Same key + same template version + same input data hash returns the cached render record; same key + different parameters returns 409 Conflict. Pair this with retry-safe job dispatch in your job runner.
POST /v1/renders
Idempotency-Key: invoice-2026-0114
{ ... }Cancellation
DELETE /v1/renders/{id}cancels a queued or in-flight render. Returns 204 on success, 409 if the render is already terminal. Cancelled renders don't produce outputs.
Result webhooks (v2)
Push notification of terminal status is on the v2 roadmap — a workspace-level result_webhook URL that POSTs the render record on completion. Until then, poll.
Next
Signed downloads — how the output URLs work.