Template management
Create, read, list, update (create-or-republish), and delete templates from a variable manifest. Covers the create body (name, manifest, optional description/slug/layout/pageConfig), derived-slug auto-suffix vs explicit-slug 409, PUT idempotent create-or-republish with version increment, DELETE soft-delete + slug tombstoning, and the manifest/jsonSchema fields returned by GET.
Create, read, list, update, and delete templates from a variable manifest — no dashboard or hand-written layout required. Craftkit synthesizes a renderable layout from the manifest and publishes it as a version; render against it with POST /v1/templates/:slug/render.
POST /v1/templates
GET /v1/templates
GET /v1/templates/:slug
PUT /v1/templates/:slug
DELETE /v1/templates/:slug:slug is the template's slug within the authenticated key's project. All five endpoints are scoped to the project that owns the API key.
Quick Start
Create a template from a manifest, then list it back.
curl
curl -X POST https://api.craftkit.dev/v1/templates \
-H "Authorization: Bearer $CRAFTKIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Charter Handover",
"slug": "charter-handover",
"manifest": {
"variables": [
{ "key": "booking.code", "label": "Booking code", "dataType": "text", "required": true },
{ "key": "handover.signedBy", "label": "Signed by", "dataType": "text", "required": true },
{ "key": "handover.signatureImageUrl", "label": "Signature", "dataType": "image" }
],
"loops": [
{ "key": "areas", "label": "Inspection areas", "itemFields": [
{ "key": "areaKey", "label": "Area", "dataType": "text" },
{ "key": "condition", "label": "Condition", "dataType": "text" }
] }
]
}
}'
curl https://api.craftkit.dev/v1/templates \
-H "Authorization: Bearer $CRAFTKIT_API_KEY"Node.js
const base = 'https://api.craftkit.dev';
const headers = {
Authorization: `Bearer ${process.env.CRAFTKIT_API_KEY}`,
'Content-Type': 'application/json',
};
// Idempotent provisioning: create-or-republish at a known slug.
const put = await fetch(`${base}/v1/templates/charter-handover`, {
method: 'PUT',
headers,
body: JSON.stringify({
name: 'Charter Handover',
manifest: {
variables: [
{ key: 'booking.code', label: 'Booking code', dataType: 'text', required: true },
{ key: 'handover.signatureImageUrl', label: 'Signature', dataType: 'image' },
],
loops: [
{
key: 'areas',
label: 'Inspection areas',
itemFields: [
{ key: 'areaKey', label: 'Area', dataType: 'text' },
{ key: 'condition', label: 'Condition', dataType: 'text' },
],
},
],
},
}),
});
const { slug, currentVersionNumber } = await put.json();Python
import os, requests
base = 'https://api.craftkit.dev'
headers = {'Authorization': f"Bearer {os.environ['CRAFTKIT_API_KEY']}"}
# Fetch the manifest + generated JSON Schema before rendering.
res = requests.get(f'{base}/v1/templates/charter-handover', headers=headers)
template = res.json()
manifest = template['manifest']
json_schema = template['jsonSchema']POST /v1/templates — create
Creates a template from a manifest, synthesizes a layout, and publishes version 1.
Request body
{
"name": "Charter Handover",
"slug": "charter-handover",
"description": "Pre/post charter inspection sign-off",
"manifest": {
"variables": [
{ "key": "booking.code", "label": "Booking code", "dataType": "text", "required": true },
{ "key": "handover.signatureImageUrl", "label": "Signature", "dataType": "image" }
],
"loops": [
{ "key": "areas", "label": "Inspection areas", "itemFields": [
{ "key": "areaKey", "label": "Area", "dataType": "text" },
{ "key": "condition", "label": "Condition", "dataType": "text" }
] }
]
},
"pageConfig": { "format": "A4", "orientation": "portrait", "margin": "20mm", "printBackground": true }
}| Field | Type | Description | Default |
|---|---|---|---|
name |
string | Display name, 1–120 chars. Required. | — |
slug |
string | Kebab-case identifier (^[a-z0-9]+(?:-[a-z0-9]+)*$), 1–120 chars, unique in the project. Omit to derive one from name. A provided slug that already exists returns 409. |
Derived from name |
description |
string | Optional, up to 280 chars. | null |
manifest |
object | Variable manifest the render payload binds to: { variables: VariableDefinition[], loops: LoopDefinition[] }. Required. Both arrays must be present. |
— |
manifest.variables[] |
object | Scalar variables. Each: key (dot-path), label, dataType, optional required/defaultValue/format/description. An image variable renders as a data-bound <img>. |
— |
manifest.loops[] |
object | Array loops, each rendered as a repeating table. Each: key (dot-free, top-level), label, itemFields[] (≥1). A loop key containing a dot is rejected with invalid_loop_key. |
— |
layout |
object | Optional CanvasDocument contentJson override. When omitted, a layout is synthesized from the manifest. |
Synthesized |
pageConfig |
object | Optional page format: format (A4 | A5 | Letter | Legal), orientation (portrait | landscape), margin (e.g. 20mm), printBackground. |
A4 portrait, 20mm, backgrounds on |
dataType is one of: text, longtext, number, currency, date, datetime, boolean, image, url, email.
Slug collision. A derived slug that collides gets a short random hex suffix appended (e.g.
charter-handover-9f3a1c) — the response carries the final slug. An explicit slug that collides is a hard409 slug_conflict.
Response — 201 Created
{
"id": "0193c2c3-...",
"slug": "charter-handover",
"currentVersionNumber": 1,
"manifest": { "variables": ["..."], "loops": ["..."] }
}| Field | Type | Description |
|---|---|---|
id |
string | New template id (UUID). |
slug |
string | Final slug — may differ from a derived input if it collided. |
currentVersionNumber |
number | Always 1 for a freshly created template. |
manifest |
object | The stored manifest, echoed back. |
GET /v1/templates — list
Returns every non-deleted template in the project, newest-updatedAt first. Lightweight — no manifest or JSON Schema (use GET /v1/templates/:slug for those).
Response — 200 OK
{
"templates": [
{
"id": "0193c2c3-...",
"name": "Charter Handover",
"slug": "charter-handover",
"description": null,
"templateType": "document",
"currentVersionNumber": 2,
"published": true,
"createdAt": "2026-05-01T09:00:00.000Z",
"updatedAt": "2026-06-01T12:00:00.000Z"
}
]
}| Field | Type | Description |
|---|---|---|
templates |
array | Templates in the project, ordered by updatedAt descending. |
templates[].id |
string | Template id. |
templates[].name |
string | Display name. |
templates[].slug |
string | Slug used in render and read calls. |
templates[].description |
string | null | Optional description. |
templates[].templateType |
string | document (manifest-driven default) or pdf-overlay. |
templates[].currentVersionNumber |
number | null | Published version number, or null if nothing is published yet. |
templates[].published |
boolean | true when a current version exists. |
templates[].createdAt |
string | ISO-8601 timestamp. |
templates[].updatedAt |
string | ISO-8601 timestamp. |
GET /v1/templates/:slug — read one
Fetches one template plus its published manifest (the contract your render data must satisfy) and an auto-generated JSON Schema for client-side validation.
Path parameters
| Field | Type | Description | Default |
|---|---|---|---|
slug |
string | Template slug, unique within the project. | — |
Response — 200 OK
{
"id": "0193c2c3-...",
"name": "Charter Handover",
"slug": "charter-handover",
"description": null,
"templateType": "document",
"currentVersionNumber": 2,
"published": true,
"manifest": {
"variables": [
{ "key": "booking.code", "label": "Booking code", "dataType": "text", "required": true }
],
"loops": [
{ "key": "areas", "label": "Inspection areas", "itemFields": [
{ "key": "areaKey", "label": "Area", "dataType": "text" }
] }
]
},
"jsonSchema": { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": {} },
"createdAt": "2026-05-01T09:00:00.000Z",
"updatedAt": "2026-06-01T12:00:00.000Z"
}| Field | Type | Description |
|---|---|---|
id |
string | Template id. |
name |
string | Display name. |
slug |
string | Template slug. |
description |
string | null | Optional description. |
templateType |
string | document or pdf-overlay. |
currentVersionNumber |
number | null | Published version number, falling back to the highest version. null when unpublished. |
published |
boolean | false when the template exists but has no published version yet (distinct from a 404). |
manifest |
object | null | The published manifest ({ variables, loops }), or null when unpublished. |
jsonSchema |
object | null | Draft-07 JSON Schema derived from the manifest, or null when unpublished. |
createdAt |
string | ISO-8601 timestamp. |
updatedAt |
string | ISO-8601 timestamp. |
PUT /v1/templates/:slug — create-or-republish
The idempotent companion to create. The URL slug is canonical — any slug in the body is ignored. The body is otherwise identical to create (name, manifest, optional description/layout/pageConfig).
- Template does not exist → it is created and published as version 1 →
201. - Template exists → a new version (n+1) is published and becomes current;
name,description, and the synthesized draft are updated too →200.
Existing renders are never affected: each render pins its own templateVersionId, so republishing only changes what new renders use. Each PUT creates a new version even when the manifest is unchanged — calling PUT N times leaves N versions in history.
Path parameters
| Field | Type | Description | Default |
|---|---|---|---|
slug |
string | Canonical kebab-case slug. The URL slug wins; a slug field in the body is ignored. |
— |
Response — 201 Created (created) or 200 OK (republished)
{
"id": "0193c2c3-...",
"slug": "charter-handover",
"currentVersionNumber": 2,
"manifest": { "variables": ["..."], "loops": ["..."] }
}| Field | Type | Description |
|---|---|---|
id |
string | Template id (stable across republishes). |
slug |
string | The canonical slug (the one in the URL). |
currentVersionNumber |
number | The newly published version. 1 on create; incremented by one on every republish. |
manifest |
object | The stored manifest, echoed back. |
Idempotent provisioning. Drive PUT from a setup script: the first run creates, every subsequent run republishes in place. Prefer it over
DELETE+POSTwhen you only need the latest layout — it keeps the sameidand slug and leaves prior renders intact.
DELETE /v1/templates/:slug — soft-delete
Soft-deletes the template (sets deletedAt) and tombstones the slug so the canonical slug can be recreated. Because (project_id, slug) is a non-partial unique index, the row's slug is moved aside ({slug}__deleted__{hex}) to free the original. The original slug is echoed back. Existing renders are untouched — they pin their own templateVersionId and remain fetchable by render id.
A second DELETE of the same slug returns 404 — idempotent in effect.
Path parameters
| Field | Type | Description | Default |
|---|---|---|---|
slug |
string | Slug of the template to delete. | — |
Response — 200 OK
{
"id": "0193c2c3-...",
"slug": "charter-handover",
"deletedAt": "2026-06-06T00:00:00.000Z"
}| Field | Type | Description |
|---|---|---|
id |
string | The deleted template's id. |
slug |
string | The original (now-freed) slug, echoed back. |
deletedAt |
string | ISO-8601 timestamp of the soft-delete. |
Errors
| HTTP | Code | Meaning | Fix |
|---|---|---|---|
| 400 | invalid_json |
Body wasn't valid JSON (POST/PUT) | Check Content-Type and JSON.stringify |
| 400 | invalid_request |
Envelope, manifest, or pageConfig failed validation |
Inspect issues for the offending fields |
| 400 | invalid_loop_key |
A loop key contains a dot |
Use a dot-free top-level key (e.g. areas, not handover.areas) |
| 401 | unauthorized |
Missing, invalid, or revoked API key | Send Authorization: Bearer $CRAFTKIT_API_KEY |
| 404 | template_not_found |
No live template with that slug in this project (GET one / PUT-republish path / DELETE) | Check the slug and the key's project scope |
| 409 | slug_conflict |
An explicit slug (POST) or a tombstoned slug (PUT create) is already occupied |
Pick a new slug or DELETE the existing template first |
| 409 | version_conflict |
Concurrent republishes raced for the next version number (PUT) | Retry the request |
See Errors for the envelope shape and full code list.
Tips
- Manifest is the contract.
manifest.variablesare scalars keyed by dot-path;manifest.loopsare arrays keyed by a dot-free top-level key. The same manifest drives the synthesized layout, the auto-generated JSON Schema, and render-time validation. - Validate before rendering. GET the template, read
jsonSchema, and validate your renderdataclient-side to catch shape errors before enqueuing a job. - Layout override. Pass
layout(a CanvasDocumentcontentJson) only when you need full control; otherwise let Craftkit synthesize one from the manifest.
Related
- POST /v1/templates/:slug/render — enqueue a render against a published template
- GET /v1/renders/:id — poll a render to completion
- Errors — error envelope and retry semantics
- Authentication — bearer token format