Webhook reference
When someone completes a funnel, Cinch posts the whole response to your endpoint as JSON. This is the shape it arrives in.
Setting one up
Add your endpoint URL in a funnel's delivery settings. It must be publicly reachable over https. Nothing is sent when you save — the first delivery is the first real completion, so run through your own funnel once to see one arrive.
The request
| Property | Value |
|---|---|
| Method | POST |
| Content type | application/json; charset=utf-8 |
User-Agent | Cinch-Webhook/1 |
Cinch-Event | response.completed |
Cinch-Delivery | The response id. It stays the same across retries of the same response, which is what makes it the right thing to de-duplicate on. |
Cinch-Signature | {unix_timestamp},{hex_hmac_sha256} — see below. |
Payload
{
"event": "response.completed",
"sent_at": "2026-08-25T09:41:12.482Z",
"funnel": {
"id": "home-valuation",
"slug": "home-valuation",
"name": "Instant Home Valuation",
"version": 1
},
"response": {
"id": "res_71Bcz9",
"started_at": "2026-08-25T09:39:58.104Z",
"completed_at": "2026-08-25T09:41:11.906Z",
"score": 82,
"band": "high",
"contact": {
"name": "Sam Whitfield",
"email": "sam@example.co.uk",
"phone": "07700 900123"
},
"answers": [
{
"step_id": "type",
"question": "What kind of property is it?",
"answer": "Semi-detached",
"score": 27
},
{
"step_id": "timing",
"question": "When are you thinking of moving?",
"answer": "In the next 6 months",
"score": 28
}
],
"fields": { "type": "semi-detached", "beds": 3, "postcode": "LS1 4AB", "email": "sam@example.co.uk" },
"computed": { "estimate": 296000, "rangeLow": 281000, "rangeHigh": 311000 },
"meta": {
"referrer": "https://instagram.com/",
"utm": { "source": "instagram", "medium": "bio", "campaign": "spring" },
"device": "mobile"
}
}
}answers is the human-readable version, ordered as the visitor saw it. fields is the raw keyed data for your own systems, and computed holds anything the funnel worked out along the way. Fields may be added over time — treat unknown keys as safe to ignore rather than an error. Only event, funnel.id and response.id are guaranteed on every event.
Verifying it came from us
Each funnel has a signing secret. Compute an HMAC-SHA256 over {timestamp}.{raw_request_body} using that secret and compare it with the signature in the header. Use the raw body, not a re-serialised object, or the hash will not match.
import crypto from "node:crypto";
export function isFromCinch(rawBody, header, secret) {
const [timestamp, signature] = header.split(",");
const expected = crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
// Constant-time compare, and reject anything older than five minutes.
const fresh = Date.now() - Number(timestamp) * 1000 < 5 * 60 * 1000;
return fresh && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}Responding
- Reply
2xxas soon as you have stored the payload. Anything else counts as a failure. - Respond within 10 seconds — do slow work in a background job, not in the request.
- Failures are retried eight times with a widening gap — a minute, then four, nine, sixteen and so on up to an hour — which spreads the attempts across roughly two hours before Cinch gives up. A delivery may therefore arrive more than once; make your handler idempotent on
response.id. - After the eighth failure the delivery is marked failed and we email you to say so. The endpoint is not disabled — the next response is tried afresh — and you will not be emailed about the same endpoint more than once a day, however many responses arrive while it is down. The failure is also shown on the response itself in the dashboard.
Testing locally
Tunnel your dev server with something like ngrok or Cloudflare Tunnel and point the webhook at the public URL, or use a request bin to inspect the raw payload before you write any code. Every response in the dashboard shows how its deliveries went — sent, retrying with the attempt count, or failed with the error your endpoint returned. Replaying one by hand is not possible yet.
Beta note
Webhook fields and header names are still settling. Breaking changes will be announced on the changelog and emailed to anyone with an active endpoint before they ship.