Skip to content

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, and change.type says how: status (from, to), followUp (to, null when cleared), note (body), edit (keys, the corrected fields) or opt_out (the person asked for no marketing). Deleting a record sends no event.
  • test: the Send test button, with entity, record and change null.

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
}
  • fields holds what the person sent, by your form's field names, with the consent record (and marketingConsentWithdrawnAt after an opt-out).
  • source says where the person came from, each part only when known: the channel (site, widget, sdk, mcp, or owner for one you added; more may come), the landing page, referrerHost and campaign tags.
  • summary, source, status and followUpAt can 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 can change.from on a first status change. Read them defensively.
  • X-BranderUX-Event names the event. X-BranderUX-Delivery is the same on every retry of a delivery, so you can skip one you already handled.

Check the signature

  • X-BranderUX-Signature reads t=<unix seconds>,v1=<hex>. Split it on the comma into t, when we signed, and v1.
  • 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.