Craftkitdocs

Quickstart

Mint a session, render the iframe, listen for events.

Drop the Craftkit builder or form into your SaaS in three steps. Both surfaces share the same partner setup, the same JWT mint flow, and the same SDK shape — the only difference is scope.mode and which mount* helper you call. This page covers both.

Before you start — two prerequisites:

  1. Embed must be enabled for your project. Dashboard → Project → Embed → OverviewEnable embed mode. A project API key is rejected by /v1/embed/sessions with invalid_credentials until this is done — even if the key is valid for other endpoints.

  2. Your API key must exist in the same environment you're calling. A key minted in local dev does not exist in production. Always create keys via the dashboard of the target environment. See Authentication for the full troubleshooting checklist.

Quick Start — builder embed

Mint a session, render the iframe, listen for events. Your customers design templates inside your app.

Mint the session (server-side)

curl -X POST https://api.craftkit.dev/v1/embed/sessions \
  -H "Authorization: Bearer $CK_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "tenant": { "externalId": "acct_42",  "displayName": "Acme Corp" },
    "actor":  { "externalId": "user_99", "email": "ops@acme.com" },
    "scope":  { "mode": "edit" }
  }'

Returns:

{
  "session_id": "...",
  "session_token": "ey...",
  "iframe_url": "https://embed.craftkit.dev/embed/builder?session_token=ey...",
  "expires_at": "2026-05-03T10:30:00.000Z",
  "renew_token": "ert_..."
}

Mount the iframe (client-side)

import { Craftkit } from '@craftkit/embed';

const builder = Craftkit.mountBuilder({
  container: '#builder',
  sessionToken: ey,
  refresh: async () => {
    const res = await fetch('/api/craftkit/refresh', { method: 'POST' });
    return (await res.json()).session_token;
  },
});

builder.on('template.published', ({ templateId }) => {
  console.log('User published template', templateId);
});

Quick Start — form embed

Same setup, different mode. Your end-users fill a published template's variables and produce documents.

Mint the session (server-side)

curl -X POST https://api.craftkit.dev/v1/embed/sessions \
  -H "Authorization: Bearer $CK_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "tenant": { "externalId": "acct_42", "displayName": "Acme Corp" },
    "actor":  { "externalId": "user_99", "email": "user@acme.com" },
    "scope":  {
      "mode": "fill",
      "template_id": "ck_tpl_invoice"
    },
    "permissions": { "submit_form": true }
  }'

The iframe_url returned points at /embed/form?session_token=....

Mount the iframe (client-side)

import { Craftkit } from '@craftkit/embed';

const form = Craftkit.mountForm({
  container: '#new-invoice',
  sessionToken: ey,
});

// Optional: dataset prefill from your app's data (stays client-side)
form.on('ready', () => {
  form.setDatasets({
    bookings: {
      label: 'Pick a booking',
      items: myBookings.map((b) => ({
        id: b.id,
        label: `${b.customer}${b.month}`,
        values: { 'customer.name': b.customer, 'booking.startDate': b.startDate },
      })),
    },
  });
});

// Optional: Stripe-style submit interception
form.on('submit', async (e) => {
  if (!needsSigning(e.data)) return;          // let default render proceed
  e.preventDefault();
  const altered = await mySigningService(e.data);
  const pdf = await myRenderPipeline(altered);
  await e.complete({ pdfUrl: pdf.url });
});

form.on('completed', ({ renderId, downloadUrl }) => {
  console.log('Document ready:', downloadUrl);
});

Step 1 — Become an embed partner

Dashboard → Project → Embed → OverviewEnable embed mode. Craftkit issues a publishable key, generates an Ed25519 signing key, and creates your first allowed origin. Your project is now an embed partner.

Add the origin(s) where you'll mount the iframe:

Pattern Example Use case
Exact match https://app.acme.com Single production host
Wildcard subdomain https://*.acme.com Per-tenant subdomains

All postMessage events are origin-gated against this list.

Step 2 — Mint a session

The session JWT is short-lived (5 minutes for view, 30 minutes for edit and fill) and scoped to one tenant + one actor. Mint it from your backend so the partner secret never touches the browser.

Field Type Description
tenant.externalId string Stable id of the customer in your system. Used for renders → tenant attribution.
tenant.displayName string Shown in the iframe chrome (and in our admin tools).
actor.externalId string The end-user inside that tenant.
actor.email string Display + audit.
scope.mode string view, edit, create, or fill (form embed only).
scope.template_id string Required for fill. The published template the form targets.
permissions object Per-mode permission flags (submit_form, save_form_draft, ...).

Keep the renew_token server-side and exchange it for a fresh session_token when the iframe emits session.expiring.

Step 3 — Render the iframe + listen for events

Drop the URL into an iframe on your page (or use the SDK's mount* helpers, which handle origin pinning and refresh). Works the same in any framework:

Vanilla HTML

<iframe
  src="https://embed.craftkit.dev/embed/builder?session_token=..."
  style="width:100%;height:100%;border:0">
</iframe>

React

<iframe
  src={iframeUrl}
  style={{ width: '100%', height: '100%', border: 0 }}
/>

Vue

<iframe :src="iframeUrl" style="width:100%;height:100%;border:0" />

Listening for events the manual way (when you don't use the SDK):

window.addEventListener('message', (e) => {
  if (e.origin !== 'https://embed.craftkit.dev') return;
  if (e.data?.type === 'craftkit.template.published') {
    console.log('Published:', e.data.payload);
  }
  if (e.data?.type === 'craftkit.form.completed') {
    console.log('Document ready:', e.data.payload.downloadUrl);
  }
});

See the postMessage protocol for the full event catalogue.

Builder vs form — when to use which

You want... Use
Customers to design their own templates Builder embed (scope.mode: 'edit' | 'create')
End-users to fill a template and get a PDF Form embed (scope.mode: 'fill')
A read-only preview of a template Builder embed with scope.mode: 'view'
Both: customers design + their users fill Both embeds, two sessions, two iframes

Tips

  • Mint the session as late as possible. Tokens are short-lived; minting one on a button click avoids expiry races.
  • Pin the origin. Always check e.origin === 'https://embed.craftkit.dev' in raw message listeners. The SDK does this for you.
  • Use the form embed for the long tail of "create document" UIs. It saves you from rebuilding a form per template.
  • Datasets stay client-side. The form embed never sends dataset content to Craftkit — just dropdown labels + selected ids for audit.