Skip to content
AI Assistant

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.

javascript
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);
}
python
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:

json
{
  "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.

AttemptDelay since previousElapsed
1 (first send)0m
21m1m
32m3m
44m7m
58m15m
616m31m
732m63m
8 (last)64m127m
dead-letterafter attempt 8 fails

Verify

  1. Confirm the endpoint shows as saved in Connections with your chosen events.
  2. Send a test event and confirm your server receives a webhook.test POST.
  3. Confirm your code accepts the signature and rejects a tampered body.
  4. Confirm a call whose timestamp is older than five minutes is rejected.
  5. 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 of X-Busymate-Timestamp, a literal dot, then the raw request body — with HMAC-SHA256 under your signing secret. Compare the hex result to the v1 part of X-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-Delivery as 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_deliveries and related tools, so an agent or script can manage them too.