GET /v1/renders/:id
Poll a render until it succeeds, fails, or times out.
Get the current status of a render. Most renders complete in under 200ms — this endpoint is what you poll between enqueue and download. For high volume, prefer the outgoing webhook.
GET /v1/renders/:idQuick Start
curl
curl https://api.craftkit.dev/v1/renders/0193c2c3 \
-H "Authorization: Bearer $CRAFTKIT_API_KEY"Node.js
async function pollUntilDone(pollUrl) {
const headers = { Authorization: `Bearer ${process.env.CRAFTKIT_API_KEY}` };
let delay = 200;
for (let i = 0; i < 20; i++) {
const job = await (await fetch(pollUrl, { headers })).json();
if (job.status === 'succeeded' || job.status === 'failed') return job;
await new Promise((r) => setTimeout(r, delay));
delay = Math.min(delay * 1.5, 1000);
}
throw new Error('render timed out');
}Python
import os, time, requests
def poll_until_done(poll_url):
headers = {"Authorization": f"Bearer {os.environ['CRAFTKIT_API_KEY']}"}
delay = 0.2
for _ in range(20):
job = requests.get(poll_url, headers=headers).json()
if job["status"] in ("succeeded", "failed"):
return job
time.sleep(delay)
delay = min(delay * 1.5, 1.0)
raise TimeoutError("render timed out")Response
{
"id": "0193c2c3-...",
"status": "succeeded",
"pollUrl": "https://api.craftkit.dev/v1/renders/0193c2c3-...",
"downloadUrl": "https://cdn.craftkit.dev/.../0193c2c3.pdf",
"errorMessage": null,
"createdAt": "2026-05-03T10:14:00.000Z",
"completedAt": "2026-05-03T10:14:00.150Z",
"durationMs": 150
}| Field | Type | Description |
|---|---|---|
id |
string | Render id (UUIDv7). |
status |
string | See status progression below. |
pollUrl |
string | Self-referential; same as the request URL. |
downloadUrl |
string | null | Populated when status === 'succeeded'. Pre-signed for 24h. |
errorMessage |
string | null | Populated when status === 'failed'. |
createdAt |
string | ISO-8601 enqueue timestamp. |
completedAt |
string | null | ISO-8601 terminal-state timestamp. |
durationMs |
integer | null | Wall-clock render duration. |
Status progression
queued → rendering → succeeded
→ failed
→ cancelledsucceeded, failed, and cancelled are terminal — once reached, the row never changes.
Polling cadence
A reasonable poll loop:
| Step | Delay |
|---|---|
| First poll | 200ms after the 202 |
| Backoff | 200ms, 300ms, 500ms, 1s, 1s, 1s ... capped at 1s |
| Timeout | 30s — return an error to your caller after that |
If you'd rather not poll, set options.sync: true on the render request and we'll hold the connection open for up to 30s.
Outgoing webhook (preferred for high volume)
Configure an outgoing webhook in your project settings. Craftkit POSTs to your URL when a render reaches a terminal state, signed with HMAC. Saves the polling round-trip and your worker count.
{
"type": "render.succeeded",
"renderId": "0193c2c3-...",
"templateSlug": "invoice",
"downloadUrl": "https://cdn.craftkit.dev/.../0193c2c3.pdf",
"durationMs": 150,
"createdAt": "2026-05-03T10:14:00.000Z"
}Verify the signature with your webhook secret:
Node.js
import crypto from 'node:crypto';
const expected = crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(rawBody)
.digest('hex');
if (expected !== req.headers['x-craftkit-signature']) {
throw new Error('bad signature');
}Python
import hmac, hashlib, os
expected = hmac.new(
os.environ["WEBHOOK_SECRET"].encode(),
raw_body,
hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(expected, request.headers["x-craftkit-signature"]):
raise ValueError("bad signature")Best practices
- Always cap your poll loop. A runaway poll loop on a stuck render burns API budget. 20 attempts × 1s max = 20s is plenty.
- Use the outgoing webhook above ~10 renders/min. Polling at scale wastes round-trips.
- Treat
downloadUrlas ephemeral. It's pre-signed for 24h. If you need long-term storage, fetch and persist it on your side. - Check
durationMsfor capacity planning. Sustained durations > 1s mean your templates are doing heavy work — consider splitting them.
Related
- POST /v1/templates/:slug/render — produces the id you poll
- Errors — what
status: 'failed'looks like - Inbound webhook — third-party trigger, same status flow