Webhook guide
Receive real-time event callbacks from Bookbag: lead.created, conversation.escalated, conversation.replied, action.run, campaign.sent. How to register, the payload shape, and verifying the HMAC signature.
View as MarkdownWebhooks let Bookbag push events to your server the moment they happen — a new lead, an escalation, a campaign send — so you can sync a CRM, alert a channel, or trigger automation. This guide covers the developer workflow; for the dashboard setup screen see Integrations: Webhooks.
Register an endpoint
A webhook is a workspace-level URL plus the list of events it subscribes to and a signing secret. Create one in the dashboard, or via the management endpoint:
| Field | Type | Notes |
|---|---|---|
| url | string | Your HTTPS endpoint. Required. |
| events | array | Event names to subscribe to, or ["*"] for all. Defaults to all. |
| secret | string | Signing secret. If you omit it, Bookbag generates one and returns it. |
curl https://app.bookbag.ai/webhooks \
-H "Content-Type: application/json" \
-d '{ "url": "https://example.com/hooks/bookbag", "events": ["lead.created", "conversation.escalated"] }'Save the returned secret — you need it to verify signatures, and it is how you know a request really came from Bookbag.
Events
Bookbag fires these events. Subscribe to the ones you need, or * for all.
| Event | Fires when | Key payload fields |
|---|---|---|
| lead.created | A lead is captured by an agent. | leadId, agentId, fields |
| conversation.escalated | A conversation is handed off to a human / help desk. | conversationId, target, transcript |
| conversation.replied | The agent posts a reply in a conversation. | conversationId and reply context |
| action.run | An agent action/tool runs. | action run details |
| campaign.sent | An outbound campaign is sent. | campaignId, agentId, recipients, message, channel |
Payload shape
Every delivery is a POST with a JSON body of the form { event, data, ts }. event is the event name, data is the event-specific payload, and ts is the send time in epoch milliseconds.
{
"event": "lead.created",
"data": {
"leadId": 42,
"agentId": 123,
"fields": { "email": "sam@acme.com", "name": "Sam" }
},
"ts": 1717286400000
}Verify the signature
When a secret is set, Bookbag signs each delivery and sends the signature in the X-Bookbag-Signature header as sha256=<hex>. The signature is an HMAC-SHA256 of the raw request body keyed with your secret. Recompute it on your side and compare with a constant-time check — reject the request if it does not match.
import crypto from "node:crypto";
// Read the RAW body — verify before any JSON middleware reparses it.
app.post("/hooks/bookbag",
express.raw({ type: "application/json" }),
(req, res) => {
const raw = req.body; // Buffer of the raw bytes
const header = req.get("X-Bookbag-Signature") || "";
const expected = "sha256=" + crypto
.createHmac("sha256", process.env.BOOKBAG_WEBHOOK_SECRET)
.update(raw)
.digest("hex");
const ok =
header.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(header), Buffer.from(expected));
if (!ok) return res.status(401).end();
const { event, data } = JSON.parse(raw.toString("utf8"));
// ... handle the event ...
res.status(200).end();
}
);Compute the HMAC over the exact bytes received, before any framework re-serializes the JSON. Re-stringifying can reorder keys or change whitespace and break the signature.
Best practices
- Respond fast. Return
2xxquickly and process asynchronously; deliveries are best-effort. - Be idempotent. Treat handlers as at-least-once; de-duplicate on the natural id in
data(e.g.leadId). - Always verify. Reject any request whose signature does not match your secret.
- Use HTTPS. Bookbag only delivers to reachable, safe URLs.