Craftkitdocs

Form submit API

Iframe-side form-fill endpoints (embed session JWT only): submit filled form data to validate against the manifest and enqueue a form render (202), and upload an image via multipart to get back a public URL.

Endpoints for the form-fill embed. Submit a filled-in form to enqueue a render, and upload an image so a form field can carry a URL instead of a raw file. Both are called from inside the iframe and authenticate with the same session JWT used to load it.

POST /v1/embed/form-submit/:sessionId
POST /v1/embed/form-submit/:sessionId/upload-image

Authentication

Both endpoints take the embed session JWT — never a project API key:

Authorization: Bearer <session_token>

The token is verified against the iframe request origin (which must be allow-listed), and the :sessionId in the path must match the token's session. Sessions must be minted with scope.mode = "fill"; submit additionally requires the submitForm permission.


POST /v1/embed/form-submit/:sessionId

Validates the submitted data against the template version's variable manifest, merges any JWT-claimed prefill under the user-supplied data (user data wins), and enqueues a render with source = "form" so the existing render-worker picks it up unchanged. Returns 202 Accepted with a poll URL.

Auth

  • Authorization: Bearer <session_token> — a fill-mode session JWT with permissions.submitForm = true.
  • The request origin must be allow-listed (used as the token audience).
  • :sessionId must equal the token's session id.

Path parameters

Field Type Description Default
sessionId string The embed session id. Must match the bearer token's session.

Quick Start

curl

curl -X POST https://api.craftkit.dev/v1/embed/form-submit/$SESSION_ID \
  -H "Authorization: Bearer $SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "data": {
      "customer.name": "Acme Corp",
      "customer.email": "hello@acme.com"
    }
  }'

Node.js

const res = await fetch(`https://api.craftkit.dev/v1/embed/form-submit/${sessionId}`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${sessionToken}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    data: {
      'customer.name': 'Acme Corp',
      'customer.email': 'hello@acme.com',
    },
  }),
});

const { id, status, pollUrl } = await res.json();

Python

import requests

res = requests.post(
    f"https://api.craftkit.dev/v1/embed/form-submit/{session_id}",
    headers={"Authorization": f"Bearer {session_token}"},
    json={
        "data": {
            "customer.name": "Acme Corp",
            "customer.email": "hello@acme.com",
        }
    },
)
job = res.json()

Request body

{
  "data": {
    "customer.name": "Acme Corp",
    "customer.email": "hello@acme.com"
  },
  "datasetSelection": {
    "booking": "bk_123"
  }
}
Field Type Description Default
data object Variable values to render. Required. May use flat dot-keys ("customer.name") — they are expanded to nested objects ({ customer: { name } }) before manifest validation, so they match the renderer's lookups. Validated against the template version's manifest.
datasetSelection object Optional string→string map persisted on the render row (for example, which source records the form was filled from). null

Prefill merge. Any form.prefill carried in the session JWT is applied under your data — your values always win. Prefill keys not present in the template manifest are dropped silently.

Dotted keys. The form embed emits flat dot-keyed state. Reserved property names (__proto__, constructor, prototype) anywhere in a key are stripped during expansion to prevent prototype pollution.

Response — 202 Accepted

{
  "id": "0193c2c3-...",
  "status": "queued",
  "pollUrl": "https://api.craftkit.dev/v1/embed/renders/0193c2c3-...",
  "downloadUrl": null,
  "errorMessage": null,
  "createdAt": "2026-06-05T10:00:00.000Z"
}
Field Type Description
id string Render id (UUID).
status string queued initially. Progresses to rendering then succeeded | failed.
pollUrl string Embed-scoped poll URL (/v1/embed/renders/:id). Poll it with the session JWT — not the partner API key.
downloadUrl string | null null until the render succeeds.
errorMessage string | null Populated on failure.
createdAt string ISO-8601 timestamp.

Errors

HTTP Code Meaning Fix
400 bad_request Request origin is not in the allowed origins list Add the embed origin to the partner's allow-list
400 invalid_json Body wasn't valid JSON Check Content-Type and JSON.stringify
400 invalid_request Body didn't match { data, datasetSelection? } See the request body table; inspect issues
400 invalid_input_data data didn't match the template manifest Inspect issues.fieldErrors for offending keys
401 unauthorized Missing/malformed Authorization header, or the session token failed verification Send Authorization: Bearer <session_token> from an allow-listed origin
403 wrong_mode Session is not scope.mode = "fill" Mint a fill-mode session
403 permission_denied Session lacks the submitForm permission Mint the session with permissions.submitForm = true
404 session_not_found :sessionId doesn't match the bearer token Use the session id the token was minted for
404 template_not_resolved Session scope carries no template id, or the external id isn't a UUID Mint with scope.templateExternalId set to the Craftkit template UUID
404 template_not_found No such template in this project Check the template id and the session's project
404 unpublished_template The template has no published version Publish a version first
503 queue_unavailable Render queue temporarily unreachable Retry in a moment
500 internal The render row could not be written or enqueued Retry; if it persists, contact support

POST /v1/embed/form-submit/:sessionId/upload-image

Accepts a multipart/form-data upload with a single file field (an image) and stores it in object storage under the session's namespace. Returns the public URL so the form submit payload can carry a URL instead of a raw file. Called by the form embed before it POSTs the JSON form data.

Auth

  • Authorization: Bearer <session_token> — a fill-mode session JWT.
  • The request origin must be allow-listed.
  • :sessionId must equal the token's session id.

The submitForm permission is not required for upload — only fill mode.

Path parameters

Field Type Description Default
sessionId string The embed session id. Must match the bearer token's session.

Request body

multipart/form-data with one field:

Field Type Description Default
file file The image to upload. MIME type must start with image/. Max size 10 MB.

The stored object key is embed-uploads/{projectId}/{sessionId}/{uuid}.{ext}. The extension comes from the original filename, falling back to a MIME-type mapping (jpg, png, gif, webp, svg) or .bin.

Quick Start

curl

curl -X POST https://api.craftkit.dev/v1/embed/form-submit/$SESSION_ID/upload-image \
  -H "Authorization: Bearer $SESSION_TOKEN" \
  -F "file=@logo.png"

Node.js

const form = new FormData();
form.append('file', fileBlob, 'logo.png');

const res = await fetch(
  `https://api.craftkit.dev/v1/embed/form-submit/${sessionId}/upload-image`,
  {
    method: 'POST',
    headers: { Authorization: `Bearer ${sessionToken}` },
    body: form,
  },
);

const { url } = await res.json();

Python

import requests

with open("logo.png", "rb") as f:
    res = requests.post(
        f"https://api.craftkit.dev/v1/embed/form-submit/{session_id}/upload-image",
        headers={"Authorization": f"Bearer {session_token}"},
        files={"file": ("logo.png", f, "image/png")},
    )

url = res.json()["url"]

Response — 200 OK

{
  "url": "https://cdn.craftkit.dev/embed-uploads/proj_01j.../sess_01j.../9f1c....png"
}
Field Type Description
url string Public URL of the uploaded image. Pass it back as the value of the matching image variable in the data of your form submit.

Errors

HTTP Code Meaning Fix
400 bad_request Request origin is not in the allowed origins list Add the embed origin to the partner's allow-list
400 invalid_multipart Body wasn't valid multipart/form-data Send the file as multipart, not JSON
400 missing_file No file field in the form data Include a file part
401 unauthorized Missing/malformed Authorization header, or the session token failed verification Send Authorization: Bearer <session_token> from an allow-listed origin
403 wrong_mode Session is not scope.mode = "fill" Mint a fill-mode session
404 session_not_found :sessionId doesn't match the bearer token Use the session id the token was minted for
413 file_too_large File exceeds the 10 MB limit Compress or resize the image
415 invalid_type The file's MIME type is not image/* Upload an image file

Last revised: 2026-06-26