Webhooks
Get real-time notifications the instant something happens in your BotInbox workspace — new messages, conversation updates, contact changes, and more — delivered straight to your server.
01Overview
Webhooks let BotInbox push events to your server the moment something happens — whereas with the REST API you pull data on your own schedule. Instead of polling for new messages, you register an HTTPS endpoint once and BotInbox calls it in real time with a signed JSON payload for every event you subscribe to.
Typical uses: sync conversations into your CRM, trigger an SMS when a customer replies, mirror contacts into your own database, or kick off internal workflows the second a conversation closes.
02Quick start
- 1Go to
https://botinbox.co.uk/manage/webhooksin your dashboard. - 2Add an endpoint — a public URL on your server that accepts
POSTrequests. HTTPS is required. - 3Select the events you want to receive (or subscribe to
*for all of them). - 4Copy the signing secret — it is shown once, at creation. Store it securely (e.g. an environment variable); you will use it to verify every delivery.
03Request format
Every delivery is an HTTP POST to your endpoint with these headers:
X-BotInbox-Event— the event name (e.g.message.created)X-BotInbox-Signature— hex-encoded HMAC-SHA256 of the raw request body, keyed with your webhook secretContent-Type— alwaysapplication/json
POST /webhooks/botinbox HTTP/1.1
Host: example.com
Content-Type: application/json
X-BotInbox-Event: message.created
X-BotInbox-Signature: 8f7d2f0c1a5e0b9d4c3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1cThe JSON body always has the same envelope — the event name, an event-specific data object, and an ISO-8601 timestamp:
{
"event": "message.created",
"data": {
"message": {
"id": "msg_9f2c1e7a",
"conversationId": "conv_abc123",
"type": "INCOMING",
"body": "Hi — is the 2pm viewing still available?",
"contactId": "ct_xyz789",
"createdAt": "2026-01-01T00:00:00.000Z"
},
"conversationId": "conv_abc123"
},
"timestamp": "2026-01-01T00:00:00.000Z"
}04Event catalog
The data field of the payload depends on the event:
| Event | data payload |
|---|---|
| conversation.created | The conversation object |
| conversation.updated | The conversation object |
| conversation.closed | The conversation object |
| conversation.assigned | The conversation object (assignee in assignedToId) |
| message.created | { message, conversationId } |
| message.updated | { message, conversationId } — fires when a message note is resolved/unresolved or a call recording is attached (there is no free-text message editing) |
| contact.created | The contact object |
| contact.updated | The contact object |
| contact.deleted | { id } |
| channel.created | The channel object |
| channel.updated | The channel object |
* to receive all events — including any added in the future.05Verify the signature
Always verify X-BotInbox-Signature before trusting a delivery — it proves the request really came from BotInbox. Compute HMAC-SHA256 of the raw request body using your webhook secret, hex-encode it, and compare it to the header with a timing-safe comparison.
const crypto = require("crypto")
const express = require("express")
const app = express()
const WEBHOOK_SECRET = process.env.BOTINBOX_WEBHOOK_SECRET
// IMPORTANT: capture the RAW body — not parsed JSON — for signature checking.
app.post(
"/webhooks/botinbox",
express.raw({ type: "application/json" }),
(req, res) => {
const raw = req.body // Buffer of the raw request bytes
const signature = req.header("X-BotInbox-Signature") || ""
const expected = crypto
.createHmac("sha256", WEBHOOK_SECRET)
.update(raw)
.digest("hex")
const ok =
signature.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
if (!ok) return res.status(401).send("bad signature")
const { event, data } = JSON.parse(raw.toString("utf8"))
// Acknowledge first, then process.
res.sendStatus(200)
switch (event) {
case "message.created":
// data.message, data.conversationId
break
case "conversation.closed":
// data is the conversation object
break
// ...handle the events you subscribed to
}
}
)
app.listen(3000)// app/api/webhooks/botinbox/route.ts
import crypto from "crypto"
export async function POST(req: Request) {
const raw = await req.text() // raw body string — required for the signature
const signature = req.headers.get("x-botinbox-signature") || ""
const expected = crypto
.createHmac("sha256", process.env.BOTINBOX_WEBHOOK_SECRET!)
.update(raw)
.digest("hex")
if (
signature.length !== expected.length ||
!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
) {
return new Response("bad signature", { status: 401 })
}
const { event, data } = JSON.parse(raw)
// Handle the event (ideally enqueue it and return immediately).
return new Response("ok", { status: 200 })
}06Retries & acknowledgement
Respond with any 2xx status as quickly as you can to acknowledge the delivery, and do heavy work asynchronously (after responding, or on a queue). Each attempt times out after 10 seconds.
If a delivery fails, BotInbox retries up to 3 attempts in total:
| Attempt | When |
|---|---|
| 1 | Immediately (0s) |
| 2 | ~3 seconds after the first failure |
| 3 | ~10 seconds after the second failure |
Retries happen on connection failures and on 408, 429, or 5xx responses. Any other 4xx is treated as a permanent rejection and is not retried — so only return 4xx if you truly want to drop the event.
07Delivery log & resend
The dashboard records every delivery — the event name, response status, number of attempts, duration, and the full payload — so you can see exactly what was sent and how your endpoint responded.
Missed something during an outage or while debugging? You can resend any past delivery from /manage/webhooks with one click.
08Security
- HTTPS is required — plain HTTP endpoints are rejected.
- Webhook URLs pointing at private, internal, or loopback addresses are blocked.
- Always verify the signature — anyone can POST JSON to a public URL; only BotInbox can sign it with your secret. See Verify the signature.
09Managing via API
You normally create and edit webhooks in the dashboard, but two API endpoints help with automation and debugging:
GET /api/v1/webhooks— list your configured webhooks.POST /api/v1/webhooks/test— body{ "webhookId": "..." }. Sends atest.pingevent to that endpoint, signed withX-BotInbox-Signatureexactly like a real delivery — perfect for verifying your signature check end-to-end.
# List your configured webhooks
curl https://botinbox.co.uk/api/v1/webhooks \
-H "X-API-Key: bi_your_api_key"
# Send a signed test.ping delivery to one of them
curl -X POST https://botinbox.co.uk/api/v1/webhooks/test \
-H "X-API-Key: bi_your_api_key" \
-H "Content-Type: application/json" \
-d '{"webhookId":"wh_abc123"}'For authentication, endpoints, and the rest of the REST API, see the full API reference.
Ready to receive your first event?
Add an endpoint in the dashboard, send yourself a test.ping, and watch it arrive — signed and verified.