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.

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.

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.