Receive records by webhook
Every new and updated lead, booking, order and request, sent to your own system and signed so you can check it came from us
Set it up
In the Agent tab, open Inbox, then Settings, and add your https address under Webhook. We send the records your agent collects (leads, bookings, orders and requests), never your catalog. Send test posts a test event, and Recent deliveries shows what we sent.
Events
record.created: a new record, from your site, the chat on your website, your app or an AI assistant, or one you added yourself.record.updated: a record changed, andchange.typesays how:status(from,to),followUp(to, null when cleared),note(body),edit(keys, the corrected fields) oropt_out(the person asked for no marketing). Deleting a record sends no event.test: the Send test button, withentity,recordandchangenull.
The payload
Each delivery is a POST with a JSON body in this shape. We read the record when we send, so a retry shows it as it is by then.
{
"event": "record.created",
"deliveredAt": "2026-09-28T10:00:05Z",
"project": { "id": "6f1c2d4e-8a9b-4c0d-9e1f-2a3b4c5d6e7f", "name": "Cook & Bake" },
"entity": { "name": "bookings", "label": "Bookings", "kind": "booking" },
"record": {
"id": "4f0c8a2e-1b3d-4e5f-8a9b-0c1d2e3f4a5b",
"createdAt": "2026-09-28T10:00:00Z",
"status": "new",
"followUpAt": null,
"fields": {
"fullName": "Dana Levi",
"phone": "054-1234567",
"marketingConsent": true,
"marketingConsentText": "Send me offers and news",
"marketingConsentAt": "2026-09-28T09:59:58Z"
},
"summary": "Table for 4 on Friday at 20:00, one guest is vegan",
"source": { "channel": "site", "utm_campaign": "summer" }
},
"change": null
}fieldsholds what the person sent, by your form's field names, with the consent record (andmarketingConsentWithdrawnAtafter an opt-out).sourcesays where the person came from, each part only when known: thechannel(site,widget,sdk,mcp, orownerfor one you added; more may come), the landingpage,referrerHostand campaign tags.summary,source,statusandfollowUpAtcan be null (a record with no summary, one whose origin we don't know, one from before the Inbox, or no follow-up set), and so canchange.fromon a first status change. Read them defensively.X-BranderUX-Eventnames the event.X-BranderUX-Deliveryis the same on every retry of a delivery, so you can skip one you already handled.
Check the signature
X-BranderUX-Signaturereadst=<unix seconds>,v1=<hex>. Split it on the comma intot, when we signed, andv1.- Compute HMAC-SHA256 of
t + "." + rawBody, as lowercase hex, with your whole secret as the key,whsec_included. - Compare it with v1 in constant time. Reject the delivery when t is more than 300 seconds from your clock, so an old one can't be sent again.
- Always check the raw body before you parse it: parsed and rewritten JSON doesn't match the bytes we signed. In Express, read the route with express.raw({ type: "application/json" }).
const crypto = require("node:crypto");
// rawBody: the body exactly as it arrived (a Buffer), before any JSON.parse.
function verifyBranderUX(rawBody, header, secret) {
const parts = Object.fromEntries(
String(header || "").split(",").map((part) => part.trim().split("="))
);
const t = Number(parts.t);
if (!Number.isInteger(t) || !parts.v1) return false;
if (Math.abs(Date.now() / 1000 - t) > 300) return false; // more than 5 minutes off
const hmac = crypto.createHmac("sha256", secret); // the whole secret, whsec_ included
const expected = hmac.update(parts.t + ".").update(rawBody).digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(parts.v1);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}Answers, retries and turning off
- Answer with any 2xx status within 5 seconds: that means delivered. Answer first, then do slow work. We ignore the rest of your answer and don't follow redirects.
- Any other answer, a timeout or a failed connection is a failed attempt. We try again 1 minute after the first failure, 10 minutes after the second and 1 hour after the third, four attempts in all (the last about 71 minutes after the first), then mark the delivery failed.
- After 20 failed attempts in a row we turn the webhook off and email the project's owner. Turn it back on in Inbox settings once your address answers.
- While the webhook is off we queue nothing, and waiting deliveries are dropped instead of sent. Turning it back on doesn't send what you missed, and a failed or dropped delivery is never sent again. To catch up, use Export CSV in the Inbox: it lists each record as it is now.
- The address must be https on port 443 and on the public internet. We send at most 1,000 deliveries an hour per project and drop the rest.