Session API (mint & refresh)
Mint a signed embed session (POST /v1/embed/sessions) and rotate it with the single-use renew token (POST /v1/embed/sessions/refresh).
Mint a short-lived embed session your front-end can load in an iframe. The partner backend calls this with its project API key; the response carries a signed session JWT, the iframe URL to mount, and a single-use renew token. Refresh rotates that token before the session expires.
POST /v1/embed/sessions
POST /v1/embed/sessions/refreshBoth endpoints authenticate with a project API key (Authorization: Bearer ck_live_…). The key's project must have embed enabled (an embed partner row), or auth fails with invalid_credentials. Sessions live for 4 hours.
Quick Start
curl
curl -X POST https://api.craftkit.dev/v1/embed/sessions \
-H "Authorization: Bearer $CRAFTKIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"tenant": { "externalId": "org_123", "displayName": "Acme Corp" },
"actor": { "externalId": "usr_456", "displayName": "Jane Smith", "email": "jane@acme.com" },
"scope": { "mode": "edit", "templateExternalId": "invoice" },
"catalogRef": { "name": "my-catalog" },
"permissions": { "publish": true, "saveDraft": true }
}'Node.js
const res = await fetch('https://api.craftkit.dev/v1/embed/sessions', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.CRAFTKIT_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
tenant: { externalId: 'org_123', displayName: 'Acme Corp' },
actor: { externalId: 'usr_456', displayName: 'Jane Smith', email: 'jane@acme.com' },
scope: { mode: 'edit', templateExternalId: 'invoice' },
catalogRef: { name: 'my-catalog' },
permissions: { publish: true, saveDraft: true },
}),
});
const { iframe_url, renew_token, expires_at } = await res.json();
// Mount iframe_url in an <iframe>; persist renew_token to rotate before expires_at.Python
import os, requests
res = requests.post(
"https://api.craftkit.dev/v1/embed/sessions",
headers={"Authorization": f"Bearer {os.environ['CRAFTKIT_API_KEY']}"},
json={
"tenant": {"externalId": "org_123", "displayName": "Acme Corp"},
"actor": {"externalId": "usr_456", "displayName": "Jane Smith", "email": "jane@acme.com"},
"scope": {"mode": "edit", "templateExternalId": "invoice"},
"catalogRef": {"name": "my-catalog"},
"permissions": {"publish": True, "saveDraft": True},
},
)
session = res.json()Request body
{
"tenant": {
"externalId": "org_123",
"displayName": "Acme Corp",
"branding": { "primaryColor": "#0F62FE" }
},
"actor": {
"externalId": "usr_456",
"displayName": "Jane Smith",
"email": "jane@acme.com",
"avatarUrl": "https://acme.com/avatars/jane.png"
},
"scope": { "mode": "edit", "templateExternalId": "invoice", "initialName": "New invoice" },
"catalogRef": { "name": "my-catalog", "version": 2 },
"permissions": { "publish": true, "saveDraft": true, "delete": false },
"permissionsPreset": "editor",
"branding": { "primaryColor": "#0F62FE", "logoUrl": "https://acme.com/logo.svg" },
"appearance": { "baseTheme": "light", "variables": { "colorPrimary": "#0F62FE" } },
"callbacks": { "onPublishedUrl": "https://acme.com/hooks/published" },
"limits": { "maxPublishes": 10 },
"form": { "showPreview": true, "prefill": { "customer.name": "Acme Corp" }, "captureMode": "render" }
}| Field | Type | Description | Default |
|---|---|---|---|
tenant |
object | The organization the session belongs to. Upserted on every mint. Required. | — |
tenant.externalId |
string | Your stable id for the org (1–160 chars). Required. | — |
tenant.displayName |
string | Org name shown in the embed (1–200 chars). Required. | — |
tenant.branding |
object | Optional partial branding override scoped to this tenant. | — |
actor |
object | The end-user inside the iframe. Upserted under the tenant. Required. | — |
actor.externalId |
string | Your stable id for the user (1–160 chars). Required. | — |
actor.displayName |
string | User's display name (≤200 chars). | — |
actor.email |
string | User email (validated). | — |
actor.avatarUrl |
string | User avatar URL (validated). | — |
scope |
object | What the session can open. | { "mode": "edit" } |
scope.mode |
string | edit, create, view, or fill. fill mounts the form route; the others mount the builder. |
edit |
scope.templateExternalId |
string | Your id for the template to load (≤200 chars). Resolved to a Craftkit template id. | — |
scope.initialName |
string | Initial name for new templates (create mode). Ignored when loading an existing template. |
— |
variableCatalog |
object | An inline catalog to use for this session only (see Publish a catalog for the shape). Mutually exclusive with catalogRef. |
— |
catalogRef |
object | Reference a published catalog by name (+ optional version). Resolves to the current version when version is omitted. |
— |
permissions |
object | Partial override of the permission flags (publish, saveDraft, delete, rename, rollback, createCustomVariables, changePageSettings, viewVersionHistory, submitForm, saveFormDraft, shareDocument, emailDocument, viewEngagement). Omitted flags fall back to schema defaults. |
schema defaults |
permissionsPreset |
string | Name of a saved permission preset (≤60 chars). Accepted by the schema; reserved. | — |
branding |
object | Partial branding (primaryColor, logoUrl, fontUrl, locale, ui, support). |
locale en |
appearance |
object | Framework-agnostic styling contract (baseTheme, variables, rules, layout, stylesheetUrl, fontUrl, logoUrl). Supersedes branding when both are present. |
partner default theme |
callbacks |
object | onPublishedUrl / onCloseUrl — partner URLs the embed posts to. |
— |
limits |
object | Partial override of maxPublishes (10), maxSaveDrafts (200), maxUploadsBytes (5 MiB). |
shown defaults |
form |
object | Form-fill claims — only meaningful when scope.mode === 'fill': prefill, showPreview (false), showDocumentAfterSubmit (true), redirectUrl, captureMode (render). |
— |
form.captureMode |
string | render (default) creates a render on submit and returns a render envelope — today's behavior. collect makes the form data-collection only: submit validates the data and emits a form.submitted webhook to the project's subscribers without creating a render or storing the field data; you persist the data and request the render as a separate call. Requires an active webhook subscribed to form.submitted. showDocumentAfterSubmit is not applicable in collect mode. |
render |
Collect-only capture.
form.captureMode: "collect"is opt-in per fill session and is designed for partners who are the system of record and don't want Craftkit to retain the submitted field data. Because nothing is stored, theform.submittedwebhook is the only delivery path (no pull fallback) — the payload is held until you2xxit, then purged. See Webhooks →form.*. ThecaptureModeis persisted server-side, so a token refresh keeps acollectsessioncollect.prefillis not persisted across refresh (your client already holds those values).
Catalog: inline vs reference. Send either
variableCatalog(a one-off inline catalog) orcatalogRef(a pointer to a published catalog). If you send neither, the session has no catalog.catalogRef.namemust already be published to this project — an unknown name returns404 catalog_not_found. See Publish a catalog.
Response
{
"session_id": "0193c2c3-1a2b-7c3d-8e4f-aabbccddeeff",
"session_token": "eyJhbGciOiJFZERTQS...",
"iframe_url": "https://embed.craftkit.dev/embed/builder?session_token=eyJhbGciOiJFZERTQS...",
"expires_at": "2026-06-05T14:00:00.000Z",
"renew_token": "ert_8sR2...Xq"
}| Field | Type | Description |
|---|---|---|
session_id |
string | Session UUID. Use it to revoke the session server-side. |
session_token |
string | Signed EdDSA JWT. Carried as ?session_token= in iframe_url; do not expose it to the wrong origin. |
iframe_url |
string | The URL to mount in your <iframe>. Points at the builder (or the form route in fill mode). |
expires_at |
string | ISO-8601 expiry, 4 hours from mint. Call refresh before this. |
renew_token |
string | Single-use token for POST /v1/embed/sessions/refresh. Each refresh returns a new one; the old one is invalidated. |
Refresh — POST /v1/embed/sessions/refresh
Rotate a session's token before it expires. The renewToken is single-use: each call mints a fresh token, extends expires_at by 4 hours, and returns a new renew_token (the old one stops working).
curl -X POST https://api.craftkit.dev/v1/embed/sessions/refresh \
-H "Authorization: Bearer $CRAFTKIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "renewToken": "ert_8sR2...Xq" }'| Field | Type | Description | Default |
|---|---|---|---|
renewToken |
string | The renew_token from the last mint or refresh (min 8 chars). Required. |
— |
The response shape is identical to the mint response (session_id, session_token, iframe_url, expires_at, renew_token).
Same project's key required. Refresh looks the session up by
(partnerId, renewToken). You must call it with an API key from the same project that minted the session — a valid key from a different project will not find the session and returns401 refresh_failed. A renew token that was already rotated, or a session that is no longer active, also returns401 refresh_failed.
Errors
| HTTP | Code | Meaning | Fix |
|---|---|---|---|
| 401 | missing_authorization |
No Authorization: Bearer header |
Send the project API key |
| 401 | invalid_credentials |
Key not found, revoked, or embed not enabled for the project | Check the key and that embed is enabled |
| 400 | invalid_json |
Body wasn't valid JSON | Check Content-Type and JSON.stringify |
| 422 | invalid_request |
Body failed schema validation (mint includes issues) |
See the request body table above |
| 404 | catalog_not_found |
catalogRef.name has no current catalog in this project |
Publish the catalog first, or fix the name |
| 401 | refresh_failed |
(refresh) Renew token invalid, already rotated, session inactive, or wrong project's key | Re-mint, or use the minting project's key |
| 500 | catalog_resolution_failed |
Inline catalog or catalogRef lookup threw server-side |
Retry; check the catalog payload |
| 500 | mint_failed |
Session minting threw (e.g. partner has no active signing key) | Retry; contact support if it persists |
Related
- Publish a catalog — define the variable picker tree referenced by
catalogRef - Multi-tenant setup — refresh with the correct per-org key
- Errors — error envelope and retry semantics
- Authentication — bearer token format