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: trueto skip polling. The connection blocks up to 30s; if the render takes longer you still get the queued response and can fall back to pollingpollUrl. - Validation introspection: the dashboard's template detail page exposes the auto-generated JSON Schema and a working cURL snippet that matches the live manifest.
Related
- GET /v1/renders/:id — poll the render to completion
- Inbound webhook — same pipeline, signed-URL trigger
- Errors — error envelope and retry semantics
- Authentication — bearer token format