Craftkitdocs

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 hard 409 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 + POST when you only need the latest layout — it keeps the same id and 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.variables are scalars keyed by dot-path; manifest.loops are 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 render data client-side to catch shape errors before enqueuing a job.
  • Layout override. Pass layout (a CanvasDocument contentJson) only when you need full control; otherwise let Craftkit synthesize one from the manifest.