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
renderrow 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 toamountbefore posting.
Related
- POST /v1/templates/:slug/render — the API-key path
- GET /v1/renders/:id — same status surface for both triggers
- Errors —
invalid_signatureand validation errors