Webhooks
Receive real-time HTTP notifications when things happen in MotherBot — messages and delivery status, conversations, deals, sequences, coupons, carts, links, number health, template approvals and calls.
How Webhooks Work
When an event occurs (a message is received, a campaign completes, etc.), MotherBot sends an HTTP POST request to your configured endpoint with a JSON payload. Your server should respond with a 200 OK within 10 seconds.Note
Each request times out after 10 seconds. A non-2xx response or a timeout is retried up to 5 attempts with exponential backoff starting at 1 second; after that the delivery is dropped and the failure — with the reason your server gave — is recorded against the endpoint.
Warning
Answer quickly and do the work afterwards. Ten seconds is the whole budget, and an endpoint that processes the event before replying will start timing out under load — which turns one slow handler into five duplicate deliveries.
Setting Up a Webhook
1
Create an endpoint on your server
Your endpoint must be publicly accessible over HTTPS. For local development, use a tunnel like ngrok:
ngrok http 3000
# Copy the https:// forwarding URL2
Register the endpoint in MotherBot
Go to Dashboard → Webhooks → Add Endpoint. Enter your URL, name it, and tick the events you want. You can do the same from the REST API with
POST /api/v1/webhooks, which also returns the full event catalogue onGET /api/v1/webhooks.Warning
The URL must be publicly reachable. Private, loopback and cloud-metadata addresses are rejected when you register, and re-checked before every delivery.
3
Copy the signing secret
MotherBot generates a Signing Secret for each webhook. Store it securely — you'll use it to verify that requests are from MotherBot.
4
Verify the signature
Every webhook request includes an
X-MotherBot-Signature header:const crypto = require("crypto");
function verifyWebhook(payload, signature, secret) {
const expected = crypto
.createHmac("sha256", secret)
.update(payload) // raw request body (string)
.digest("hex");
return `sha256=${expected}` === signature;
}
// In your Express handler:
app.post("/webhook", (req, res) => {
const sig = req.headers["x-motherbot-signature"];
if (!verifyWebhook(req.rawBody, sig, process.env.WEBHOOK_SECRET)) {
return res.status(401).send("Invalid signature");
}
const event = req.body;
// handle event...
res.sendStatus(200);
});Event Types
Every event below is dispatched from real code — there are no placeholders. The same catalogue, with machine-readable keys and descriptions, comes back fromGET /api/v1/webhooks, so you can build your own picker instead of hard-coding a list that will grow.Messages
| Event | Fires when |
|---|---|
| message.received | A customer messaged you — on WhatsApp, Instagram, Messenger or the website widget. |
| message.sent | An outbound message was accepted by WhatsApp. |
| message.delivered | A sent message reached the recipient's phone. |
| message.read | The recipient opened a delivered message. |
| message.failed | A message could not be delivered. The payload carries Meta's own error code and reason. |
Contacts
| Event | Fires when |
|---|---|
| contact.created | A new contact was added — by an inbound message, an import, or the API. |
| contact.updated | A contact's details, tags or custom fields changed. |
| contact.deleted | A contact was deleted. Fires once per contact, including inside a bulk delete — so your own copy can be removed too. |
| contact.enriched | Explorium returned data for a contact — job title, employer, verified work email, company firmographics. Fires only on a successful match, so it is a clean trigger for lead routing or scoring. |
| contact.opted_out | A contact asked to stop hearing from you (a STOP-style keyword, or opt-out set by hand). Marketing to them after this is a compliance problem, so this is the one contact event worth alerting on. |
Conversations & Desk
| Event | Fires when |
|---|---|
| conversation.opened | A new support conversation began — the first inbound message after the last one was resolved. |
| conversation.closed | An agent resolved a conversation. Carries how long it was open, for your own reporting. |
| csat.submitted | A customer answered the satisfaction question after a conversation was resolved. |
| agent.assigned | A conversation was assigned to a team member, by hand or by auto-assignment. |
Broadcasts
| Event | Fires when |
|---|---|
| campaign.started | A broadcast began sending. |
| campaign.completed | A broadcast finished. Carries the final sent/delivered/failed counts. |
| campaign.paused | A running broadcast was paused — by a person, or by Number Health Guardian stepping in to protect the number. |
Sequences
| Event | Fires when |
|---|---|
| sequence.enrolled | A contact entered a drip sequence. |
| sequence.completed | A contact received the last step of a sequence. |
| sequence.exited | A contact left a sequence before the end — usually because they replied, which is the outcome the sequence was for. |
Chatbots & Flows
| Event | Fires when |
|---|---|
| chatbot.triggered | A chatbot session started for a contact. |
| chatbot.completed | A chatbot session ended. |
| flow.submitted | A customer submitted a WhatsApp Flow form. Carries the answers. |
| knowledge_base.document_processed | A document finished indexing into an AI Knowledge Base — ready or errored, with the reason when it failed. |
Deals & Pipeline
| Event | Fires when |
|---|---|
| deal.created | A deal was opened against a contact. |
| deal.stage_changed | A deal moved between pipeline stages. Carries both the old and the new stage. |
| deal.won | A deal was marked won — the event to post into your accounting or fulfilment system. |
| deal.lost | A deal was marked lost, with the reason when one was given. |
Store & Coupons
| Event | Fires when |
|---|---|
| coupon.redeemed | A discount code was redeemed — from your checkout, or recorded through the API. |
| cart.abandoned | A shopper left items in their cart and became eligible for a recovery message. |
| catalog_order.created | A customer submitted WhatsApp's native cart/catalog checkout. Fires on capture, independent of whether it's ever pushed into your store. |
Links & Ads
| Event | Fires when |
|---|---|
| link.clicked | Someone opened one of your trackable links. Carries the ref code, so a click on a specific poster is distinguishable from one on an email. |
| ad.referral | A conversation started from a Click-to-WhatsApp ad, with the ad and headline it came from. |
Number Health
| Event | Fires when |
|---|---|
| number.quality_changed | Meta changed a number's quality rating. A drop to YELLOW is the earliest warning you get before restrictions. |
| number.sending_paused | Guardian paused sending on a number to protect it. Anything you queue while this is in force will not go out. |
Templates
| Event | Fires when |
|---|---|
| template.approved | Meta approved a message template — it can now be used in broadcasts. |
| template.rejected | Meta rejected a template, with the reason they gave. |
Payments
| Event | Fires when |
|---|---|
| payment.received | A WhatsApp payment succeeded. |
| payment.failed | A WhatsApp payment failed or was cancelled. |
Calls
| Event | Fires when |
|---|---|
| call.completed | A WhatsApp call ended, with its duration and direction. |
| call.missed | An inbound call was not answered — the one worth calling somebody back about. |
Scheduling
| Event | Fires when |
|---|---|
| calendly.booking_created | A contact booked (or rescheduled into) a Calendly event type. Does not re-fire for the routing-form step, only a real booking. |
| calendly.booking_canceled | A contact canceled a Calendly booking. Does not fire for the cancel half of a reschedule — that's a booking_created for the new time, not a real cancellation. |
Lead sources
| Event | Fires when |
|---|---|
| indiamart.lead_received | A buyer enquiry arrived from IndiaMART Lead Manager — by real-time push or by the pull sweep, whichever reached it first. Fires once per UNIQUE_QUERY_ID, never for a duplicate, and never for leads collected by a historical import. |
Quotations
| Event | Fires when |
|---|---|
| quotation.created | A quotation was drafted — by a person, by a chatbot, or through the API. Fires while it is still a draft, so it is the hook for pushing a draft into your own system before anyone sees it. |
| quotation.sent | A quotation was put in front of the customer, by email, WhatsApp or a shared link. Carries which channel it went out on. |
| quotation.viewed | The customer opened the quotation link. Fires on every open, not just the first, with the running view count — repeated opens without a decision is the signal to call them. |
| quotation.accepted | The customer accepted, with the name they signed under and the exact time. This is the event to raise an invoice or a work order from. |
| quotation.rejected | The customer declined, with the reason when they gave one. |
| quotation.expired | A quotation passed its valid-until date without a decision. Fires once, from the sweep. |
| quotation.cancelled | Someone on your team withdrew a quotation that had already gone out. |
| quotation.revised | A new version replaced an earlier one. Carries both the old and the new quotation ids, so your copy can be superseded rather than duplicated. |
| quotation.reminded | An automatic follow-up went to a customer who had not responded yet. |
| quotation.approval_requested | A quotation crossed the value or discount threshold and is waiting for a manager to sign off before it can be sent. |
| quotation.approved | A manager approved a quotation that was waiting for sign-off — it can now be sent. |
| quotation.approval_rejected | A manager sent a quotation back instead of approving it, with their comment. |
Payload Format
Every delivery is a POST with this envelope. The event-specific fields are insidepayload, andevent is repeated there so a handler can switch on the object alone.{
"event": "message.received",
"payload": { /* event-specific, see below */ },
"timestamp": "2026-07-27T09:32:11.000Z"
}Two headers travel with it:| Header | Value |
|---|---|
| X-MotherBot-Event | The event key, so you can route without parsing the body |
| X-MotherBot-Signature | sha256=<hex HMAC of the raw body, keyed with your signing secret> — only when a secret is set |
Warning
Verify the signature against the raw request body, before any JSON parsing or re-serialisation. A body that has been parsed and stringified again is a different string, and the HMAC will never match.
message.received delivery:{
"event": "message.received",
"payload": {
"event": "message.received",
"messageId": "665f2a...",
"wamid": "wamid.HBgMOTE5...",
"from": "919876543210",
"contactId": "664a1f...",
"whatsappAccountId": "6640a...",
"type": "text",
"preview": "Hello, I need help with my order",
"timestamp": "2026-07-27T09:32:10.000Z"
},
"timestamp": "2026-07-27T09:32:11.000Z"
}A deal.won delivery — every deal event carries the same deal object, so one handler covers all four:{
"event": "deal.won",
"payload": {
"event": "deal.won",
"dealId": "670c11...",
"title": "Bulk order — 200 units",
"value": 48000,
"currency": "INR",
"pipelineId": "66f0aa...",
"stage": "won",
"status": "won",
"previousStage": "negotiation",
"contactId": "664a1f...",
"contactName": "Asha Patel",
"contactPhoneNumber": "+919876543210",
"ownerId": "6641b...",
"ownerName": "Ravi",
"expectedCloseAt": null,
"timestamp": "2026-07-27T09:40:02.000Z"
},
"timestamp": "2026-07-27T09:40:02.000Z"
}A number.sending_paused delivery — the one worth alerting a human about, because anything queued while it holds will not go out:{
"event": "number.sending_paused",
"payload": {
"event": "number.sending_paused",
"accountId": "66aa12...",
"pausedUntil": "2026-07-28T09:40:02.000Z",
"reason": "Guardian paused sending: Meta withheld 4.2% of "Diwali offer" ...",
"trigger": "circuit_breaker",
"blockRate": 0.042,
"campaignId": "665c3b...",
"timestamp": "2026-07-27T09:40:02.000Z"
},
"timestamp": "2026-07-27T09:40:02.000Z"
}Note
Payload fields are added over time, never renamed or removed. Ignore keys you do not recognise rather than validating strictly against this list.
Testing & Troubleshooting
- Use a tunnel (ngrok, Cloudflare Tunnel) to point an endpoint at your machine while you build the handler.
- Dashboard → Webhooks shows each endpoint's delivered and failed counts, when it last succeeded, and the exact error your server returned on the last failure.
- Pause an endpoint with the Pause button (or isActive: false on the API) while you deploy — deliveries stop, the endpoint keeps its id, stats and its slot against your plan's webhook quota.
- Nothing arriving? Check the endpoint is active, that the event is actually ticked, and that the URL is publicly reachable — a private address is refused at delivery time.
Note
An endpoint can be scoped to one WhatsApp number. When it is, it only receives events for that number — which is how a business running two brands on two numbers keeps their integrations separate.