API Reference
Manage contacts and segments, send messages, broadcast templates, run campaigns and drip sequences, issue coupons that work at your Shopify or WooCommerce checkout, track deals and products, and receive every event as a webhook — over one REST API.
Base URL & authentication
Every endpoint lives under /api/v1 and takes a Bearer key, created under Settings → API Keys in your dashboard.
https://motherbot.io/api/v1
Authorization: Bearer motherbot_<your_api_key>Keys are scoped to one organization and cannot reach another’s data. The raw key is shown once at creation — only a SHA-256 hash is stored, so there is no way to recover it afterwards. Never ship one in client-side code or a public repository; revoke and reissue from the same screen if one leaks.
Key permissions & channels
A key carries two independent restrictions, both chosen when you create it. Permissions decide which endpoints it may call — messages:send, contacts:read, broadcasts:write and so on, one per resource and direction. Channels decide which messaging channels it may send on and read from.
Give a key only what its integration actually calls. A key handed to a spreadsheet add-on or an agency to send order updates has no reason to be able to rewrite your deal pipeline, and a leaked read-only key is an incident rather than a business.
Five named roles cover the common cases in one click — Full access, Messaging (send and broadcast, no CRM data), Send only (a notification service or an order-update script), Read only (dashboards and exports), and CRM sync (contacts, deals and products, but no sending). Every role is just a preset over the same checkboxes, so you can start from one and adjust.
- A call the key lacks the permission for answers
403withinsufficient_scope, anddetails.requiredScopenames the one it needed. - A channel the key is not allowed on answers
403withchannel_not_allowed, anddetails.allowedChannelslists the ones it may use. - Leaving the channel list empty allows every channel, including ones you connect later.
- Keys created before permissions existed keep full access and keep working. Narrow one at any time from Settings → API Keys — no re-issue needed.
An API key is an organisationcredential — its scopes are the ones set on the key itself, not a team member’s. Staff permissions and the per-member access scopes apply to the dashboard, the mobile app and the browser extensions, and are documented under Team Access. A refusal there answers 403 with forbidden rather than insufficient_scope, which is how you can tell the two apart in a log.
GET /api/v1/channels and GET /api/v1/usage are deliberately exempt from both. An integration that has just been refused has to be able to ask what it can do without needing a wider key to ask the question with.
Choosing a sender and a channel
Start at GET /api/v1/channels. It returns every sending identity you have — WhatsApp numbers, SMS/RCS/email connections, Telegram, LINE, Viber, Messenger, Instagram and the website widget — each with an id and a capabilities block.
Pass that id as accountId to POST /messages, POST /messages/bulk or POST /campaigns/send to choose the number a message leaves on, and pass channel to choose the channel. Both default to what they always were: WhatsApp, from your default number.
Two things are refused rather than worked around, on purpose. An accountIdthat isn’t yours or isn’t connected is an error, never a quiet fall back to your default number — a message on the wrong number costs that number’s quality rating and the response would look like a success. And a message type the channel cannot carry is an error, never a degrade to plain text — if you asked for buttons, a reply-less message that reports success is a broken flow nobody notices. capabilities tells you which is which before you send.
Sending to thousands of contacts
Never hold a request open while thousands of messages go out. Two endpoints accept a large list, queue the work and answer immediately:
POST /messages/bulk— up to 100,000 recipients, each with its own variables and header. Batches of 250 or fewer are sent inline and return207with per-recipient results; anything larger returns202with abatchId. PollGET /messages/bulk/:batchId.POST /campaigns/send— the same thing framed as a broadcast, with a name and a pause/resume control. Always queued. PollGET /campaigns/:id/stats.
Branch on the mode field ("sync" or "async") rather than on the status code, and force either path with mode in the request. Asking for mode: "sync" above 250 recipients is refused rather than accepted and left to time out.
The split is about what can actually work. Ten thousand messages sent during one request is several minutes of open connection: it ends at a gateway timeout, the messages have already gone, you cannot tell which, and retrying — the obvious reaction to a 504— sends them all again. Queued, the same work runs on the worker with per-second pacing against the number’s real WhatsApp budget, automatic retries, and an idempotency guard that stops a redelivered job sending twice. Recipients that can never send — an unusable number, a duplicate, a blocked or opted-out contact — are still reported in the immediate response rather than left for you to discover by polling.
Dynamic template headers
A template’s header is dynamic in two ways, and both are settable per message: a TEXT header’s {{n}} placeholders, and a mediaheader’s link — the image, video or document the customer receives. That is what lets each person get their own invoice PDF or ticket image instead of one shared banner.
Send values, not a components array. headerMediaUrl takes a public URL and headerVariablestakes a TEXT header’s values; which one applies is decided by the template’s approved definition, not by which field you used — so you never need to know whether a header is IMAGE or TEXT, and a template edited from one to the other does not break your integration. Omit the value on a media header and the sample the template was approved with is used.
On the fan-out endpoints the same fields exist per recipient: recipients[].headerMediaUrl and recipients[].headerVariables on POST /messages/bulk and POST /campaigns/send. A recipient without one falls back to the shared value per position, so a two-variable header can have one value that differs per person and one fixed for the whole send.
Links are checked before anything is sent. WhatsApp fetches a media URL itself, so a blank, a spreadsheet #N/A or a Drive share page comes back as an opaque rejection with the send already charged for. Instead: POST /messages answers 400, POST /messages/bulk marks that one recipient skipped with the reason and sends the rest, and POST /campaigns/send refuses the whole request naming the offending recipient — before a single job is queued.
The same fields and the same rules apply everywhere a template is sent — the Chrome extension, the Google Sheets and Excel add-ins, the Zoho CRM widget, the WordPress plugin, broadcasts, and chatbot flows. In an extension the value is usually a mapped column or CRM field; in a chatbot it is a {{variable}}, so each conversation can carry its own header image or text.
A header’s {{n}} numbering is its own. Meta counts each component’s parameters independently, so a header’s {{1}} is not the body’s {{1}} — they are separate slots and mixing them is the most common cause of a #132000 Number of parameters does not match rejection.
Postman collection
Every endpoint on this page, plus a request for each webhook event, as a collection you can run. Example bodies, documented query parameters and saved example responses are already filled in.
Prefer to import it yourself, or want the version that is always current? These URLs are generated from the live API on every request, so re-importing one picks up endpoints added since — where a fork, like any downloaded copy, stays as it was when you took it.
Collection https://motherbot.io/postman/motherbot-api.postman_collection.json
Environment https://motherbot.io/postman/motherbot-api.postman_environment.jsonIn Postman choose Import → Link and paste the collection URL, then repeat for the environment.
- Select the MotherBot — Production environment and set
apiKey. Auth is set on the collection, so you set the key once and every request inherits it. - Send Usage & Plan first — it tells you which folders your plan can actually use, before a
403does. - Requests that create something save the new id into a collection variable (
contactId,campaignId,couponId, …), so the requests below them run without pasting ids by hand. - The Webhook Events folder fires each event at your own endpoint, signed with
webhookSecret, so you can test a receiver before subscribing to anything.
Rate limits
Enforced per API key, per minute, by plan.
| Starter | 60 req/min |
| Growth | 300 req/min |
| Enterprise | 1,200 req/min |
POST /messages/bulk and POST /broadcasts/send each draw on a separate bucket set to a tenth of the plan limit, so a broadcast cannot exhaust the allowance ordinary reads depend on. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset; exceeding the limit returns 429.
Response format & errors
Everything is JSON. Successes carry success: true and a data payload; list endpoints add a pagination object. Failures are uniform:
{
"success": false,
"error": "error_code",
"message": "Human-readable description",
"details": {}
}| Code | error | Meaning |
|---|---|---|
| 400 | invalid_request | Missing or malformed parameter |
| 400 | invalid_template | Template exists but is not APPROVED |
| 400 | invalid_state | Resource is in the wrong state for the action |
| 400 | no_whatsapp_account | No connected WhatsApp account found |
| 400 | no_channel_account | That channel has no connected account — see GET /channels |
| 400 | unsupported_for_channel | That channel cannot carry that message type — see GET /channels |
| 401 | unauthorized | Missing, invalid, or revoked API key |
| 402 | plan_limit_reached | Quota full — details carry the numbers |
| 403 | feature_not_available | Your plan does not include this feature |
| 403 | insufficient_scope | This key lacks the permission the endpoint needs — details.requiredScope names it |
| 403 | channel_not_allowed | This key is restricted to other channels — details.allowedChannels lists them |
| 404 | not_found | Resource does not exist |
| 409 | already_exists | A resource with that unique id already exists |
| 413 | file_too_large | Upload exceeds the size limit |
| 415 | unsupported_media_type | File type not accepted, or contents don't match it |
| 429 | rate_limit_exceeded | Too many requests |
| 502 | upstream_error | WhatsApp / Meta API returned an error |
Plan limits & feature access
Two refusals mean two different things and need different handling. A 403 feature_not_available means the feature is not on the plan at all — stop calling the endpoint; the customer has to upgrade. A 402 plan_limit_reached means it is there but the quota is full — retry next cycle, or free a slot. A 402 carries the numbers, so your integration can say exactly what ran out:
{
"success": false,
"error": "plan_limit_reached",
"message": "Webhook limit reached (5/5). Upgrade your plan to add more.",
"details": { "limitKey": "webhooks", "limit": 5, "current": 5 }
}Quotas are checked on create only — you can always read what is using one up. Call GET /usage before a long run: it returns every quota, how much is used and every feature flag, so you can size a batch instead of discovering the ceiling halfway through an import.
Analytics
Get analytics summary
Returns aggregate message, contact, and broadcast statistics for a rolling time window. Delivery rate and read rate are computed percentages.
Query parameters
days | integer | Number of days to look back from todayDefault 30Allowed 1–90 |
Request
curl "https://motherbot.io/api/v1/analytics/summary?days=7" \
-H "Authorization: Bearer motherbot_xxx"Response
{
"success": true,
"data": {
"period": {
"days": 7,
"from": "2026-06-17T00:00:00.000Z",
"to": "2026-06-24T00:00:00.000Z"
},
"messages": {
"total": 4821,
"sent": 3200,
"received": 1621,
"delivered": 3050,
"read": 2400,
"failed": 150,
"deliveryRate": 95,
"readRate": 78
},
"contacts": { "total": 12400 },
"campaigns": {
"total": 5,
"draft": 1,
"scheduled": 1,
"running": 1,
"paused": 0,
"completed": 2,
"failed": 0
}
}
}Channels
List connected channels
Every sending identity your organisation has, across every channel — WhatsApp numbers, SMS/RCS/email provider connections, and the chatbot channels (Telegram, LINE, Viber, Messenger, Instagram, Website Chat). Start here: the `id` of a row is the `accountId` you pass to `POST /messages`, `POST /messages/bulk` and `POST /campaigns/send`, and `capabilities` tells you which controls to draw. `capabilities` is derived from what the send layer can genuinely do for that channel and provider, so a capability listed here will not fail at send time, and one that is absent will be refused with `unsupported_for_channel`. This endpoint needs only a valid API key — no plan feature. An integration that has just been told a channel is unavailable has to be able to ask what IS available.
Query parameters
channel | string | Only this channelAllowed whatsapp | messenger | instagram | telegram | line | viber | webchat | sms | rcs | email |
status | string | Connection state to returnDefault connectedAllowed connected | disconnected | pending | error | flagged | all |
Request
curl "https://motherbot.io/api/v1/channels" \
-H "Authorization: Bearer motherbot_xxx"
# Just the WhatsApp numbers
curl "https://motherbot.io/api/v1/channels?channel=whatsapp" \
-H "Authorization: Bearer motherbot_xxx"Response
{
"success": true,
"data": [
{
"id": "66f1c2a0d3b4e5f6a7b8c9d0",
"channel": "whatsapp",
"provider": "meta",
"label": "Acme Retail",
"identifier": "919876543210",
"isDefault": true,
"status": "connected",
"capabilities": {
"template": true,
"text": true,
"media": ["image", "video", "document", "audio"],
"interactive": ["button", "list", "cta_url", "flow", "location_request"]
}
},
{
"id": "66f1c2a0d3b4e5f6a7b8c9d1",
"channel": "sms",
"provider": "msg91",
"label": "MSG91 — ACMERT",
"identifier": "ACMERT",
"isDefault": false,
"status": "connected",
"capabilities": { "template": false, "text": true, "media": [], "interactive": [] }
}
]
}| 200 | Channel list returned (an empty array when nothing is connected) |
| 400 | Unknown `channel` or `status` |
| 401 | Missing or invalid API key |
Contacts
List contacts
Returns a paginated list of contacts, across every channel. Supports filtering by channel and tag, and partial-match search across name, phone number, and email. Every row carries `channel` and `channelId`. `channelId` is that channel's own id for the person — the phone number on whatsapp/sms/rcs, the address on email, the chat or scoped id elsewhere. It is the same value as the legacy `waId` field, under a name that isn't a lie about a Telegram contact.
Query parameters
page | integer | Page numberDefault 1 |
limit | integer | Results per pageDefault 20Allowed 1–100 |
channel | string | Only contacts on this channel. Omit for every channel.Allowed whatsapp | messenger | instagram | telegram | line | viber | webchat | sms | rcs | email |
tag | string | Filter by tag (exact match) |
search | string | Partial match on name, phone, or email |
Request
curl "https://motherbot.io/api/v1/contacts?tag=vip&limit=50" \
-H "Authorization: Bearer motherbot_xxx"Response
{
"success": true,
"data": [
{
"_id": "664a1f...",
"channel": "whatsapp",
"channelId": "919876543210",
"phoneNumber": "919876543210",
"waId": "919876543210",
"name": "John Doe",
"email": "john@example.com",
"tags": ["vip", "mumbai"],
"optedIn": true,
"blocked": false,
"customFields": { "cf1": "Acme Ltd", "cf2": "gold" },
"source": "api",
"createdAt": "2026-05-01T10:00:00.000Z"
}
],
"pagination": {
"page": 1, "limit": 50, "total": 284, "pages": 6
}
}Create / upsert contact
Creates a new contact or merges with an existing one on the same channel. Name, email, and tags are updated on match; customFields are merged. Identity is `(organisation, channel, id)`. On whatsapp/sms/rcs that id is `phoneNumber`; on every other channel it is `channelId` — that channel's own id for the person. A Telegram contact has no phone number, and requiring one made those people impossible to create.
Body
phoneNumberrequired | string | Phone number with country code, no + (e.g. 919876543210). Required on whatsapp, sms and rcs; optional elsewhere, where it is stored as a detail rather than as the identity. |
channel | string | Which channel this contact is reachable onDefault whatsappAllowed whatsapp | messenger | instagram | telegram | line | viber | webchat | sms | rcs | email |
channelId | string | The channel's own id for the person — required on every channel except whatsapp/sms/rcs. An email address on email; a chat, scoped or visitor id elsewhere. |
name | string | Full display name |
firstName | string | First name |
lastName | string | Last name |
email | string | Email address |
tags | string[] | Labels to attach (merged with existing tags) |
customFields | object | Custom field values. Use the numbered slots — cf1…cf10 — which are what the contact screen, `{{contact.cf1}}` in a template and `{{cf1}}` in a chatbot all read. You may also send a field's LABEL as the key ("Company Name") and it is written into that slot for you. Any other key is stored as given and is available as `{{contact.<key>}}`, but will not appear on the contact screen. |
source | string | Origin label for this contactDefault api |
Request
curl -X POST "https://motherbot.io/api/v1/contacts" \
-H "Authorization: Bearer motherbot_xxx" \
-H "Content-Type: application/json" \
-d '{
"phoneNumber": "919876543210",
"name": "Priya Sharma",
"email": "priya@example.com",
"tags": ["new-user", "delhi"],
"customFields": { "cf1": "Acme Ltd", "cf2": "gold" }
}'
# A Telegram contact — the chat id is the identity, and there is no phone number
curl -X POST "https://motherbot.io/api/v1/contacts" \
-H "Authorization: Bearer motherbot_xxx" \
-H "Content-Type: application/json" \
-d '{
"channel": "telegram",
"channelId": "5551234567",
"name": "Priya Sharma",
"tags": ["telegram"]
}'Response
{
"success": true,
"data": {
"_id": "664a1f...",
"channel": "whatsapp",
"channelId": "919876543210",
"phoneNumber": "919876543210",
"waId": "919876543210",
"name": "Priya Sharma",
"email": "priya@example.com",
"tags": ["new-user", "delhi"],
"customFields": { "cf1": "Acme Ltd", "cf2": "gold" },
"optedIn": true,
"blocked": false,
"source": "api",
"createdAt": "2026-06-24T09:00:00.000Z"
}
}Get contact
Fetches a single contact by its MongoDB _id.
Path parameters
idrequired | string | Contact _id |
Request
curl "https://motherbot.io/api/v1/contacts/664a1f..." \
-H "Authorization: Bearer motherbot_xxx"Response
{
"success": true,
"data": {
"_id": "664a1f...",
"phoneNumber": "919876543210",
"name": "Priya Sharma",
"tags": ["vip", "delhi"],
"optedIn": true,
"blocked": false,
"customFields": { "cf1": "Acme Ltd", "cf2": "gold" },
"createdAt": "2026-06-24T09:00:00.000Z",
"updatedAt": "2026-06-24T09:00:00.000Z"
}
}Update contact
Partially updates a contact. Only the fields you include in the body are changed; omitted fields are left untouched. Sending firstName and/or lastName without a name rebuilds the display name from them. Sending phoneNumber moves the contact to that number and carries its conversation history across.
Path parameters
idrequired | string | Contact _id |
Body
name | string | Full name. Wins over a name rebuilt from firstName/lastName in the same request. |
firstName | string | First name. Rebuilds 'name' unless you send 'name' too. |
lastName | string | Last name. Rebuilds 'name' unless you send 'name' too. |
phoneNumber | string | Move this contact to a new number, in E.164 (+919876543210). The conversation history moves with it. Rejected with 400 if the number isn't one we can message. |
email | string | Email address |
tags | string[] | Replaces the full tag list |
customFields | object | Replaces all custom fields |
notes | string | Free-text internal notes |
optedIn | boolean | Marketing opt-in status |
blocked | boolean | Block contact from receiving messages |
language | string | ISO 639-1 language code |
country | string | ISO 3166-1 alpha-2 country code |
city | string | City name |
Request
curl -X PATCH "https://motherbot.io/api/v1/contacts/664a1f..." \
-H "Authorization: Bearer motherbot_xxx" \
-H "Content-Type: application/json" \
-d '{ "tags": ["vip", "delhi"], "optedIn": false }'Response
{
"success": true,
"data": {
"_id": "664a1f...",
"name": "Priya Sharma",
"tags": ["vip", "delhi"],
"optedIn": false,
"updatedAt": "2026-06-24T10:00:00.000Z"
}
}Delete contact
Permanently deletes a contact. This action is irreversible.
Path parameters
idrequired | string | Contact _id |
Request
curl -X DELETE "https://motherbot.io/api/v1/contacts/664a1f..." \
-H "Authorization: Bearer motherbot_xxx"Response
{
"success": true,
"message": "Contact deleted"
}| 200 | Contact deleted |
| 404 | Contact not found |
Messages
List message history
Every outbound and inbound message, newest first — the log to reconcile against when you need to prove what was sent and what became of it. `source` tells you which part of the product sent it, so you can separate campaign traffic from chatbot, sequence or agent replies.
Query parameters
contactId | string | Only this contact's thread |
channel | string | Only messages that travelled on this channelAllowed whatsapp | messenger | instagram | telegram | line | viber | webchat | sms | rcs | email |
direction | string | Filter by directionAllowed inbound | outbound |
status | string | Filter by delivery statusAllowed sent | delivered | read | failed |
source | string | Which part of the product sent itAllowed campaign | sequence | chatbot | live_chat | api | coupon | … |
since | string | ISO date — inclusive lower bound |
until | string | ISO date — inclusive upper bound |
page | integer | Page numberDefault 1 |
limit | integer | Results per pageDefault 20Allowed 1–100 |
Request
curl "https://motherbot.io/api/v1/messages?direction=outbound&status=failed&limit=50" \
-H "Authorization: Bearer motherbot_xxx"Response
{
"success": true,
"data": [
{
"_id": "6650...",
"contact": "6650f1a2b3c4d5e6f7a8b9c0",
"waId": "919876543210",
"direction": "outbound",
"type": "template",
"status": "delivered",
"source": "campaign",
"wamid": "wamid.HBgM...",
"timestamp": "2026-07-23T09:14:02.000Z",
"sentAt": "2026-07-23T09:14:02.000Z",
"deliveredAt": "2026-07-23T09:14:05.000Z"
}
],
"pagination": { "page": 1, "limit": 50, "total": 1482, "pages": 30 }
}Send message
Sends a single message to one contact, on any connected channel. The recipient does not need to exist beforehand — an unknown number or id is added to your contacts automatically and the response tells you whether it was created. Every field except `to` is optional, and with all of them omitted this behaves exactly as it always has: a text or template message on WhatsApp, from your default number. **Choosing a sender** — pass `accountId` from `GET /channels` to send from a specific number or account. An id that isn't yours, or isn't connected, is a 400 naming which: it is never silently swapped for your default number. **Choosing a channel** — pass `channel` (default `whatsapp`). A message type the channel cannot carry is refused with `unsupported_for_channel` rather than being degraded to plain text: if you asked for buttons, a reply-less message that reports success is a broken flow nobody notices. `GET /channels` tells you what each one carries. **Limits are checked here, before Meta sees them** — at most 3 reply buttons with titles of 20 characters, at most 10 list sections and 10 rows in total with row titles of 24 characters. Meta's own rejection names a component index rather than the problem, and by then the conversation has been charged for.
Body
torequired | string | The recipient. A phone number in international format, digits only (e.g. 919876543210) on whatsapp/sms/rcs — '+', spaces and dashes are accepted and stripped. An email address on email; the channel's own id (Telegram chat id, Instagram-scoped id, widget visitor id) elsewhere. Created as a contact if new. |
channel | string | Which channel to send on. Must be connected — see GET /channels.Default whatsappAllowed whatsapp | sms | rcs | email | telegram | line | viber | messenger | instagram | webchat |
accountId | string | Which connected account to send from — an `id` from GET /channels. Defaults to your default account for that channel. An account that isn't yours or isn't connected is refused, never swapped. |
type | string | Message typeDefault textAllowed text, template, image, video, document, audio, interactive, location, reaction |
text.body | string | Message body (required when type is text, max 4096 chars) |
subject | string | Email only — the subject line. Defaults to "A message from us". |
template.name | string | Approved template name (required when type is template) |
template.language.code | string | Template language code e.g. en_US (required when type is template) |
template.components | array | The raw Meta components array, if you would rather build it yourself. Sent verbatim — only the copy-code button of an AUTHENTICATION (OTP) template is filled in for you. Mutually exclusive with the value fields below: send one style or the other. |
template.variables | object | string[] | Body values keyed by placeholder number ("1", "2", …) or positionally. Filled to exactly the count the APPROVED template takes — extras are dropped, missing ones sent blank — so a stale mapping can't fail the send. |
template.headerMediaUrl | string | A public http(s) URL for an IMAGE / VIDEO / DOCUMENT header — dynamic per message, so each customer can get their own invoice, ticket or product shot. Checked before the send: an unusable value is a 400 here rather than an opaque rejection from Meta after you have been charged. Omit it and the sample the template was approved with is used. |
template.headerVariables | string[] | The same slot for a TEXT header: one value per `{{n}}`, in placeholder order. A header's numbering is its OWN — Meta counts each component's parameters independently, so these are not the body's. Which shape your header is comes from the approved definition, not from which field you use: `headerVariables: [url]` and `headerMediaUrl: url` are the same thing. |
template.buttonVariables | string[] | One value per dynamic URL button, in the order those buttons appear on the template. |
template.buttonVariablesByIndex | object | Button values keyed by the button's real index among all the template's buttons. Takes precedence over `buttonVariables`, and is the unambiguous shape when quick-reply buttons sit between the URL ones. |
template.couponCode | string | Overrides the coupon on a COPY_CODE button. Defaults to the one the template was approved with. |
template.offerExpiresAt | string | ISO time a LIMITED_TIME_OFFER template's countdown expires. |
image | video | document | audio | object | Media, under a key named for the type — Meta's own shape. Takes `link` (a public https URL) or `id` (a media id from POST /media), plus `caption` on image/video/document. `document` also takes `filename`; omit it and we derive one from the URL. |
interactive.type | string | Which interactive message to send (required when type is interactive)Allowed button | list | cta_url | flow | location_request |
interactive.header | object | `{ "type": "text", "text": "…" }` or a media header, e.g. `{ "type": "video", "video": { "link": "https://…" } }`. Max 60 characters of text. |
interactive.body.text | string | The message body. Required for every interactive type. |
interactive.footer.text | string | Small print under the message, max 60 characters. |
interactive.action.buttons | array | For `button`: 1–3 objects of `{ id, title }` (Meta's `{ type: "reply", reply: {…} }` is accepted too). Titles max 20 characters; ids must be unique — they are what comes back when the customer taps. |
interactive.action.button | string | For `list`: the label on the button that opens the list, max 20 characters.Default Choose |
interactive.action.sections | array | For `list`: up to 10 sections of `{ title, rows: [{ id, title, description }] }`, and at most 10 rows across ALL sections. Row titles max 24 characters, descriptions max 72. |
interactive.action.displayText | string | For `cta_url`: the button label, max 20 characters. |
interactive.action.url | string | For `cta_url`: the https URL the button opens. |
interactive.action.flowId | string | For `flow`: the WhatsApp Flow to open. `flowToken`, `flowCta` and `screen` are optional. |
location.latitude | number | For type location: -90 to 90. `longitude` (-180 to 180) is required with it; `name` and `address` are optional. |
reaction.message_id | string | For type reaction: the `wamid` of the message being reacted to. `reaction.emoji` is the emoji — send an empty string to remove a reaction. WhatsApp only. |
Request
# Text message
curl -X POST "https://motherbot.io/api/v1/messages" \
-H "Authorization: Bearer motherbot_xxx" \
-H "Content-Type: application/json" \
-d '{
"to": "919876543210",
"type": "text",
"text": { "body": "Hello! Your order has shipped." }
}'
# Template message with body variables
curl -X POST "https://motherbot.io/api/v1/messages" \
-H "Authorization: Bearer motherbot_xxx" \
-H "Content-Type: application/json" \
-d '{
"to": "919876543210",
"type": "template",
"template": {
"name": "order_confirmation",
"language": { "code": "en_US" },
"components": [
{
"type": "body",
"parameters": [
{ "type": "text", "text": "John" },
{ "type": "text", "text": "ORDER-9912" }
]
}
]
}
}'
# Authentication (OTP) template — the code goes in the body only;
# the copy-code button parameter WhatsApp requires is added for you.
curl -X POST "https://motherbot.io/api/v1/messages" \
-H "Authorization: Bearer motherbot_xxx" \
-H "Content-Type: application/json" \
-d '{
"to": "919876543210",
"type": "template",
"template": {
"name": "otp_login",
"language": { "code": "en" },
"components": [
{ "type": "body", "parameters": [{ "type": "text", "text": "483920" }] }
]
}
}'
# Template with a DYNAMIC header — this customer's own invoice, not a shared banner.
# Send values, not a components array: the payload is built from the approved
# definition, so you don't need to know whether the header is IMAGE or TEXT.
curl -X POST "https://motherbot.io/api/v1/messages" \
-H "Authorization: Bearer motherbot_xxx" \
-H "Content-Type: application/json" \
-d '{
"to": "919876543210",
"type": "template",
"template": {
"name": "invoice_ready",
"language": { "code": "en_US" },
"headerMediaUrl": "https://cdn.mystore.com/invoices/9912.pdf",
"variables": { "1": "Priya", "2": "ORDER-9912" },
"buttonVariablesByIndex": { "0": "TRACK-9912" }
}
}'
# A TEXT header's {{1}} is dynamic the same way
curl -X POST "https://motherbot.io/api/v1/messages" \
-H "Authorization: Bearer motherbot_xxx" \
-H "Content-Type: application/json" \
-d '{
"to": "919876543210",
"type": "template",
"template": {
"name": "ticket_update",
"language": { "code": "en_US" },
"headerVariables": ["#9912"],
"variables": ["Priya"]
}
}'
# From a SPECIFIC number — accountId comes from GET /api/v1/channels
curl -X POST "https://motherbot.io/api/v1/messages" \
-H "Authorization: Bearer motherbot_xxx" \
-H "Content-Type: application/json" \
-d '{
"to": "919876543210",
"accountId": "66f1c2a0d3b4e5f6a7b8c9d0",
"type": "text",
"text": { "body": "Sent from our support number." }
}'
# Video with a caption (image, audio and document take the same shape)
curl -X POST "https://motherbot.io/api/v1/messages" \
-H "Authorization: Bearer motherbot_xxx" \
-H "Content-Type: application/json" \
-d '{
"to": "919876543210",
"type": "video",
"video": {
"link": "https://cdn.example.com/a.mp4",
"caption": "Your video is ready"
}
}'
# Reply buttons, with a video header
curl -X POST "https://motherbot.io/api/v1/messages" \
-H "Authorization: Bearer motherbot_xxx" \
-H "Content-Type: application/json" \
-d '{
"to": "919876543210",
"type": "interactive",
"interactive": {
"type": "button",
"header": { "type": "video", "video": { "link": "https://cdn.example.com/a.mp4" } },
"body": { "text": "Your video is ready, Priya." },
"footer": { "text": "Acme Retail" },
"action": {
"buttons": [
{ "id": "yes", "title": "Book a call" },
{ "id": "no", "title": "Not now" }
]
}
}
}'
# List picker — at most 10 sections, and 10 rows across all of them
curl -X POST "https://motherbot.io/api/v1/messages" \
-H "Authorization: Bearer motherbot_xxx" \
-H "Content-Type: application/json" \
-d '{
"to": "919876543210",
"type": "interactive",
"interactive": {
"type": "list",
"body": { "text": "Pick a slot" },
"action": {
"button": "Choose",
"sections": [
{
"title": "Tomorrow",
"rows": [ { "id": "10", "title": "10:00", "description": "45 min" } ]
}
]
}
}
}'
# URL button
curl -X POST "https://motherbot.io/api/v1/messages" \
-H "Authorization: Bearer motherbot_xxx" \
-H "Content-Type: application/json" \
-d '{
"to": "919876543210",
"type": "interactive",
"interactive": {
"type": "cta_url",
"body": { "text": "Your video is ready" },
"action": { "displayText": "Watch it", "url": "https://cdn.example.com/a.mp4" }
}
}'
# Another channel entirely — Telegram (chat id as 'to'), and email
curl -X POST "https://motherbot.io/api/v1/messages" \
-H "Authorization: Bearer motherbot_xxx" \
-H "Content-Type: application/json" \
-d '{
"to": "5551234567",
"channel": "telegram",
"type": "text",
"text": { "body": "Your order has shipped." }
}'
curl -X POST "https://motherbot.io/api/v1/messages" \
-H "Authorization: Bearer motherbot_xxx" \
-H "Content-Type: application/json" \
-d '{
"to": "priya@example.com",
"channel": "email",
"subject": "Your order has shipped",
"text": { "body": "Tracking: ABC123" }
}'Response
{
"success": true,
"data": {
"messageId": "wamid.HBgNOTE5...",
"to": "919876543210",
"status": "sent",
"channel": "whatsapp",
"accountId": "66f1c2a0d3b4e5f6a7b8c9d0",
"contactId": "6650f1a2b3c4d5e6f7a8b9c0",
"contactCreated": false
}
}| 201 | Message accepted by the channel |
| 400 | Invalid payload, unusable 'to', or an accountId that isn't yours / isn't connected |
| 400 | unsupported_for_channel — that channel cannot carry that message type |
| 402 | Monthly message limit reached |
| 403 | Contact is blocked or has opted out, or the key is not allowed on this channel |
| 502 | The channel rejected the send — see `details.upstream` |
Bulk send template
Sends a WhatsApp template to up to 100,000 recipients. Each recipient can carry its own body variables, header value and button values. Numbers you have never messaged are added to your contacts automatically; unusable numbers, duplicates and contacts who are blocked or opted out come back as skipped without failing the rest. **Small batches send inline; large batches are queued.** Up to 250 recipients the messages are sent during the request and you get `207` with a per-recipient `results` array, exactly as before. Above that the request is accepted, the work is queued, and you get `202` immediately with a `batchId` — poll `GET /api/v1/messages/bulk/:batchId` for progress. Check the `mode` field (`"sync"` or `"async"`) rather than the status code. This split is not a preference. A 10,000-recipient batch sent inline is several minutes of held-open connection: it ends at a gateway timeout, the messages have already gone, you have no idea which, and retrying — the obvious reaction to a 504 — sends them all again. Queued, the work runs on the same machinery broadcasts use, with per-second pacing against the number's real Meta budget, retries, and an idempotency guard that stops a redelivered job sending twice. Force either path with `mode`. `mode: "sync"` above 250 recipients is refused rather than accepted and timed out.
Body
templateNamerequired | string | Template name exactly as stored (must be APPROVED) |
languageCoderequired | string | Template language code e.g. en_US |
accountId | string | Which WhatsApp number to send from — an `id` from GET /channels. Defaults to your default number. An account that isn't yours or isn't connected is refused, never swapped; a template that doesn't belong to it is refused too, before the batch runs. |
recipientsrequired | array | Array of recipient objectsAllowed 1–100,000 items |
mode | string | `auto` sends batches of 250 or fewer inline (207) and queues anything larger (202). `sync` forces the inline path and is refused above 250. `async` queues any size, including a single recipient.Default autoAllowed auto | sync | async |
rateLimitPerSecond | number | Queued path only — messages per second. Defaults to what this number's Meta messaging tier can safely absorb, which is almost always the right answer; the worker re-checks the real budget before every send regardless.Allowed 1–80 |
recipients[].torequired | string | Recipient phone number in international format, digits only. Created as a contact if new. |
recipients[].variables | object | Body variable values keyed by placeholder number ("1", "2", …). Filled to exactly the count the approved template takes — extras are dropped, missing ones sent blank — so a stale mapping can't fail the send. |
recipients[].headerMediaUrl | string | This recipient's OWN header media — their invoice, their ticket, their product shot. Falls back to the shared `headerVariables` when absent. An unusable link is reported as `skipped` for that recipient with the reason, rather than failing the batch or costing a send. |
recipients[].headerVariables | string[] | The same slot for a TEXT header: this recipient's `{{n}}` values. Falls back to the shared list **per position**, so a two-variable header can have one value that differs per recipient and one fixed for the batch. |
recipients[].buttonVariablesByIndex | object | This recipient's dynamic URL button values, keyed by the button's real index. |
headerVariables | string[] | Shared header values for all recipients: the media URL for an image/video/document header, or the values for a TEXT header's {{n}}. The template's approved header format decides which. Omit for a media header to reuse the sample it was approved with. Overridden per position by `recipients[].headerVariables`. |
buttonVariables | string[] | One value per dynamic URL button, in the order those buttons appear on the template. Each is placed at that button's real index, so a quick-reply button ahead of it doesn't shift the position. Copy-code, catalogue and carousel templates need nothing here — their parameters are filled from the approved definition. |
Request
curl -X POST "https://motherbot.io/api/v1/messages/bulk" \
-H "Authorization: Bearer motherbot_xxx" \
-H "Content-Type: application/json" \
-d '{
"templateName": "order_confirmation",
"languageCode": "en_US",
"headerVariables": ["https://cdn.mystore.com/banner.jpg"],
"buttonVariables": ["https://track.mystore.com/orders/"],
"recipients": [
{
"to": "919876543210",
"variables": { "1": "John", "2": "ORDER-9912", "3": "₹599" }
},
{
"to": "918765432109",
"variables": { "1": "Priya", "2": "ORDER-9913", "3": "₹299" }
}
]
}'Response
// 250 recipients or fewer — sent inline (HTTP 207)
{
"success": true,
"mode": "sync",
"channel": "whatsapp",
"accountId": "66f1c2a0d3b4e5f6a7b8c9d0",
"summary": {
"total": 3,
"sent": 2,
"failed": 0,
"skipped": 1
},
"results": [
{ "to": "919876543210", "status": "sent", "messageId": "wamid.AAA..." },
{ "to": "918765432109", "status": "sent", "messageId": "wamid.BBB..." },
{ "to": "910000000000", "status": "skipped", "error": "Contact has opted out" }
]
}
// More than 250 — accepted and queued (HTTP 202), back in milliseconds
{
"success": true,
"mode": "async",
"channel": "whatsapp",
"accountId": "66f1c2a0d3b4e5f6a7b8c9d0",
"batchId": "66a2b1c0d3b4e5f6a7b8c9d0",
"status": "queued",
"statusUrl": "/api/v1/messages/bulk/66a2b1c0d3b4e5f6a7b8c9d0",
"summary": { "total": 10000, "queued": 9987, "skipped": 13 },
"results": [
{ "to": "910000000000", "status": "skipped", "error": "Contact has opted out" }
],
"rateLimitPerSecond": 10,
"etaSeconds": 999,
"etaText": "~17 minutes"
}| 202 | Accepted and queued — poll `statusUrl` for per-recipient results |
| 207 | Sent inline — check each result's status field |
| 400 | invalid_template — template not APPROVED or not found |
| 400 | `mode: "sync"` above 250 recipients — use the queued path |
| 429 | Bulk rate limit exceeded |
Bulk send status
How a queued bulk send is getting on: the running totals and every recipient's outcome, including the delivery timestamps the inline path can never report because it answers before WhatsApp has said anything. Poll this with the `batchId` from a `202`. Needs `messages:read` — a key that can send a bulk message can read what happened to it without also holding the broadcast permission.
Path parameters
batchIdrequired | string | The `batchId` returned by POST /api/v1/messages/bulk |
Query parameters
status | string | Only recipients in this state — `status=failed` is the one to poll for a retry listAllowed queued | sent | delivered | read | failed |
page | integer | Page numberDefault 1 |
limit | integer | Recipients per pageDefault 100Allowed 1–1000 |
Request
curl "https://motherbot.io/api/v1/messages/bulk/66a2b1c0d3b4e5f6a7b8c9d0" \
-H "Authorization: Bearer motherbot_xxx"
# Just the ones that failed
curl "https://motherbot.io/api/v1/messages/bulk/66a2b1c0d3b4e5f6a7b8c9d0?status=failed&limit=1000" \
-H "Authorization: Bearer motherbot_xxx"Response
{
"success": true,
"data": {
"batchId": "66a2b1c0d3b4e5f6a7b8c9d0",
"status": "running",
"accountId": "66f1c2a0d3b4e5f6a7b8c9d0",
"summary": {
"total": 9987, "queued": 4102, "sent": 5885,
"delivered": 5210, "read": 3840, "failed": 0
},
"rateLimitPerSecond": 10,
"etaSeconds": 410,
"startedAt": "2026-08-16T09:00:00.000Z",
"completedAt": null,
"recipients": [
{
"to": "919876543210",
"name": "Priya Sharma",
"status": "read",
"messageId": "wamid.AAA...",
"sentAt": "2026-08-16T09:00:01.000Z",
"deliveredAt": "2026-08-16T09:00:03.000Z",
"readAt": "2026-08-16T09:04:11.000Z"
}
]
},
"pagination": { "page": 1, "limit": 100, "total": 9987, "pages": 100 }
}| 200 | Batch status returned |
| 404 | No such batch in this organization |
Templates
List templates
Returns your WhatsApp message templates with optional filtering by status, category, and language. Use this to discover which templates are approved and ready to use in messages or bulk sends. Templates are approved **per WhatsApp number**, and each row carries the `accountId` it belongs to. Pass `accountId` to list only the templates for the number a customer picked — sending a template from a number it wasn't approved on is rejected by Meta, once per recipient.
Query parameters
accountId | string | Only templates belonging to this WhatsApp account — an `id` from GET /channels |
status | string | Filter by Meta approval statusAllowed APPROVED, PENDING, REJECTED, DISABLED, DRAFT |
category | string | Filter by template categoryAllowed AUTHENTICATION, MARKETING, UTILITY |
language | string | Filter by language code e.g. en_US, hi, ar |
search | string | Partial match on template name (case-insensitive) |
page | integer | Page numberDefault 1 |
limit | integer | Results per pageDefault 20Allowed 1–100 |
Request
curl "https://motherbot.io/api/v1/templates?status=APPROVED&category=MARKETING" \
-H "Authorization: Bearer motherbot_xxx"
# Only the templates for one number
curl "https://motherbot.io/api/v1/templates?accountId=66f1c2a0d3b4e5f6a7b8c9d0&status=APPROVED" \
-H "Authorization: Bearer motherbot_xxx"Response
{
"success": true,
"data": [
{
"_id": "665b2a...",
"accountId": "66f1c2a0d3b4e5f6a7b8c9d0",
"metaTemplateId": "1234567890",
"name": "order_confirmation",
"category": "MARKETING",
"language": "en_US",
"status": "APPROVED",
"variables": ["customer_name", "order_id", "amount"],
"usageCount": 4821,
"components": [
{ "type": "HEADER", "format": "IMAGE" },
{ "type": "BODY", "text": "Hi {{1}}, your order {{2}} of {{3}} is confirmed!" },
{ "type": "FOOTER", "text": "Reply STOP to unsubscribe" },
{
"type": "BUTTONS",
"buttons": [{ "type": "URL", "text": "Track Order", "url": "https://mystore.com/track/{{1}}" }]
}
],
"createdAt": "2026-04-10T08:00:00.000Z",
"updatedAt": "2026-06-01T12:00:00.000Z"
}
],
"pagination": { "page": 1, "limit": 20, "total": 12, "pages": 1 }
}Broadcasts
List broadcasts
Returns a paginated list of broadcasts with their delivery stats and template info.
Query parameters
status | string | Filter by broadcast statusAllowed draft, scheduled, running, paused, completed, failed |
page | integer | Page numberDefault 1 |
limit | integer | Results per pageDefault 20Allowed 1–100 |
Request
curl "https://motherbot.io/api/v1/campaigns?status=completed" \
-H "Authorization: Bearer motherbot_xxx"Response
{
"success": true,
"data": [
{
"_id": "665c3b...",
"name": "June Sale Blast",
"status": "completed",
"audienceType": "all",
"audienceCount": 9800,
"template": { "_id": "665b2a...", "name": "order_confirmation", "category": "MARKETING" },
"stats": {
"total": 9800, "sent": 9750, "delivered": 9600,
"read": 7200, "failed": 50, "replied": 320
},
"startedAt": "2026-06-01T09:00:01.000Z",
"completedAt": "2026-06-01T09:14:22.000Z"
}
],
"pagination": { "page": 1, "limit": 20, "total": 1, "pages": 1 }
}Create broadcast
Creates a new broadcast as a draft or schedules it for a future time. The broadcast is not sent immediately. Only contacts with optedIn: true and blocked: false are counted in the audience.
Body
namerequired | string | Broadcast display name |
templateIdrequired | string | _id of an APPROVED template |
audienceType | string | Audience selection strategyDefault allAllowed all, tags |
audienceTags | string[] | Required when audienceType is tags |
scheduledAt | date-time string | When to send. Write it as YYYY-MM-DD HH:MM:SS and it is read in your organisation's timezone (Settings → Organisation) — '2026-09-01 09:00:00' means nine in the morning where your business is. A string ending in Z or an offset is taken as that exact instant instead. Omit to save as a draft. Must be in the future and no more than 90 days ahead. |
description | string | Internal description |
Request
curl -X POST "https://motherbot.io/api/v1/campaigns" \
-H "Authorization: Bearer motherbot_xxx" \
-H "Content-Type: application/json" \
-d '{
"name": "Diwali Offer 2026",
"templateId": "665b2a...",
"audienceType": "tags",
"audienceTags": ["opted-in", "delhi"],
"scheduledAt": "2026-10-20 09:00:00"
}'Response
{
"success": true,
"data": {
"_id": "665d4c...",
"name": "Diwali Offer 2026",
"status": "scheduled",
"audienceType": "tags",
"audienceTags": ["opted-in", "delhi"],
"audienceCount": 3200,
"scheduledAt": "2026-10-20T09:00:00.000Z",
"stats": { "total": 3200, "sent": 0, "delivered": 0, "read": 0, "failed": 0 },
"createdAt": "2026-06-24T10:30:00.000Z"
}
}Broadcast send (inline recipients)
Creates and immediately launches a broadcast campaign. Recipients are provided inline — no pre-registration in your contacts list is needed. Use commonVariables for values shared by every recipient; only include per-recipient overrides in recipients[].variables. Merged result per recipient: { ...commonVariables, ...recipient.variables }. Up to 100,000 recipients per request. Duplicate phone numbers are silently deduplicated. The campaign appears in the dashboard with a Source badge of 'API'.
Body
name | string | Campaign name — auto-generated from template name and date if omitted |
templateId | string | Template MongoDB _id (use this or templateName + languageCode) |
templateName | string | Template name — requires languageCode |
languageCode | string | Language code e.g. en_US, hi — required when using templateName |
commonVariables | object | Body variable values shared by all recipients e.g. { "1": "MotherBot", "3": "support@example.com" } |
headerMediaUrl | string | Public URL for an IMAGE / VIDEO / DOCUMENT header, shared by every recipient. Overridden by `recipients[].headerMediaUrl`. Every link is validated BEFORE any job is queued — one unusable value refuses the whole request naming which recipient, rather than failing 40,000 sends one at a time after the broadcast has started. |
headerVariables | string[] | The same slot for a TEXT header: values for the header's `{{n}}`, shared by every recipient and overridden per position by `recipients[].headerVariables`. A TEXT header used to get no parameter at all from this endpoint, which rejected every recipient with #132000. |
recipients[].headerMediaUrl | string | This recipient's OWN header media — their invoice or ticket rather than one banner for the whole broadcast. |
recipients[].headerVariables | string[] | This recipient's TEXT header values, falling back per position to the campaign-wide list. |
rateLimitPerSecond | number | Messages per secondDefault 3Allowed 1–80 |
accountId | string | Which WhatsApp number to broadcast from — an `id` from GET /channels. Defaults to your default number. An account that isn't yours or isn't connected is refused, never swapped: a broadcast on the wrong number damages that number's quality rating, and the response would not show it. |
channel | string | Accepted for symmetry with POST /messages, but a broadcast sends an approved WhatsApp template — any other channel is refused with `unsupported_for_channel`. Use POST /messages to reach contacts elsewhere.Default whatsapp |
recipientsrequired | array | Recipient list — no contact pre-registration neededAllowed 1–100,000 items |
recipients[].torequired | string | Phone number with country code, no + prefix (e.g. 919876543210) |
recipients[].name | string | Recipient name stored in the broadcast message log for reporting |
recipients[].variables | object | Per-recipient variable overrides — merged on top of commonVariables |
Request
curl -X POST "https://motherbot.io/api/v1/campaigns/send" \
-H "Authorization: Bearer motherbot_xxx" \
-H "Content-Type: application/json" \
-d '{
"name": "June Order Dispatch",
"templateName": "order_confirmation",
"languageCode": "en_US",
"commonVariables": {
"1": "MotherBot Store",
"3": "support@mystore.com"
},
"headerMediaUrl": "https://cdn.mystore.com/banner.jpg",
"rateLimitPerSecond": 10,
"recipients": [
{
"to": "919876543210",
"name": "John Smith",
"variables": { "2": "ORDER-9912" }
},
{
"to": "918765432109",
"name": "Priya Sharma",
"variables": { "2": "ORDER-9913" }
},
{
"to": "917654321098",
"name": "Ravi Kumar"
}
]
}'Response
{
"success": true,
"data": {
"campaignId": "665d4c...",
"status": "running",
"total": 3,
"channel": "whatsapp",
"accountId": "66f1c2a0d3b4e5f6a7b8c9d0",
"rateLimitPerSecond": 10,
"etaSeconds": 1,
"etaText": "~1 second"
}
}| 201 | Broadcast created and running — use campaignId to poll stats |
| 400 | invalid_template — template not found, not APPROVED, or not on the account you chose |
| 400 | no_whatsapp_account — no connected WhatsApp account, or the accountId isn't yours |
| 400 | unsupported_for_channel — broadcasts are WhatsApp templates only |
| 429 | Broadcast rate limit exceeded |
Get broadcast stats
Returns live delivery statistics for a specific broadcast, including source (platform or api), who launched it, and estimated WhatsApp conversation cost. Billable count = sent + delivered + read messages. Estimated cost = billableCount × price per conversation for the template category.
Path parameters
idrequired | string | Broadcast _id |
Request
curl "https://motherbot.io/api/v1/campaigns/665c3b.../stats" \
-H "Authorization: Bearer motherbot_xxx"Response
{
"success": true,
"data": {
"_id": "665c3b...",
"name": "June Sale Blast",
"status": "completed",
"source": "api",
"audienceCount": 9800,
"stats": {
"total": 9800, "queued": 0, "sent": 9750,
"delivered": 9600, "read": 7200,
"failed": 50, "replied": 320, "clicked": 480, "optedOut": 12
},
"cost": {
"pricePerConversation": 0.78,
"billableCount": 9750,
"estimatedCost": 7605.00,
"currency": "INR",
"templateCategory": "MARKETING"
},
"startedAt": "2026-06-01T09:00:01.000Z",
"completedAt": "2026-06-01T09:14:22.000Z"
}
}Pause broadcast
Pauses a broadcast that is currently running or scheduled. Returns 400 invalid_state if the broadcast is in any other status.
Path parameters
idrequired | string | Broadcast _id |
Request
curl -X POST "https://motherbot.io/api/v1/campaigns/665c3b.../pause" \
-H "Authorization: Bearer motherbot_xxx"Response
{
"success": true,
"data": { "id": "665c3b...", "status": "paused" }
}| 200 | Broadcast paused |
| 400 | invalid_state — broadcast is not running or scheduled |
Resume broadcast
Resumes a paused campaign. Returns 400 invalid_state if the campaign is not in paused status.
Path parameters
idrequired | string | Broadcast _id |
Request
curl -X POST "https://motherbot.io/api/v1/campaigns/665c3b.../resume" \
-H "Authorization: Bearer motherbot_xxx"Response
{
"success": true,
"data": { "id": "665c3b...", "status": "running" }
}| 200 | Broadcast resumed |
| 400 | invalid_state — broadcast is not paused |
Segments
List saved segments
A segment stores RULES, never a member list, so it re-evaluates every time it is used. Use a segment's id as segmentId when launching a broadcast, enrolling a sequence, or sending a coupon.
Query parameters
count | string | Pass 1 to also resolve each segment's current size (one count query per segment)Allowed 1 |
Request
curl "https://motherbot.io/api/v1/segments?count=1" \
-H "Authorization: Bearer motherbot_xxx"Response
{
"success": true,
"data": [
{
"id": "6651a...",
"name": "Lapsed buyers",
"description": "No order in 90 days",
"match": "all",
"ruleCount": 3,
"count": 412,
"updatedAt": "2026-07-20T11:02:00.000Z"
}
]
}Sequences
List sequences
Every drip sequence with its step count, entry rule and stats.
Request
curl "https://motherbot.io/api/v1/sequences" \
-H "Authorization: Bearer motherbot_xxx"Response
{
"success": true,
"data": [
{ "id": "6653d...", "name": "Trial nudges", "status": "active", "stepCount": 3,
"stats": { "enrolled": 420, "completed": 180, "exited": 60, "sent": 940, "failed": 2 } }
]
}Create a sequence
Always created as a DRAFT — activate it from the dashboard once the steps look right, so an API typo can never start messaging customers immediately. Every {{n}} in a step's template must appear in templateVariableMapping, or Meta rejects the send.
Body
namerequired | string | Sequence name |
whatsappAccountIdrequired | string | Number the sequence sends from |
entry | object | { type: "manual" | "segment" | "tags" | "ad", segmentId?, tags?, adSourceId? } |
stepsrequired | array | Up to 20 steps: { templateId, delayValue, delayUnit, templateVariableMapping, headerMediaUrl? } |
stopOnReply | boolean | Exit the sequence the moment the customer repliesDefault true |
Request
curl -X POST "https://motherbot.io/api/v1/sequences" \
-H "Authorization: Bearer motherbot_xxx" \
-H "Content-Type: application/json" \
-d '{
"name": "Trial nudges",
"whatsappAccountId": "6640a...",
"entry": { "type": "manual" },
"steps": [
{ "templateId": "6642b...", "delayValue": 0, "delayUnit": "minutes",
"templateVariableMapping": { "1": "{{contact.firstName}}" } },
{ "templateId": "6642c...", "delayValue": 2, "delayUnit": "days",
"templateVariableMapping": { "1": "{{contact.firstName}}", "2": "TRIAL20" } }
],
"stopOnReply": true
}'Response
{
"success": true,
"data": { "id": "6653d...", "status": "draft" }
}| 201 | Sequence created as a draft |
| 400 | invalid_request — unapproved template, bad delay, or >20 steps |
Enroll contacts
Pass exactly one selector. Only opted-in, non-blocked contacts are enrolled and anyone already in the sequence is skipped — so calling this repeatedly from an automation is safe.
Path parameters
idrequired | string | Sequence ID |
Body
tag | string | Everyone carrying this tag |
segmentId | string | Everyone in this saved segment |
contactIds | array | Explicit contact IDs |
Request
curl -X POST "https://motherbot.io/api/v1/sequences/6653d.../enroll" \
-H "Authorization: Bearer motherbot_xxx" \
-H "Content-Type: application/json" \
-d '{ "tag": "trial" }'Response
{
"success": true,
"data": { "enrolled": 128, "skipped": 12 }
}Per-recipient progress
Who is in the sequence, which step they are on, what sent and what failed — the detail the stats summary cannot give you.
Path parameters
idrequired | string | Sequence ID |
Query parameters
status | string | Filter by enrollment statusAllowed active | completed | exited | failed |
Request
curl "https://motherbot.io/api/v1/sequences/6653d.../enrollments?status=failed" \
-H "Authorization: Bearer motherbot_xxx"Response
{
"success": true,
"data": {
"totalSteps": 3,
"rows": [
{ "contactId": "6650...", "name": "Priya", "phone": "+919876543210",
"status": "active", "step": 2, "totalSteps": 3, "sentCount": 1,
"nextRunAt": "2026-07-25T09:00:00.000Z" }
]
}
}Coupons
List coupons
Every coupon with its issued and redeemed counts.
Request
curl "https://motherbot.io/api/v1/coupons" \
-H "Authorization: Bearer motherbot_xxx"Response
{
"success": true,
"data": [
{ "_id": "6654e...", "name": "Diwali 25", "kind": "shared", "code": "DIWALI25",
"discountType": "percent", "discountValue": 25,
"issuedCount": 380, "redeemedCount": 112, "isActive": true }
]
}Create a coupon
A "shared" coupon is one code everyone gets (needs code). A "unique" coupon mints a distinct code per person on issue — the only way a one-use-each promise is actually enforceable.
Body
namerequired | string | Internal name (unique per org) |
kind | string | One code for all, or one per personDefault sharedAllowed shared | unique |
code | string | Required when kind is shared |
discountType | string | Discount shapeDefault percentAllowed percent | amount | free_shipping | custom |
discountValue | number | 20 for 20% or ₹20 |
expiresAt | string | ISO date |
maxRedemptions | integer | Total allowed across everyone (0 = unlimited)Default 0 |
maxPerContact | integer | Redemptions allowed per contactDefault 1 |
Request
curl -X POST "https://motherbot.io/api/v1/coupons" \
-H "Authorization: Bearer motherbot_xxx" \
-H "Content-Type: application/json" \
-d '{ "name": "Diwali 25", "kind": "shared", "code": "DIWALI25",
"discountType": "percent", "discountValue": 25, "maxPerContact": 1 }'Response
{
"success": true,
"data": { "id": "6654e...", "name": "Diwali 25", "kind": "shared", "code": "DIWALI25" }
}| 201 | Coupon created |
| 409 | duplicate — a coupon with that name or code already exists |
Issue & send over WhatsApp
Issues the coupon to an audience AND delivers it. It must travel inside an approved template — nothing else clears the 24-hour window for a promotional send — so name the template and which variable carries the code. Uses the broadcast rate-limit bucket (1/10th of your plan limit per call).
Path parameters
idrequired | string | Coupon ID |
Body
contactIds | string[] | Audience — explicit contact IDs. Pass exactly one of contactIds, tag or segmentId. |
tag | string | Audience — every contact carrying this tag. Pass exactly one of contactIds, tag or segmentId. |
segmentId | string | Audience — every contact in this segment. Pass exactly one of contactIds, tag or segmentId. |
templateNamerequired | string | An APPROVED template with at least one variable |
templateLanguage | string | Language code to send the template in, when it is approved in more than one.Default the template's own language |
codeVarIndexrequired | integer | Which {{n}} receives the coupon code |
variables | object | Values for the template's OTHER variables, e.g. { "1": "{{contact.firstName}}" } |
whatsappAccountId | string | Number to send from (defaults to your primary) |
headerMediaUrl | string | Required when the template's header is an image, video or document — the public URL to put in it. Named only in prose before this. |
Request
curl -X POST "https://motherbot.io/api/v1/coupons/6654e.../send" \
-H "Authorization: Bearer motherbot_xxx" \
-H "Content-Type: application/json" \
-d '{ "segmentId": "6651a...", "templateName": "diwali_offer",
"codeVarIndex": 2, "variables": { "1": "{{contact.firstName}}" } }'Response
{
"success": true,
"data": { "sent": 380, "failed": 2, "skipped": 0, "total": 382 }
}Push the code to the store (or pull it back)
Creates the coupon inside the connected Shopify or WooCommerce store so it actually discounts at checkout. Idempotent — a coupon already in the store answers without touching it — so this is what you call after fixing whatever the store objected to. Removing a `unique` coupon's codes is bounded per call; the response reports `remaining`, call again to continue.
Path parameters
idrequired | string | Coupon ID |
Body
action | string | Put the code into the store, or take it outDefault syncAllowed sync | remove |
Request
curl -X POST "https://motherbot.io/api/v1/coupons/6654e.../sync" \
-H "Authorization: Bearer motherbot_xxx" \
-H "Content-Type: application/json" \
-d '{ "action": "sync" }'Response
{
"success": true,
"data": { "externalId": "gid://shopify/DiscountCodeNode/14...", "code": "DIWALI25", "adopted": false }
}List redemptions
The orders behind the counters — order number, order value, who redeemed, and whether it came from a store checkout, the API, or a person at a till. That is the difference between "47 redemptions" and knowing whether the promotion paid for itself. Newest first.
Path parameters
idrequired | string | Coupon ID |
Query parameters
limit | integer | How many redemptions to returnDefault 100Allowed 1–500 |
Request
curl "https://motherbot.io/api/v1/coupons/6654e.../redemptions?limit=200" \
-H "Authorization: Bearer motherbot_xxx"Response
{
"success": true,
"data": [
{
"_id": "6712aa...",
"code": "DIWALI25",
"reference": "order_10482",
"orderNumber": "#10482",
"source": "shopify",
"amount": 2499,
"currency": "INR",
"contactId": "664a1f...",
"contactName": "Priya Sharma",
"waId": "919876543210",
"redeemedAt": "2026-07-11T14:20:31.000Z"
}
]
}Redeem (or issue) a code
THE single place expiry, the global cap and the per-contact cap are enforced, so a storefront can call this and trust the answer. Pass contactId — without it a shared code's per-customer limit cannot be enforced. Use action:"issue" to hand a coupon to one contact without sending it.
Body
coderequired | string | The code the customer entered (omit when action is issue) |
contactId | string | Who is redeeming — required for per-contact limits to mean anything |
reference | string | Your order id, stored on the redemption |
action | string | Set to "issue" with couponId + contactId + waId to mint a code without sending itAllowed issue |
couponId | string | The coupon to mint from. Required when action is "issue"; ignored otherwise. |
waId | string | The recipient's WhatsApp id, in international format. Required when action is "issue". |
Request
curl -X POST "https://motherbot.io/api/v1/coupons/redeem" \
-H "Authorization: Bearer motherbot_xxx" \
-H "Content-Type: application/json" \
-d '{ "code": "DIWALI25", "contactId": "6650...", "reference": "order_10482" }'Response
{
"success": true,
"data": { "couponId": "6654e...", "discountType": "percent", "discountValue": 25 }
}| 200 | Redeemed |
| 400 | invalid_request — expired, already used, or cap reached |
Links & QR
List trackable links
Every link with clicks, unique clicks and — for WhatsApp links — how many actually started a chat. Clicks flatter; chats are the number worth reporting.
Request
curl "https://motherbot.io/api/v1/links" \
-H "Authorization: Bearer motherbot_xxx"Response
{
"success": true,
"data": [
{ "_id": "6655f...", "name": "Diwali poster", "kind": "whatsapp",
"url": "https://motherbot.io/r/diwali-poster-a7k2", "refCode": "A7K2",
"clicks": 1840, "uniqueClicks": 1502, "conversations": 311, "isActive": true }
]
}Create a trackable link
kind "whatsapp" opens a chat with one of your numbers and prefills a hidden ref code so the resulting CHAT is attributed; kind "url" is a plain trackable short link. A print-ready QR for any link is at /api/growth/links/:id/qr?size=1024.
Body
namerequired | string | Where you're publishing it, e.g. "Diwali poster" |
kind | string | Opens a WhatsApp chat, or redirects to a URLDefault whatsappAllowed whatsapp | url |
whatsappAccountId | string | Required when kind is whatsapp |
prefillText | string | The first message the customer sends (they can edit it) |
destinationUrl | string | Required when kind is url |
utm | object | { source, medium, broadcast, content, term } appended to destinationUrl |
Request
curl -X POST "https://motherbot.io/api/v1/links" \
-H "Authorization: Bearer motherbot_xxx" \
-H "Content-Type: application/json" \
-d '{ "name": "Diwali poster", "kind": "whatsapp",
"whatsappAccountId": "6640a...", "prefillText": "Hi! I saw the Diwali poster." }'Response
{
"success": true,
"data": { "_id": "6655f...", "slug": "diwali-poster-a7k2",
"url": "https://motherbot.io/r/diwali-poster-a7k2", "refCode": "A7K2" }
}Deals & Pipeline
Every endpoint here needs the salesPipeline feature on the plan.
List sales boards and their stages
Stage keys are per-organisation — renaming a stage in the dashboard changes what a valid `stage` value is. Read them here rather than hard-coding them.
Requires the salesPipeline feature — without it this returns 403.
Request
curl "https://motherbot.io/api/v1/pipelines" \
-H "Authorization: Bearer motherbot_xxx"Response
{
"success": true,
"data": [
{ "id": "66f0a...", "name": "Sales", "currency": "INR", "isDefault": true,
"stages": [
{ "key": "new", "name": "New", "order": 0, "isWon": false, "isLost": false },
{ "key": "won", "name": "Won", "order": 4, "isWon": true, "isLost": false }
] }
]
}List deals
A flat, filtered list rather than the dashboard's board — an integration wants rows to sync, not columns to draw.
Requires the salesPipeline feature — without it this returns 403.
Query parameters
pipelineId | string | Only deals on this board |
stage | string | Stage key from GET /pipelines |
status | string | Filter by outcomeAllowed open | won | lost |
contactId | string | Every deal for one contact |
ownerId | string | Deals owned by one team member |
page | integer | Page numberDefault 1 |
limit | integer | Max 100Default 20 |
Request
curl "https://motherbot.io/api/v1/deals?status=open&limit=50" \
-H "Authorization: Bearer motherbot_xxx"Response
{
"success": true,
"data": [
{ "id": "670c1...", "title": "Bulk order — 200 units", "value": 48000, "currency": "INR",
"pipelineId": "66f0a...", "stage": "qualified", "status": "open",
"contact": { "id": "664a1...", "name": "Asha Patel", "waId": "919406537037" },
"ageInStage": 3 }
],
"pagination": { "page": 1, "limit": 50, "total": 12, "pages": 1 }
}Open a deal
A deal always belongs to a contact. Omit `stage` and it lands in the first stage — which is what a new enquiry is. An unknown stage key is a 400 that lists the valid ones rather than silently using the default.
Requires the salesPipeline feature — without it this returns 403.
Body
contactIdrequired | string | The contact this deal is for |
title | string | Defaults to the contact's name |
value | number | Deal value |
currency | string | Defaults to the pipeline's currency |
pipelineId | string | Defaults to your default board |
stage | string | Stage key; defaults to the first stage |
ownerId | string | Team member who owns it |
expectedCloseAt | string | ISO date |
notes | string | Free text |
source | string | Where the deal came from — stored on the record and shown in Pipeline reporting.Default api |
Request
curl -X POST "https://motherbot.io/api/v1/deals" \
-H "Authorization: Bearer motherbot_xxx" \
-H "Content-Type: application/json" \
-d '{ "contactId": "664a1...", "title": "Bulk order — 200 units",
"value": 48000, "stage": "qualified" }'Response
{ "success": true, "data": { "id": "670c1...", "stage": "qualified", "status": "open" } }Get a deal
One deal with its contact and owner resolved to names, so a card can be rendered without a second lookup.
Requires the salesPipeline feature — without it this returns 403.
Path parameters
idrequired | string | Deal ID |
Request
curl "https://motherbot.io/api/v1/deals/670c11..." \
-H "Authorization: Bearer motherbot_xxx"Response
{
"success": true,
"data": {
"id": "670c11...",
"title": "Bulk order — 200 units",
"value": 48000,
"currency": "INR",
"stage": "qualified",
"status": "open",
"contactId": "664a1f...",
"contactName": "Priya Sharma",
"ownerName": "Ankit",
"expectedCloseAt": "2026-08-15T00:00:00.000Z",
"updatedAt": "2026-07-20T11:02:00.000Z"
}
}| 200 | OK |
| 404 | not_found — no such deal in your organization |
Update, move or close a deal
`stage` moves the card — landing in a Won or Lost column sets the status and close date, and moving back out reopens it. `status` does the reverse: it closes the deal AND moves it to the matching column, so a won deal is never left sitting in Negotiation.
Requires the salesPipeline feature — without it this returns 403.
Path parameters
idrequired | string | Deal ID |
Body
stage | string | Move the card to this stage |
status | string | Close or reopen the dealAllowed open | won | lost |
lostReason | string | Why it was lost |
title | string | Rename |
value | number | New value |
ownerId | string | Reassign |
expectedCloseAt | string | ISO date |
notes | string | Free text |
currency | string | ISO 4217 code for the deal's value, e.g. INR, USD. |
Request
curl -X PATCH "https://motherbot.io/api/v1/deals/670c11..." \
-H "Authorization: Bearer motherbot_xxx" \
-H "Content-Type: application/json" \
-d '{ "status": "won" }'Response
{ "success": true, "data": { "id": "670c1...", "stage": "won", "status": "won" } }Delete a deal
Removes the deal permanently. To take a card off the board without losing the history, close it with `status: "lost"` instead.
Requires the salesPipeline feature — without it this returns 403.
Path parameters
idrequired | string | Deal ID |
Request
curl -X DELETE "https://motherbot.io/api/v1/deals/670c11..." \
-H "Authorization: Bearer motherbot_xxx"Response
{ "success": true, "data": { "id": "670c11...", "deleted": true } }Products
List catalogue products
Your WhatsApp catalogue, filterable by search term, category, status and stock.
Query parameters
search | string | Matches name, SKU or retailer ID |
category | string | Exact category match |
status | string | Listing statusAllowed active | inactive |
inStock | boolean | true or false |
page | integer | Page numberDefault 1 |
limit | integer | Max 100Default 20 |
Request
curl "https://motherbot.io/api/v1/products?inStock=true" \
-H "Authorization: Bearer motherbot_xxx"Response
{
"success": true,
"data": [
{ "id": "670d4...", "name": "Cotton Kurta", "retailerId": "SKU-1182",
"price": 1299, "currency": "INR", "inStock": true, "metaProductId": "78214..." }
],
"pagination": { "page": 1, "limit": 20, "total": 84, "pages": 5 }
}Add a product
Validated for Meta and pushed to your connected catalogue, exactly as from the dashboard. `retailerId` is your own id for the item and is unique per organisation — re-posting the same one returns 409 rather than duplicating it. A failed Meta sync is reported in `metaSync`, not fatal: the product is saved either way.
Body
namerequired | string | Product name |
retailerIdrequired | string | Your own unique id for this item |
pricerequired | number | Price in major units (e.g. 1299 = ₹1,299) |
salePrice | number | Must not exceed price |
currency | string | ISO currency codeDefault INR |
description | string | Shown in the catalogue |
imageUrl | string | Public image URL — must be a JPG or PNG, the only formats Meta's catalogue accepts (WebP, AVIF, HEIC and GIF are rejected). See POST /media |
url | string | Product page on your site |
sku | string | Stock keeping unit |
category | string | Category name |
brand | string | Brand name |
whatsappAccount | string | Which connected WhatsApp number's catalogue this product belongs to. Defaults to your primary. |
status | string | Listing statusAllowed active | inactive |
inStock | boolean | Keep this current — it's what stops a chatbot offering something sold outDefault true |
Request
curl -X POST "https://motherbot.io/api/v1/products" \
-H "Authorization: Bearer motherbot_xxx" \
-H "Content-Type: application/json" \
-d '{ "name": "Cotton Kurta", "retailerId": "SKU-1182", "price": 1299, "inStock": true }'Response
{
"success": true,
"data": { "id": "670d4...", "retailerId": "SKU-1182", "metaProductId": null },
"metaSync": { "synced": false, "error": "No catalogue configured" }
}| 201 | Created |
| 402 | Product limit reached on your plan |
| 409 | That retailerId already exists |
Get a product
One catalogue row. `metaProductId` is the field worth reading — a product Meta doesn't have cannot be sent in a product message, and that is otherwise an unanswerable "why didn't it send".
Path parameters
idrequired | string | Product ID |
Request
curl "https://motherbot.io/api/v1/products/670d4a..." \
-H "Authorization: Bearer motherbot_xxx"Response
{
"success": true,
"data": {
"id": "670d4a...",
"name": "Cotton Kurta",
"retailerId": "SKU-1182",
"price": 1299,
"salePrice": null,
"currency": "INR",
"inStock": true,
"status": "active",
"metaProductId": "7712994...",
"updatedAt": "2026-07-02T06:41:00.000Z"
}
}Update a product and re-sync to Meta
The endpoint a store keeps pointed at its own price and stock changes. A catalogue that drifts from the shop is worse than no catalogue — the chatbot quotes prices that no longer exist.
Path parameters
idrequired | string | Product ID |
Request
curl -X PATCH "https://motherbot.io/api/v1/products/670d4a..." \
-H "Authorization: Bearer motherbot_xxx" \
-H "Content-Type: application/json" \
-d '{ "price": 1149, "inStock": false }'Response
{ "success": true, "data": { "id": "670d4...", "price": 1149, "inStock": false },
"metaSync": { "synced": true } }Delete a product
Removes it here and from the connected Meta catalogue.
Path parameters
idrequired | string | Product ID |
Request
curl -X DELETE "https://motherbot.io/api/v1/products/670d4a..." \
-H "Authorization: Bearer motherbot_xxx"Response
{ "success": true, "data": { "id": "670d4a...", "deleted": true } }Media
Upload a file, get a public URL
The missing rung: POST /messages takes a media link, so without this you'd have to host every image and PDF publicly yourself before you could send it. Multipart, 8 MB max, rate-limited to a tenth of your plan's per-minute allowance. File contents are checked against the declared type — an executable renamed .png is refused.
Body
filerequired | file | multipart/form-data. JPEG, PNG, MP4, 3GP, MP3, OGG, AAC, PDF, DOC(X), XLS(X), PPTX, TXT, CSV |
Request
curl -X POST "https://motherbot.io/api/v1/media" \
-H "Authorization: Bearer motherbot_xxx" \
-F "file=@invoice.pdf"Response
{
"success": true,
"data": { "url": "https://cdn.motherbot.io/.../invoice.pdf",
"mediaType": "document", "mimeType": "application/pdf",
"filename": "invoice.pdf", "bytes": 148213, "storage": "s3" }
}| 201 | Uploaded — `mediaType` is the message type to send it as |
| 413 | File over 8 MB |
| 415 | Unsupported type, or contents don't match it |
Number Health
Every endpoint here needs the numberHealthGuardian feature on the plan.
Health of every connected number
Quality rating, the 24-hour allowance and what's left of it, seven-day delivery/block/opt-out rates, warm-up state, and whether Guardian has paused sending. Read this before a bulk run — a paused number rejects sends, and `remainingToday` decides whether a broadcast finishes or stalls halfway. `dailyCap` and `remainingToday` are `null` on a number Meta has granted an unlimited tier: there is no cap to count down from. Treat null as "no limit", not as zero. The three `sevenDay` rates are 0–1 fractions, and every one of them is a share of `sevenDay.ruledOn` — the messages Meta returned a verdict on (`delivered + failed`). That is deliberately not `sevenDay.sent`: sent counts only broadcast, sequence and template-retry traffic, while the delivery statuses arrive for every message the number sends, so dividing by sent produced rates above 100%. Messages still in flight are in neither term, so the rates stay meaningful mid-broadcast.
Requires the numberHealthGuardian feature — without it this returns 403.
Request
curl "https://motherbot.io/api/v1/number-health" \
-H "Authorization: Bearer motherbot_xxx"Response
{
"success": true,
"data": [
{ "accountId": "66aa1...", "phoneNumber": "+91 94065 37037", "status": "connected",
"health": { "score": 78, "level": "caution", "qualityRating": "YELLOW",
"dailyCap": 10000, "usedToday": 2140, "remainingToday": 7860,
"sevenDay": { "sent": 9310, "delivered": 8994, "failed": 305,
"ruledOn": 9299, "deliveryRate": 0.967,
"blockRate": 0.0023, "optOutRate": 0.0004 },
"sendingPaused": null,
"factors": ["Meta has rated this number YELLOW …"] } }
]
}Webhooks
List webhook endpoints
Your registered endpoints with delivery stats, plus an `events` array of everything you may subscribe to. Secrets are never listed — see the create call.
Request
curl "https://motherbot.io/api/v1/webhooks" \
-H "Authorization: Bearer motherbot_xxx"Response
{
"success": true,
"data": [
{ "id": "671aa...", "name": "Order sync", "url": "https://example.com/hooks/motherbot",
"events": ["message.received"], "isActive": true,
"stats": { "delivered": 8421, "failed": 3, "lastDeliveredAt": "2026-07-27T09:11:04Z" } }
],
"events": ["message.received", "message.sent", "..."]
}Register a webhook endpoint
The other direction of the API — so you never have to poll for an inbound message. The URL must be publicly reachable: private, loopback and cloud-metadata addresses are rejected. The signing secret is returned ONCE by this response and never again; store it when you create the endpoint. Capped by your plan's webhook quota.
Body
urlrequired | string | Publicly reachable https endpoint |
eventsrequired | string[] | At least one. An unknown event is a 400 listing the valid ones |
name | string | Label shown in the dashboardDefault API webhook |
secret | string | One is generated if you don't supply it |
isActive | boolean | Create it paused by passing false — nothing is delivered until you enable it.Default true |
Request
curl -X POST "https://motherbot.io/api/v1/webhooks" \
-H "Authorization: Bearer motherbot_xxx" \
-H "Content-Type: application/json" \
-d '{ "url": "https://example.com/hooks/motherbot",
"events": ["message.received", "message.failed"] }'Response
{
"success": true,
"data": { "id": "671aa...", "url": "https://example.com/hooks/motherbot",
"events": ["message.received", "message.failed"],
"secret": "whsec_9f2c…", "isActive": true },
"message": "Store the secret now — it is not returned again."
}| 201 | Registered |
| 400 | Unknown event, or a URL we won't call |
| 402 | Webhook limit reached on your plan |
Update or pause an endpoint
Set `isActive: false` to pause deliveries during maintenance — the endpoint keeps its id and its stats, and you don't have to re-register it against your quota afterwards.
Path parameters
idrequired | string | Webhook ID |
Body
url | string | New endpoint URL |
events | string[] | Replaces the subscription list |
name | string | Rename |
isActive | boolean | false pauses deliveries |
Request
curl -X PATCH "https://motherbot.io/api/v1/webhooks/671aa2..." \
-H "Authorization: Bearer motherbot_xxx" \
-H "Content-Type: application/json" \
-d '{ "isActive": false }'Response
{ "success": true, "data": { "id": "671aa...", "isActive": false } }Unregister an endpoint
Frees a slot against your plan's webhook quota.
Path parameters
idrequired | string | Webhook ID |
Request
curl -X DELETE "https://motherbot.io/api/v1/webhooks/671aa2..." \
-H "Authorization: Bearer motherbot_xxx"Response
{ "success": true, "data": { "id": "671aa2...", "deleted": true } }Usage & Plan
Plan limits, usage and feature flags
Call this before a long run. Every other endpoint can refuse with 402 (quota full) or 403 (feature not on the plan); without this one you can only discover those by hitting them — which means finding out mid-import that the contact quota ran out three hundred rows ago. `limit: -1` means unlimited, and `remaining` is null rather than a misleading number.
Request
curl "https://motherbot.io/api/v1/usage" \
-H "Authorization: Bearer motherbot_xxx"Response
{
"success": true,
"data": {
"plan": { "name": "Growth", "subscriptionStatus": "active", "isActive": true },
"rateLimit": { "perMinute": 300 },
"limits": [
{ "key": "contacts", "label": "Contacts", "used": 8412, "limit": 25000,
"unlimited": false, "remaining": 16588, "percentUsed": 34 }
],
"features": { "salesPipeline": true, "numberHealthGuardian": true, "webChat": false },
"integrations": ["shopify", "hubspot"]
}
}Catalog Orders
List captured WhatsApp cart checkouts
Carts customers submit through WhatsApp's native catalog/cart experience — captured automatically on every such message, independent of whether a chatbot flow is wired up for it. There is no create endpoint; subscribe to the `catalog_order.created` webhook to react the moment one lands instead of polling.
Query parameters
contactId | string | Only this contact's carts |
status | string | Filter by statusAllowed new | pushed_to_store | dismissed |
page | integer | Page numberDefault 1 |
limit | integer | Max 100Default 20 |
Request
curl "https://motherbot.io/api/v1/catalog-orders?status=new" \
-H "Authorization: Bearer motherbot_xxx"Response
{
"success": true,
"data": [
{ "id": "670c9f2...", "contactId": "664a1f8...", "totalValue": 1947, "currency": "INR",
"status": "new", "items": [ { "productRetailerId": "sku-2201", "quantity": 2 } ] }
],
"pagination": { "page": 1, "limit": 20, "total": 6, "pages": 1 }
}Get a captured cart
One cart, with the resolved product match (when the retailer id matched a local Product) and, once pushed, the store's own order id.
Path parameters
idrequired | string | Catalog order ID |
Request
curl "https://motherbot.io/api/v1/catalog-orders/670c9f2..." \
-H "Authorization: Bearer motherbot_xxx"Response
{ "success": true, "data": { "id": "670c9f2...", "status": "new", "totalValue": 1947, "currency": "INR" } }Dismiss a captured cart
The only write this endpoint allows — `status` must be `"dismissed"`. Pushing a cart into your connected store as a real order stays dashboard-only and staff-confirmed: a wrong retailer-id-to-variant mapping there would create a live, possibly mispriced order in your commerce backend, so that step deliberately has no API path.
Path parameters
idrequired | string | Catalog order ID |
Body
statusrequired | string | The only accepted valueAllowed dismissed |
Request
curl -X PATCH "https://motherbot.io/api/v1/catalog-orders/670c9f2..." \
-H "Authorization: Bearer motherbot_xxx" \
-H "Content-Type: application/json" \
-d '{ "status": "dismissed" }'Response
{ "success": true, "data": { "id": "670c9f2...", "status": "dismissed" } }Recurring Broadcasts
Every endpoint here needs the recurringCampaigns feature on the plan.
List recurring/date-triggered broadcasts
A schedule ("every Friday 10am") or a per-contact date field (birthday, anniversary) that spawns and launches a real, ordinary Broadcast each time it fires.
Requires the recurringCampaigns feature — without it this returns 403.
Query parameters
status | string | Filter by statusAllowed active | paused |
Request
curl "https://motherbot.io/api/v1/recurring-campaigns" \
-H "Authorization: Bearer motherbot_xxx"Response
{ "success": true, "data": [ { "_id": "670e1...", "name": "Weekly Digest", "status": "active",
"trigger": { "type": "schedule", "frequency": "weekly", "dayOfWeek": 5, "time": "10:00", "timezone": "Asia/Kolkata" } } ] }Create a recurring broadcast
Fires through the exact same launch path a one-off broadcast does — every plan limit, Number Health Guardian check and per-recipient tracking applies identically. `trigger` is either a `schedule` (frequency + optional dayOfWeek/dayOfMonth + time + timezone) or a `date_field` (a cf1–cf10 custom field + offsetDays + time + timezone).
Requires the recurringCampaigns feature — without it this returns 403.
Body
namerequired | string | Broadcast name |
whatsappAccountIdrequired | string | Number to send from |
messageType | string | Default templateAllowed template | freeform |
templateId | string | Required unless messageType is freeform |
audienceTyperequired | string | Allowed all | tags | segment |
audienceTags | string[] | Required when audienceType is tags |
segmentId | string | Required when audienceType is segment |
triggerrequired | object | Schedule or date-field trigger — see description |
rateLimitPerSecond | number | Default 3 |
Request
curl -X POST "https://motherbot.io/api/v1/recurring-campaigns" \
-H "Authorization: Bearer motherbot_xxx" \
-H "Content-Type: application/json" \
-d '{ "name": "Weekly Digest", "whatsappAccountId": "66aa1...", "templateId": "66f0a...",
"audienceType": "all", "trigger": { "type": "schedule", "frequency": "weekly", "dayOfWeek": 5, "time": "10:00", "timezone": "Asia/Kolkata" } }'Response
{ "success": true, "data": { "_id": "670e1...", "name": "Weekly Digest", "status": "active" } }| 201 | Created |
| 402 | Not on your plan, or a plan limit is full |
Get a recurring broadcast
Includes totalRuns, totalRecipients and lastRunAt — the running tally across every occurrence it has fired.
Requires the recurringCampaigns feature — without it this returns 403.
Path parameters
idrequired | string | Recurring broadcast ID |
Request
curl "https://motherbot.io/api/v1/recurring-campaigns/670e1..." \
-H "Authorization: Bearer motherbot_xxx"Response
{ "success": true, "data": { "_id": "670e1...", "name": "Weekly Digest", "totalRuns": 12, "totalRecipients": 38400 } }Pause or resume
The only write this endpoint allows. Editing the audience, trigger or content is dashboard-only. Resuming a paused schedule recomputes its next run from now — it does not fire every occurrence it missed while paused.
Requires the recurringCampaigns feature — without it this returns 403.
Path parameters
idrequired | string | Recurring broadcast ID |
Body
statusrequired | string | Allowed active | paused |
Request
curl -X PATCH "https://motherbot.io/api/v1/recurring-campaigns/670e1..." \
-H "Authorization: Bearer motherbot_xxx" \
-H "Content-Type: application/json" \
-d '{ "status": "paused" }'Response
{ "success": true, "data": { "_id": "670e1...", "status": "paused" } }Delete a recurring broadcast
Stops future occurrences. Broadcasts it already spawned are untouched.
Requires the recurringCampaigns feature — without it this returns 403.
Path parameters
idrequired | string | Recurring broadcast ID |
Request
curl -X DELETE "https://motherbot.io/api/v1/recurring-campaigns/670e1..." \
-H "Authorization: Bearer motherbot_xxx"Response
{ "success": true, "data": { "id": "670e1...", "deleted": true } }AI Knowledge Base
Every endpoint here needs the aiKnowledgeBase feature on the plan.
List knowledge bases
The collections your AI Reply chatbot node and inbox copilot retrieve from.
Requires the aiKnowledgeBase feature — without it this returns 403.
Request
curl "https://motherbot.io/api/v1/knowledge-base" \
-H "Authorization: Bearer motherbot_xxx"Response
{ "success": true, "data": [ { "_id": "670c120...", "name": "Return Policy", "status": "ready", "documentCount": 4 } ] }Create a knowledge base
Starts empty — add sources with POST /knowledge-base/:id/documents.
Requires the aiKnowledgeBase feature — without it this returns 403.
Body
namerequired | string | |
description | string | |
embeddingProvider | string | Fixed at creation — every document in this KB is embedded with itDefault localAllowed local | openai | gemini |
Request
curl -X POST "https://motherbot.io/api/v1/knowledge-base" \
-H "Authorization: Bearer motherbot_xxx" \
-H "Content-Type: application/json" \
-d '{ "name": "Return Policy" }'Response
{ "success": true, "data": { "_id": "670c120...", "name": "Return Policy", "status": "ready" } }| 201 | Created |
| 402 | Not on your plan, or the knowledge-base limit is full |
Add a URL or pasted text as a source
Runs synchronously — extracted, chunked and embedded before the response returns, so it only lands once the document is actually searchable (or has failed to become so). File upload is dashboard-only, since it needs multipart handling this endpoint doesn't take on. Also fires the `knowledge_base.document_processed` webhook, useful for an integration that doesn't want to hold the request open on a slow crawl.
Requires the aiKnowledgeBase feature — without it this returns 403.
Path parameters
idrequired | string | Knowledge base ID |
Body
sourceTyperequired | string | Allowed url | text |
url | string | Required when sourceType is url — crawled and stripped to text |
text | string | Required when sourceType is text |
title | string | Defaults to the page title (url) or first line (text) |
Request
curl -X POST "https://motherbot.io/api/v1/knowledge-base/670c120.../documents" \
-H "Authorization: Bearer motherbot_xxx" \
-H "Content-Type: application/json" \
-d '{ "sourceType": "url", "url": "https://mystore.com/pages/returns" }'Response
{ "success": true, "data": { "documentId": "670c124...", "chunkCount": 14 } }| 201 | Indexed |
| 400 | ingest_failed — no usable text found, or the embedding provider has no API key configured |
Calendly
List Calendly bookings
Read-only — there is no create endpoint, since Calendly has no "book on someone's behalf" API, only the invitee's own scheduling link. Subscribe to `calendly.booking_created` / `calendly.booking_canceled` to react the moment one changes instead of polling.
Query parameters
contactId | string | Only this contact's bookings |
status | string | Allowed active | canceled | rescheduled |
page | integer | Page numberDefault 1 |
limit | integer | Max 100Default 20 |
Request
curl "https://motherbot.io/api/v1/calendly/bookings?status=active" \
-H "Authorization: Bearer motherbot_xxx"Response
{
"success": true,
"data": [
{ "id": "670ca01...", "contactId": "664a1f8...", "eventTypeName": "30 Minute Demo",
"startTime": "2026-08-05T09:30:00.000Z", "status": "active" }
],
"pagination": { "page": 1, "limit": 20, "total": 3, "pages": 1 }
}Webhook events
The other direction of the API. Register an endpoint with POST /webhooks and we POST to it when things happen, so you never poll for an inbound message. Every event below is dispatched from real code — there are no placeholders.
The envelope
{
"event": "message.received",
"payload": { "contactId": "664a1f…", "…": "…" },
"timestamp": "2026-07-28T09:14:22.000Z"
}The per-event fields live under payload. Reading them off the top level is the mistake that makes a handler silently do nothing. Deliveries carry X-MotherBot-Event and, when the endpoint has a secret, X-MotherBot-Signature: sha256=… — an HMAC-SHA256 of the raw body. Verify against the raw bytes; a re-serialized object will not match.