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'. A permanent public URL (no presigning, no TTL); null if no public bucket is configured. For private or revokable delivery, use the partner-key download stream or a share link.
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 and failed are the terminal states you will observe — once reached, the row never changes. cancelled exists in the enum but is not produced today (there is no cancellation path), so branch on succeeded/failed.

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

There is no synchronous render mode — options.sync is accepted by the schema but not honored, and the render endpoint always returns 202 with status: "queued". Either poll (above) or use the outgoing webhook (below). If you'd rather not poll, prefer the webhook.

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.

{
  "event": "render.succeeded",
  "renderId": "0193c2c3-...",
  "templateId": "1f2e3d4c-...",
  "status": "succeeded",
  "downloadUrl": "https://cdn.craftkit.dev/.../0193c2c3.pdf",
  "errorMessage": null,
  "createdAt": "2026-05-03T10:14:00.000Z",
  "completedAt": "2026-05-03T10:14:00.150Z"
}

Headers include x-craftkit-event, x-craftkit-signature (HMAC-SHA256 hex of the raw body), x-craftkit-timestamp, and x-craftkit-delivery-id. Subscribed events are render.succeeded and render.failed.

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.
  • downloadUrl is a permanent public URL (no presigning, no TTL). If you need expiry or private delivery, mint a share link or use the partner-key download stream rather than handing out the raw URL.
  • Check durationMs for capacity planning. Sustained durations > 1s mean your templates are doing heavy work — consider splitting them.