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" } }
}| 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. |
— |
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