Developer docs

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

  1. 1Go to https://botinbox.co.uk/manage/webhooks in your dashboard.
  2. 2Add an endpoint — a public URL on your server that accepts POST requests. HTTPS is required.
  3. 3Select the events you want to receive (or subscribe to * for all of them).
  4. 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 secret
  • Content-Type — always application/json
Request headers
POST /webhooks/botinbox HTTP/1.1
Host: example.com
Content-Type: application/json
X-BotInbox-Event: message.created
X-BotInbox-Signature: 8f7d2f0c1a5e0b9d4c3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c

The JSON body always has the same envelope — the event name, an event-specific data object, and an ISO-8601 timestamp:

Example payload — message.created
{
  "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:

Eventdata payload
conversation.createdThe conversation object
conversation.updatedThe conversation object
conversation.closedThe conversation object
conversation.assignedThe 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.createdThe contact object
contact.updatedThe contact object
contact.deleted{ id }
channel.createdThe channel object
channel.updatedThe channel object
Subscribe to * 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.

You must hash the raw bytes. If you parse the JSON and re-serialize it, key order and spacing will differ and the signature will not match. Read the body before (or instead of) any JSON body parser.
Node.js / Express
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)
Next.js (App Router) route handler
// 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:

AttemptWhen
1Immediately (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.

Make your handler idempotent. Because of retries, the same event may arrive more than once — design your processing so a duplicate delivery has no extra effect (e.g. de-duplicate on the resource ID).

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 a test.ping event to that endpoint, signed with X-BotInbox-Signature exactly like a real delivery — perfect for verifying your signature check end-to-end.
cURL
# 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.