Skip to content

Webhooks

Signed, retried callbacks for received mail and email delivery events, with filters and replay.

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.

Shell
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.

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

TypeScript
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).

Shell
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}