Guides
Get a signed webhook on every event
Register an endpoint, verify the signature and timestamp on every call, choose your events, and handle retries and the dead-letter state.
On this page
AI Assistant can call your own system on every event that matters — a conversation starts, a message arrives, a visitor asks for a person, an identity is verified, a tool runs. Each call is a signed HTTPS POST you verify and act on however you like. This guide sets one up end to end.
1. Register an endpoint
In Console → Connections → Webhooks, open Webhooks and add an endpoint. Give it a public https:// address that your server controls, pick the events you want, and save. The platform generates a signing secret and shows it once — copy it now and store it where your server reads its secrets. You will not see it again; if you lose it, rotate the endpoint for a new one.
2. Verify every call
Each delivery carries three headers: X-Busymate-Timestamp (unix seconds), X-Busymate-Signature (t=<timestamp>,v1=<hex>), and X-Busymate-Delivery (a stable id). Recompute the signature as an HMAC-SHA256 over the exact string <timestamp>.<raw request body> using your signing secret, and compare it to the v1 value in constant time — never ===, which leaks timing information a byte at a time. Reject the call if it does not match, and reject it if the timestamp is more than five minutes from your clock — that window stops an old call being replayed.
import { createHmac, timingSafeEqual } from "node:crypto";
// req.rawBody must be the EXACT bytes received — a body a framework has
// already JSON.parse'd and re-stringified will not reproduce the same hash.
function verify(req, signingSecret) {
const timestamp = req.headers["x-busymate-timestamp"];
const header = req.headers["x-busymate-signature"]; // "t=<ts>,v1=<hex>"
if (!timestamp || !header) return false;
const ageSeconds = Math.abs(Date.now() / 1000 - Number(timestamp));
if (!Number.isFinite(ageSeconds) || ageSeconds > 300) return false; // stop a replayed call
const given = header.split(",").find((p) => p.startsWith("v1="))?.slice(3);
if (!given) return false;
const expected = createHmac("sha256", signingSecret)
.update(`${timestamp}.${req.rawBody}`, "utf8")
.digest("hex");
const a = Buffer.from(given, "hex");
const b = Buffer.from(expected, "hex");
return a.length === b.length && timingSafeEqual(a, b);
}import hashlib
import hmac
import time
def verify(headers, raw_body: bytes, signing_secret: str) -> bool:
timestamp = headers.get("x-busymate-timestamp")
header = headers.get("x-busymate-signature") # "t=<ts>,v1=<hex>"
if not timestamp or not header:
return False
if abs(time.time() - float(timestamp)) > 300:
return False # stop a replayed call
given = next((p[3:] for p in header.split(",") if p.startswith("v1=")), None)
if not given:
return False
message = f"{timestamp}.".encode() + raw_body
expected = hmac.new(signing_secret.encode(), message, hashlib.sha256).hexdigest()
return hmac.compare_digest(given, expected)3. Choose your events
Subscribe only to what you use. The available events are conversation.started, message.received, handoff.requested, handoff.resolved, identity.verified, and tool.called; a subscription of * receives all of them. Every payload has the same envelope: the event name, your tenant id, an occurred_at timestamp, and a data object with the ids for that event — a real handoff.requested delivery:
{
"event": "handoff.requested",
"tenant_id": "3d9d9a3e-2a35-4d3c-8a2e-2a6b7c9c2f10",
"occurred_at": "2026-09-18T14:02:31.045Z",
"data": {
"handoff_id": "8f5e2b7e-9a3e-4b13-9f21-6b0a2f7d5c44",
"session_id": "b7e2a3c1-4f6d-4a2e-9c3b-1d2e3f4a5b6c",
"requested_by": "visitor"
}
}4. Send a test event
Use "Send test event" on the endpoint to queue a webhook.test delivery. It is signed exactly like a real one, so it proves your verification code before any real traffic depends on it. If you want to inspect the raw call first, the card offers a hosted capture URL that records what it receives so you can read the exact headers and body.
5. Handle retries and dead letters
Delivery is at-least-once: because a call can be retried, the same X-Busymate-Delivery id may arrive more than once, so treat that id as an idempotency key and ignore a repeat. A call that does not return a 2xx is retried with growing backoff; after the last attempt it moves to a dead-letter state you can see in the same card, with the last status and error. Fix your endpoint, then retry a dead-lettered delivery from there.
| Attempt | Delay since previous | Elapsed |
|---|---|---|
| 1 (first send) | — | 0m |
| 2 | 1m | 1m |
| 3 | 2m | 3m |
| 4 | 4m | 7m |
| 5 | 8m | 15m |
| 6 | 16m | 31m |
| 7 | 32m | 63m |
| 8 (last) | 64m | 127m |
| dead-letter | after attempt 8 fails | — |
Verify
- Confirm the endpoint shows as saved in Connections with your chosen events.
- Send a test event and confirm your server receives a
webhook.testPOST. - Confirm your code accepts the signature and rejects a tampered body.
- Confirm a call whose timestamp is older than five minutes is rejected.
- Point the endpoint at a broken URL, send a test event, and confirm it retries and then shows in the dead-letter state.
Questions
Where do I get the signing secret?
It is generated when you create the endpoint and shown once in that same response. Rotate the endpoint to get a new one; the old secret stops working immediately.
What exactly do I sign to verify a call?
The string
timestamp.body— the value ofX-Busymate-Timestamp, a literal dot, then the raw request body — with HMAC-SHA256 under your signing secret. Compare the hex result to thev1part ofX-Busymate-Signature.Why did I receive the same event twice?
Delivery is at-least-once, so a retry can repeat an event. Use
X-Busymate-Deliveryas an idempotency key and skip a delivery id you have already handled.What happens if my endpoint is down?
The call is retried with exponential backoff. After several failures it is dead-lettered and stops retrying; you can see it, fix your endpoint, and retry it from Connections.
Can I manage endpoints without the Console?
Yes. The same actions are on the MCP server as
set_webhook_endpoint,get_webhook_status,list_webhook_deliveriesand related tools, so an agent or script can manage them too.