01
Register an endpoint#
Requires an account-scoped key. The URL must be a public https:// endpoint; URLs with credentials, internal hostnames, or private/reserved IP addresses are rejected with 400. The response returns a wh_bi_ signing secret exactly once. Optionally pass events (the event types to receive; empty or omitted means every event), domain_id (email.* events only for emails sent from that domain) and a description.
curl https://botinbox.dev/v0/webhooks -X POST -H "Authorization: Bearer $BOTINBOX_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://app.example.com/hooks/email","events":["email.delivered","email.bounced","email.complained"]}'02
Event types#
message.received (mail arrived in an agent inbox), email.sent, email.delivered, email.delivery_delayed, email.bounced, email.complained, email.failed, email.suppressed, email.opened and email.clicked, plus thread.handoff_requested, thread.handoff_resolved and thread.outcome_set for conversation insights (their data carries thread_id, inbox_id, pod_id, outcome and handoff, never message content). The email.* events cover Email API sends and messages sent from agent inboxes. Every payload is { id, type, created_at, data }; email events carry email_id (or message_id and inbox_id for inbox sends), from, to, subject, tags, the email's current status, and the recipient for per-recipient events. An endpoint registered without events receives every type, including ones added later, so check type before acting.
{
"id": "5f0c...",
"type": "email.bounced",
"created_at": "2026-09-27T10:00:04.000Z",
"data": {
"email_id": "em_01j9z3k8q4t7w2x5v6b8n0m1c3",
"from": "Acme <noreply@acme.com>",
"to": ["ada@example.com"],
"subject": "Your receipt",
"tags": [{ "name": "category", "value": "receipt" }],
"status": "bounced",
"recipient": "ada@example.com",
"bounce": { "type": "hard", "sub_type": "NoEmail" },
"occurred_at": "2026-09-27T10:00:04.000Z"
}
}03
Verify signatures#
Each delivery carries x-botinbox-timestamp (unix seconds) and x-botinbox-signature-v2: an HMAC-SHA256 of the timestamp, a dot, and the exact raw body. Recompute it with your secret, compare in constant time, and reject timestamps more than 5 minutes old so a captured request can't be replayed. The older x-botinbox-signature (an HMAC of the body alone) is still sent for existing receivers and will be removed in a later release.
const timestamp = req.headers["x-botinbox-timestamp"];
const expected = "sha256=" + createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(timestamp)) <= 300;
const valid = fresh && timingSafeEqual(
Buffer.from(expected),
Buffer.from(req.headers["x-botinbox-signature-v2"])
);04
Delivery and retries#
Answer with any 2xx within 10 seconds. Otherwise BotInbox retries on this schedule: immediately, then after 5 seconds, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours and 10 hours (8 attempts over about 28 hours). Every attempt sends x-botinbox-event-id (the same on retries, so you can deduplicate) and x-botinbox-attempt. Redirects are not followed, and delivery is refused if the hostname resolves to a private address. GET /v0/webhooks/:id/deliveries shows each delivery's status, attempts and last error.
05
Failing endpoints and replay#
An endpoint that keeps failing for more than a day, across a full round of retries, is turned off automatically and the organization's Owners and Admins get an email. Fix it, turn it back on with PATCH /v0/webhooks/:id and {"active": true}, then replay what it missed: POST /v0/webhooks/:id/replay sends failed deliveries again with the same event ids (all failed ones from the last 7 days, or pass delivery_ids or since).
curl https://botinbox.dev/v0/webhooks/WEBHOOK_ID -X PATCH -H "Authorization: Bearer $BOTINBOX_API_KEY" \
-H "Content-Type: application/json" -d '{"active":true}'
curl https://botinbox.dev/v0/webhooks/WEBHOOK_ID/replay -X POST -H "Authorization: Bearer $BOTINBOX_API_KEY"
# 202 {"replayed":12}