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:
Embed must be enabled for your project. Dashboard → Project → Embed → Overview → Enable embed mode. A project API key is rejected by
/v1/embed/sessionswithinvalid_credentialsuntil this is done — even if the key is valid for other endpoints.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 → Overview → Enable 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 rawmessagelisteners. 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.
Related
- Embed overview — architecture and partner model
- Form-fill embeddable — full reference for the form surface
- Styling & themes — make the embed look like your product
- Variable catalog — show partner-specific fields in the variable picker
- JWT spec — token shape, signing, kid rotation
- postMessage protocol — full event catalogue
- Host SDK — TypeScript helpers for mount + listen