Craftkitdocs

POST /v1/templates/:slug/render

Enqueue a render job against a template version.

Enqueue a render against a template. Returns 202 Accepted with a poll URL. The variable data must match the template version's manifest, validated server-side with Zod.

POST /v1/templates/:slug/render

:slug is the template's slug within the authenticated project.

Quick Start

curl

curl -X POST https://api.craftkit.dev/v1/templates/invoice/render \
  -H "Authorization: Bearer $CRAFTKIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "data": {
      "customer": { "name": "Acme Corp" },
      "items": [{ "name": "Widget", "qty": 5, "price": 9.99 }],
      "total": 49.95
    }
  }'

Node.js

const res = await fetch('https://api.craftkit.dev/v1/templates/invoice/render', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.CRAFTKIT_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    data: {
      customer: { name: 'Acme Corp' },
      items: [{ name: 'Widget', qty: 5, price: 9.99 }],
      total: 49.95,
    },
  }),
});
const { id, pollUrl } = await res.json();

Python

import os, requests

res = requests.post(
    "https://api.craftkit.dev/v1/templates/invoice/render",
    headers={"Authorization": f"Bearer {os.environ['CRAFTKIT_API_KEY']}"},
    json={
        "data": {
            "customer": {"name": "Acme Corp"},
            "items": [{"name": "Widget", "qty": 5, "price": 9.99}],
            "total": 49.95,
        }
    },
)
job = res.json()

Request body

{
  "data": {
    "customer": { "name": "Acme Corp" },
    "items": [{ "name": "Widget", "qty": 5, "price": 9.99 }],
    "total": 49.95
  },
  "options": {
    "versionNumber": 3,
    "sync": false,
    "filename": "invoice-2026-005.pdf"
  }
}
Field Type Description Default
data object Variable values keyed by manifest key paths. Required.
options.versionNumber integer Pin to a specific published version. Current published version
options.sync boolean When true, hold the response open until the render completes (long-poll, max 30s). false
options.filename string Override the asset filename. Template default

Response — 202 Accepted

{
  "id": "0193c2c3-...",
  "status": "queued",
  "pollUrl": "https://api.craftkit.dev/v1/renders/0193c2c3-...",
  "downloadUrl": null,
  "errorMessage": null,
  "createdAt": "2026-05-03T10:14:00.000Z"
}
Field Type Description
id string Render id (UUIDv7).
status string queued initially. Progresses to rendering then succeeded | failed.
pollUrl string Hit this with the same bearer token to poll status.
downloadUrl string | null Populated on success. Pre-signed URL valid for 24h.
errorMessage string | null Populated on failure.
createdAt string ISO-8601 timestamp.

Poll pollUrl until status is succeeded or failed. See GET /v1/renders/:id for polling cadence and the outgoing webhook alternative.

Errors

HTTP Code Meaning Fix
400 invalid_json Body wasn't valid JSON Check Content-Type and JSON.stringify
400 invalid_request Top-level shape didn't match { data, options? } See request body table above
400 invalid_input_data Data didn't match the version's manifest Inspect issues.fieldErrors for offending keys
404 template_not_found No template with that slug in this project Check the slug and the API key's project scope
404 version_not_found options.versionNumber doesn't exist Omit it or pick a real version
409 no_published_version Template has no published version yet Publish a version in the dashboard
429 rate_limited Project quota exceeded Back off with jitter

See Errors for the envelope shape and full code list.

Tips

  • Idempotency: safe to retry on 5xx and network errors. The worker dedupes by jobId (derived from request body hash + template version).
  • Sync mode: for low-volume / interactive use, set options.sync: true to skip polling. The connection blocks up to 30s; if the render takes longer you still get the queued response and can fall back to polling pollUrl.
  • Validation introspection: the dashboard's template detail page exposes the auto-generated JSON Schema and a working cURL snippet that matches the live manifest.