Craftkitdocs

POST /v1/hooks/:token

Trigger a render from any external system via signed inbound webhook.

Trigger a render from any external system that can POST JSON. The webhook URL and HMAC secret are auto-generated when a template is created. Inbound webhooks share the rendering pipeline with the regular render endpoint, so the same validation, versioning, dashboard view, and outgoing webhooks apply.

POST /v1/hooks/:token

:token is the per-template inbound token from the Use this template → Inbound webhook panel.

Quick Start

The body is the variable data, flat — no { data: ... } wrapper. This shape is intentionally compatible with what most third-party integrations (Stripe, Zapier, Make, n8n) emit out of the box.

curl

SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $2}')
curl -X POST "https://api.craftkit.dev/v1/hooks/$TOKEN" \
  -H "x-craftkit-signature: $SIG" \
  -H "Content-Type: application/json" \
  -d "$BODY"

Node.js

import crypto from 'node:crypto';

const body = JSON.stringify({
  customer: { name: 'Acme Corp' },
  items: [{ name: 'Widget', qty: 5, price: 9.99 }],
  total: 49.95,
});

const signature = crypto
  .createHmac('sha256', process.env.CRAFTKIT_INBOUND_SECRET)
  .update(body)
  .digest('hex');

await fetch(`https://api.craftkit.dev/v1/hooks/${process.env.CRAFTKIT_INBOUND_TOKEN}`, {
  method: 'POST',
  headers: {
    'x-craftkit-signature': signature,
    'Content-Type': 'application/json',
  },
  body,
});

Python

import hashlib, hmac, json, os, requests

body = json.dumps({
    "customer": {"name": "Acme Corp"},
    "items": [{"name": "Widget", "qty": 5, "price": 9.99}],
    "total": 49.95,
})

signature = hmac.new(
    os.environ["CRAFTKIT_INBOUND_SECRET"].encode(),
    body.encode(),
    hashlib.sha256,
).hexdigest()

requests.post(
    f"https://api.craftkit.dev/v1/hooks/{os.environ['CRAFTKIT_INBOUND_TOKEN']}",
    headers={"x-craftkit-signature": signature, "Content-Type": "application/json"},
    data=body,
)

Request body

{
  "customer": { "name": "Acme Corp" },
  "items": [
    { "name": "Widget", "qty": 5, "price": 9.99 }
  ],
  "total": 49.95
}

The keys are the variable manifest keys for the target template (no envelope, no data wrapper). The same validation that gates the REST render endpoint applies — invalid payloads return 400 invalid_input_data with the offending fields in issues.fieldErrors.

Path parameters

Field Type Description Default
token string The per-template inbound token. Identifies the template and authenticates the request.

HMAC signature

If the sender supports HMAC, include the SHA-256 hex digest of the raw body under x-craftkit-signature, computed against the template's inbound secret.

Header Value
x-craftkit-signature Hex-encoded HMAC-SHA256 of the raw body, keyed by the template's inbound secret
Condition Result
Signature present and valid 202 Accepted (render enqueued)
Signature present and invalid 401 invalid_signature
Signature absent Accepted by default (no enforcement)

Note — Pin signature enforcement on per template via the dashboard for production traffic. The unsigned mode is convenient for prototyping with services that can't sign (e.g., a Google Form via Zapier), but always sign in production.

Response — 202 Accepted

{
  "id": "0193c2c3-...",
  "status": "queued",
  "pollUrl": "https://api.craftkit.dev/v1/renders/0193c2c3-..."
}
Field Type Description
id string Render id (UUIDv7).
status string Always queued on accept. Progresses to rendering then succeeded | failed.
pollUrl string Poll this with your project bearer token to track the render to completion.

Errors

HTTP Code Meaning Fix
400 invalid_json Body wasn't valid JSON Check Content-Type and serialization
400 invalid_input_data Body didn't match the template manifest Inspect issues.fieldErrors for offending keys
401 invalid_signature x-craftkit-signature was present but didn't match Recompute the HMAC over the exact raw bytes with the template's inbound secret
404 invalid_token No template matches this inbound token Check the token; regenerate it in the dashboard if rotated
409 no_published_version Template has no published version yet Publish a version in the dashboard
503 queue_unavailable Render queue is temporarily unreachable Retry with backoff

See Errors for the envelope shape and full code list.

Use cases

System Trigger Body shape needed
Stripe payment_intent.succeeded webhook Map invoice fields from data.object in your Stripe handler
Zapier / Make Form submission, Sheets row, etc. Map fields with the visual mapper to the template's variable keys
n8n Workflow node Set node maps CRM fields to manifest keys
Your own backend Any business event One fewer round-trip than the API key path

Same pipeline

Inbound webhook renders go through the exact same pipeline as POST /v1/templates/:slug/render:

  • Same Zod validation against the manifest.
  • Same versioning (uses the latest published version unless pinned).
  • Same render worker.
  • Same outgoing webhooks fire on completion.
  • Same render row in the dashboard.

The only differences are the auth (per-template HMAC vs project bearer token) and the body shape (flat vs enveloped).

Tips

  • One token per template. Tokens are scoped to a single template, which means revoking one doesn't affect others.
  • Sign in production. Toggle "Require signature" in the dashboard for any template that processes real money or PII.
  • Test with Zapier first. It's the fastest way to verify the manifest mapping is right without writing a server.
  • Match the source's body shape. If Stripe sends data.object.amount, configure your Stripe handler (or Zap) to flatten it to amount before posting.