Craftkitdocs

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/:id

Quick 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
                   → cancelled

succeeded, 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 downloadUrl as ephemeral. It's pre-signed for 24h. If you need long-term storage, fetch and persist it on your side.
  • Check durationMs for capacity planning. Sustained durations > 1s mean your templates are doing heavy work — consider splitting them.