Skip to content
MotherBot
Integrations

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 403 with insufficient_scope, and details.requiredScope names the one it needed.
  • A channel the key is not allowed on answers 403 with channel_not_allowed, and details.allowedChannels lists 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 return 207 with per-recipient results; anything larger returns 202 with a batchId. Poll GET /messages/bulk/:batchId.
  • POST /campaigns/send — the same thing framed as a broadcast, with a name and a pause/resume control. Always queued. Poll GET /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.json

In 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 403 does.
  • 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.

Starter60 req/min
Growth300 req/min
Enterprise1,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": {}
}
CodeerrorMeaning
400invalid_requestMissing or malformed parameter
400invalid_templateTemplate exists but is not APPROVED
400invalid_stateResource is in the wrong state for the action
400no_whatsapp_accountNo connected WhatsApp account found
400no_channel_accountThat channel has no connected account — see GET /channels
400unsupported_for_channelThat channel cannot carry that message type — see GET /channels
401unauthorizedMissing, invalid, or revoked API key
402plan_limit_reachedQuota full — details carry the numbers
403feature_not_availableYour plan does not include this feature
403insufficient_scopeThis key lacks the permission the endpoint needs — details.requiredScope names it
403channel_not_allowedThis key is restricted to other channels — details.allowedChannels lists them
404not_foundResource does not exist
409already_existsA resource with that unique id already exists
413file_too_largeUpload exceeds the size limit
415unsupported_media_typeFile type not accepted, or contents don't match it
429rate_limit_exceededToo many requests
502upstream_errorWhatsApp / 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/api/v1/analytics/summary

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

daysintegerNumber 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

json
{
  "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

GET/api/v1/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

channelstringOnly this channelAllowed whatsapp | messenger | instagram | telegram | line | viber | webchat | sms | rcs | email
statusstringConnection 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

json
{
  "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": [] }
    }
  ]
}
200Channel list returned (an empty array when nothing is connected)
400Unknown `channel` or `status`
401Missing or invalid API key

Contacts

GET/api/v1/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

pageintegerPage numberDefault 1
limitintegerResults per pageDefault 20Allowed 1–100
channelstringOnly contacts on this channel. Omit for every channel.Allowed whatsapp | messenger | instagram | telegram | line | viber | webchat | sms | rcs | email
tagstringFilter by tag (exact match)
searchstringPartial match on name, phone, or email

Request

curl "https://motherbot.io/api/v1/contacts?tag=vip&limit=50" \
  -H "Authorization: Bearer motherbot_xxx"

Response

json
{
  "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
  }
}
POST/api/v1/contacts

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

phoneNumberrequiredstringPhone 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.
channelstringWhich channel this contact is reachable onDefault whatsappAllowed whatsapp | messenger | instagram | telegram | line | viber | webchat | sms | rcs | email
channelIdstringThe 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.
namestringFull display name
firstNamestringFirst name
lastNamestringLast name
emailstringEmail address
tagsstring[]Labels to attach (merged with existing tags)
customFieldsobjectCustom 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.
sourcestringOrigin 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

json
{
  "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/api/v1/contacts/:id

Get contact

Fetches a single contact by its MongoDB _id.

Path parameters

idrequiredstringContact _id

Request

curl "https://motherbot.io/api/v1/contacts/664a1f..." \
  -H "Authorization: Bearer motherbot_xxx"

Response

json
{
  "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"
  }
}
PATCH/api/v1/contacts/:id

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

idrequiredstringContact _id

Body

namestringFull name. Wins over a name rebuilt from firstName/lastName in the same request.
firstNamestringFirst name. Rebuilds 'name' unless you send 'name' too.
lastNamestringLast name. Rebuilds 'name' unless you send 'name' too.
phoneNumberstringMove 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.
emailstringEmail address
tagsstring[]Replaces the full tag list
customFieldsobjectReplaces all custom fields
notesstringFree-text internal notes
optedInbooleanMarketing opt-in status
blockedbooleanBlock contact from receiving messages
languagestringISO 639-1 language code
countrystringISO 3166-1 alpha-2 country code
citystringCity 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

json
{
  "success": true,
  "data": {
    "_id": "664a1f...",
    "name": "Priya Sharma",
    "tags": ["vip", "delhi"],
    "optedIn": false,
    "updatedAt": "2026-06-24T10:00:00.000Z"
  }
}
DELETE/api/v1/contacts/:id

Delete contact

Permanently deletes a contact. This action is irreversible.

Path parameters

idrequiredstringContact _id

Request

curl -X DELETE "https://motherbot.io/api/v1/contacts/664a1f..." \
  -H "Authorization: Bearer motherbot_xxx"

Response

json
{
  "success": true,
  "message": "Contact deleted"
}
200Contact deleted
404Contact not found

Messages

GET/api/v1/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

contactIdstringOnly this contact's thread
channelstringOnly messages that travelled on this channelAllowed whatsapp | messenger | instagram | telegram | line | viber | webchat | sms | rcs | email
directionstringFilter by directionAllowed inbound | outbound
statusstringFilter by delivery statusAllowed sent | delivered | read | failed
sourcestringWhich part of the product sent itAllowed campaign | sequence | chatbot | live_chat | api | coupon | …
sincestringISO date — inclusive lower bound
untilstringISO date — inclusive upper bound
pageintegerPage numberDefault 1
limitintegerResults 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

json
{
  "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 }
}
POST/api/v1/messages

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

torequiredstringThe 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.
channelstringWhich channel to send on. Must be connected — see GET /channels.Default whatsappAllowed whatsapp | sms | rcs | email | telegram | line | viber | messenger | instagram | webchat
accountIdstringWhich 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.
typestringMessage typeDefault textAllowed text, template, image, video, document, audio, interactive, location, reaction
text.bodystringMessage body (required when type is text, max 4096 chars)
subjectstringEmail only — the subject line. Defaults to "A message from us".
template.namestringApproved template name (required when type is template)
template.language.codestringTemplate language code e.g. en_US (required when type is template)
template.componentsarrayThe 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.variablesobject | 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.headerMediaUrlstringA 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.headerVariablesstring[]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.buttonVariablesstring[]One value per dynamic URL button, in the order those buttons appear on the template.
template.buttonVariablesByIndexobjectButton 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.couponCodestringOverrides the coupon on a COPY_CODE button. Defaults to the one the template was approved with.
template.offerExpiresAtstringISO time a LIMITED_TIME_OFFER template's countdown expires.
image | video | document | audioobjectMedia, 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.typestringWhich interactive message to send (required when type is interactive)Allowed button | list | cta_url | flow | location_request
interactive.headerobject`{ "type": "text", "text": "…" }` or a media header, e.g. `{ "type": "video", "video": { "link": "https://…" } }`. Max 60 characters of text.
interactive.body.textstringThe message body. Required for every interactive type.
interactive.footer.textstringSmall print under the message, max 60 characters.
interactive.action.buttonsarrayFor `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.buttonstringFor `list`: the label on the button that opens the list, max 20 characters.Default Choose
interactive.action.sectionsarrayFor `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.displayTextstringFor `cta_url`: the button label, max 20 characters.
interactive.action.urlstringFor `cta_url`: the https URL the button opens.
interactive.action.flowIdstringFor `flow`: the WhatsApp Flow to open. `flowToken`, `flowCta` and `screen` are optional.
location.latitudenumberFor type location: -90 to 90. `longitude` (-180 to 180) is required with it; `name` and `address` are optional.
reaction.message_idstringFor 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

json
{
  "success": true,
  "data": {
    "messageId": "wamid.HBgNOTE5...",
    "to": "919876543210",
    "status": "sent",
    "channel": "whatsapp",
    "accountId": "66f1c2a0d3b4e5f6a7b8c9d0",
    "contactId": "6650f1a2b3c4d5e6f7a8b9c0",
    "contactCreated": false
  }
}
201Message accepted by the channel
400Invalid payload, unusable 'to', or an accountId that isn't yours / isn't connected
400unsupported_for_channel — that channel cannot carry that message type
402Monthly message limit reached
403Contact is blocked or has opted out, or the key is not allowed on this channel
502The channel rejected the send — see `details.upstream`
POST/api/v1/messages/bulk

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

templateNamerequiredstringTemplate name exactly as stored (must be APPROVED)
languageCoderequiredstringTemplate language code e.g. en_US
accountIdstringWhich 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.
recipientsrequiredarrayArray of recipient objectsAllowed 1–100,000 items
modestring`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
rateLimitPerSecondnumberQueued 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[].torequiredstringRecipient phone number in international format, digits only. Created as a contact if new.
recipients[].variablesobjectBody 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[].headerMediaUrlstringThis 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[].headerVariablesstring[]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[].buttonVariablesByIndexobjectThis recipient's dynamic URL button values, keyed by the button's real index.
headerVariablesstring[]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`.
buttonVariablesstring[]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

json
// 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"
}
202Accepted and queued — poll `statusUrl` for per-recipient results
207Sent inline — check each result's status field
400invalid_template — template not APPROVED or not found
400`mode: "sync"` above 250 recipients — use the queued path
429Bulk rate limit exceeded
GET/api/v1/messages/bulk/:batchId

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

batchIdrequiredstringThe `batchId` returned by POST /api/v1/messages/bulk

Query parameters

statusstringOnly recipients in this state — `status=failed` is the one to poll for a retry listAllowed queued | sent | delivered | read | failed
pageintegerPage numberDefault 1
limitintegerRecipients 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

json
{
  "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 }
}
200Batch status returned
404No such batch in this organization

Templates

GET/api/v1/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

accountIdstringOnly templates belonging to this WhatsApp account — an `id` from GET /channels
statusstringFilter by Meta approval statusAllowed APPROVED, PENDING, REJECTED, DISABLED, DRAFT
categorystringFilter by template categoryAllowed AUTHENTICATION, MARKETING, UTILITY
languagestringFilter by language code e.g. en_US, hi, ar
searchstringPartial match on template name (case-insensitive)
pageintegerPage numberDefault 1
limitintegerResults 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

json
{
  "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

GET/api/v1/campaigns

List broadcasts

Returns a paginated list of broadcasts with their delivery stats and template info.

Query parameters

statusstringFilter by broadcast statusAllowed draft, scheduled, running, paused, completed, failed
pageintegerPage numberDefault 1
limitintegerResults per pageDefault 20Allowed 1–100

Request

curl "https://motherbot.io/api/v1/campaigns?status=completed" \
  -H "Authorization: Bearer motherbot_xxx"

Response

json
{
  "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 }
}
POST/api/v1/campaigns

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

namerequiredstringBroadcast display name
templateIdrequiredstring_id of an APPROVED template
audienceTypestringAudience selection strategyDefault allAllowed all, tags
audienceTagsstring[]Required when audienceType is tags
scheduledAtdate-time stringWhen 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.
descriptionstringInternal 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

json
{
  "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"
  }
}
POST/api/v1/campaigns/send

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

namestringCampaign name — auto-generated from template name and date if omitted
templateIdstringTemplate MongoDB _id (use this or templateName + languageCode)
templateNamestringTemplate name — requires languageCode
languageCodestringLanguage code e.g. en_US, hi — required when using templateName
commonVariablesobjectBody variable values shared by all recipients e.g. { "1": "MotherBot", "3": "support@example.com" }
headerMediaUrlstringPublic 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.
headerVariablesstring[]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[].headerMediaUrlstringThis recipient's OWN header media — their invoice or ticket rather than one banner for the whole broadcast.
recipients[].headerVariablesstring[]This recipient's TEXT header values, falling back per position to the campaign-wide list.
rateLimitPerSecondnumberMessages per secondDefault 3Allowed 1–80
accountIdstringWhich 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.
channelstringAccepted 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
recipientsrequiredarrayRecipient list — no contact pre-registration neededAllowed 1–100,000 items
recipients[].torequiredstringPhone number with country code, no + prefix (e.g. 919876543210)
recipients[].namestringRecipient name stored in the broadcast message log for reporting
recipients[].variablesobjectPer-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

json
{
  "success": true,
  "data": {
    "campaignId": "665d4c...",
    "status": "running",
    "total": 3,
    "channel": "whatsapp",
    "accountId": "66f1c2a0d3b4e5f6a7b8c9d0",
    "rateLimitPerSecond": 10,
    "etaSeconds": 1,
    "etaText": "~1 second"
  }
}
201Broadcast created and running — use campaignId to poll stats
400invalid_template — template not found, not APPROVED, or not on the account you chose
400no_whatsapp_account — no connected WhatsApp account, or the accountId isn't yours
400unsupported_for_channel — broadcasts are WhatsApp templates only
429Broadcast rate limit exceeded
GET/api/v1/campaigns/:id/stats

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

idrequiredstringBroadcast _id

Request

curl "https://motherbot.io/api/v1/campaigns/665c3b.../stats" \
  -H "Authorization: Bearer motherbot_xxx"

Response

json
{
  "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"
  }
}
POST/api/v1/campaigns/:id/pause

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

idrequiredstringBroadcast _id

Request

curl -X POST "https://motherbot.io/api/v1/campaigns/665c3b.../pause" \
  -H "Authorization: Bearer motherbot_xxx"

Response

json
{
  "success": true,
  "data": { "id": "665c3b...", "status": "paused" }
}
200Broadcast paused
400invalid_state — broadcast is not running or scheduled
POST/api/v1/campaigns/:id/resume

Resume broadcast

Resumes a paused campaign. Returns 400 invalid_state if the campaign is not in paused status.

Path parameters

idrequiredstringBroadcast _id

Request

curl -X POST "https://motherbot.io/api/v1/campaigns/665c3b.../resume" \
  -H "Authorization: Bearer motherbot_xxx"

Response

json
{
  "success": true,
  "data": { "id": "665c3b...", "status": "running" }
}
200Broadcast resumed
400invalid_state — broadcast is not paused

Segments

GET/api/v1/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

countstringPass 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

json
{
  "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

GET/api/v1/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

json
{
  "success": true,
  "data": [
    { "id": "6653d...", "name": "Trial nudges", "status": "active", "stepCount": 3,
      "stats": { "enrolled": 420, "completed": 180, "exited": 60, "sent": 940, "failed": 2 } }
  ]
}
POST/api/v1/sequences

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

namerequiredstringSequence name
whatsappAccountIdrequiredstringNumber the sequence sends from
entryobject{ type: "manual" | "segment" | "tags" | "ad", segmentId?, tags?, adSourceId? }
stepsrequiredarrayUp to 20 steps: { templateId, delayValue, delayUnit, templateVariableMapping, headerMediaUrl? }
stopOnReplybooleanExit 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

json
{
  "success": true,
  "data": { "id": "6653d...", "status": "draft" }
}
201Sequence created as a draft
400invalid_request — unapproved template, bad delay, or >20 steps
POST/api/v1/sequences/:id/enroll

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

idrequiredstringSequence ID

Body

tagstringEveryone carrying this tag
segmentIdstringEveryone in this saved segment
contactIdsarrayExplicit 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

json
{
  "success": true,
  "data": { "enrolled": 128, "skipped": 12 }
}
GET/api/v1/sequences/:id/enrollments

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

idrequiredstringSequence ID

Query parameters

statusstringFilter 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

json
{
  "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

GET/api/v1/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

json
{
  "success": true,
  "data": [
    { "_id": "6654e...", "name": "Diwali 25", "kind": "shared", "code": "DIWALI25",
      "discountType": "percent", "discountValue": 25,
      "issuedCount": 380, "redeemedCount": 112, "isActive": true }
  ]
}
POST/api/v1/coupons

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

namerequiredstringInternal name (unique per org)
kindstringOne code for all, or one per personDefault sharedAllowed shared | unique
codestringRequired when kind is shared
discountTypestringDiscount shapeDefault percentAllowed percent | amount | free_shipping | custom
discountValuenumber20 for 20% or ₹20
expiresAtstringISO date
maxRedemptionsintegerTotal allowed across everyone (0 = unlimited)Default 0
maxPerContactintegerRedemptions 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

json
{
  "success": true,
  "data": { "id": "6654e...", "name": "Diwali 25", "kind": "shared", "code": "DIWALI25" }
}
201Coupon created
409duplicate — a coupon with that name or code already exists
POST/api/v1/coupons/:id/send

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

idrequiredstringCoupon ID

Body

contactIdsstring[]Audience — explicit contact IDs. Pass exactly one of contactIds, tag or segmentId.
tagstringAudience — every contact carrying this tag. Pass exactly one of contactIds, tag or segmentId.
segmentIdstringAudience — every contact in this segment. Pass exactly one of contactIds, tag or segmentId.
templateNamerequiredstringAn APPROVED template with at least one variable
templateLanguagestringLanguage code to send the template in, when it is approved in more than one.Default the template's own language
codeVarIndexrequiredintegerWhich {{n}} receives the coupon code
variablesobjectValues for the template's OTHER variables, e.g. { "1": "{{contact.firstName}}" }
whatsappAccountIdstringNumber to send from (defaults to your primary)
headerMediaUrlstringRequired 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

json
{
  "success": true,
  "data": { "sent": 380, "failed": 2, "skipped": 0, "total": 382 }
}
POST/api/v1/coupons/:id/sync

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

idrequiredstringCoupon ID

Body

actionstringPut 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

json
{
  "success": true,
  "data": { "externalId": "gid://shopify/DiscountCodeNode/14...", "code": "DIWALI25", "adopted": false }
}
GET/api/v1/coupons/:id/redemptions

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

idrequiredstringCoupon ID

Query parameters

limitintegerHow 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

json
{
  "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"
    }
  ]
}
POST/api/v1/coupons/redeem

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

coderequiredstringThe code the customer entered (omit when action is issue)
contactIdstringWho is redeeming — required for per-contact limits to mean anything
referencestringYour order id, stored on the redemption
actionstringSet to "issue" with couponId + contactId + waId to mint a code without sending itAllowed issue
couponIdstringThe coupon to mint from. Required when action is "issue"; ignored otherwise.
waIdstringThe 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

json
{
  "success": true,
  "data": { "couponId": "6654e...", "discountType": "percent", "discountValue": 25 }
}
200Redeemed
400invalid_request — expired, already used, or cap reached

Deals & Pipeline

Every endpoint here needs the salesPipeline feature on the plan.

GET/api/v1/pipelines

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

json
{
  "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 }
      ] }
  ]
}
GET/api/v1/deals

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

pipelineIdstringOnly deals on this board
stagestringStage key from GET /pipelines
statusstringFilter by outcomeAllowed open | won | lost
contactIdstringEvery deal for one contact
ownerIdstringDeals owned by one team member
pageintegerPage numberDefault 1
limitintegerMax 100Default 20

Request

curl "https://motherbot.io/api/v1/deals?status=open&limit=50" \
  -H "Authorization: Bearer motherbot_xxx"

Response

json
{
  "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 }
}
POST/api/v1/deals

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

contactIdrequiredstringThe contact this deal is for
titlestringDefaults to the contact's name
valuenumberDeal value
currencystringDefaults to the pipeline's currency
pipelineIdstringDefaults to your default board
stagestringStage key; defaults to the first stage
ownerIdstringTeam member who owns it
expectedCloseAtstringISO date
notesstringFree text
sourcestringWhere 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

json
{ "success": true, "data": { "id": "670c1...", "stage": "qualified", "status": "open" } }
GET/api/v1/deals/:id

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

idrequiredstringDeal ID

Request

curl "https://motherbot.io/api/v1/deals/670c11..." \
  -H "Authorization: Bearer motherbot_xxx"

Response

json
{
  "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"
  }
}
200OK
404not_found — no such deal in your organization
PATCH/api/v1/deals/:id

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

idrequiredstringDeal ID

Body

stagestringMove the card to this stage
statusstringClose or reopen the dealAllowed open | won | lost
lostReasonstringWhy it was lost
titlestringRename
valuenumberNew value
ownerIdstringReassign
expectedCloseAtstringISO date
notesstringFree text
currencystringISO 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

json
{ "success": true, "data": { "id": "670c1...", "stage": "won", "status": "won" } }
DELETE/api/v1/deals/:id

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

idrequiredstringDeal ID

Request

curl -X DELETE "https://motherbot.io/api/v1/deals/670c11..." \
  -H "Authorization: Bearer motherbot_xxx"

Response

json
{ "success": true, "data": { "id": "670c11...", "deleted": true } }

Products

GET/api/v1/products

List catalogue products

Your WhatsApp catalogue, filterable by search term, category, status and stock.

Query parameters

searchstringMatches name, SKU or retailer ID
categorystringExact category match
statusstringListing statusAllowed active | inactive
inStockbooleantrue or false
pageintegerPage numberDefault 1
limitintegerMax 100Default 20

Request

curl "https://motherbot.io/api/v1/products?inStock=true" \
  -H "Authorization: Bearer motherbot_xxx"

Response

json
{
  "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 }
}
POST/api/v1/products

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

namerequiredstringProduct name
retailerIdrequiredstringYour own unique id for this item
pricerequirednumberPrice in major units (e.g. 1299 = ₹1,299)
salePricenumberMust not exceed price
currencystringISO currency codeDefault INR
descriptionstringShown in the catalogue
imageUrlstringPublic 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
urlstringProduct page on your site
skustringStock keeping unit
categorystringCategory name
brandstringBrand name
whatsappAccountstringWhich connected WhatsApp number's catalogue this product belongs to. Defaults to your primary.
statusstringListing statusAllowed active | inactive
inStockbooleanKeep 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

json
{
  "success": true,
  "data": { "id": "670d4...", "retailerId": "SKU-1182", "metaProductId": null },
  "metaSync": { "synced": false, "error": "No catalogue configured" }
}
201Created
402Product limit reached on your plan
409That retailerId already exists
GET/api/v1/products/:id

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

idrequiredstringProduct ID

Request

curl "https://motherbot.io/api/v1/products/670d4a..." \
  -H "Authorization: Bearer motherbot_xxx"

Response

json
{
  "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"
  }
}
PATCH/api/v1/products/:id

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

idrequiredstringProduct 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

json
{ "success": true, "data": { "id": "670d4...", "price": 1149, "inStock": false },
  "metaSync": { "synced": true } }
DELETE/api/v1/products/:id

Delete a product

Removes it here and from the connected Meta catalogue.

Path parameters

idrequiredstringProduct ID

Request

curl -X DELETE "https://motherbot.io/api/v1/products/670d4a..." \
  -H "Authorization: Bearer motherbot_xxx"

Response

json
{ "success": true, "data": { "id": "670d4a...", "deleted": true } }

Media

POST/api/v1/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

filerequiredfilemultipart/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

json
{
  "success": true,
  "data": { "url": "https://cdn.motherbot.io/.../invoice.pdf",
            "mediaType": "document", "mimeType": "application/pdf",
            "filename": "invoice.pdf", "bytes": 148213, "storage": "s3" }
}
201Uploaded — `mediaType` is the message type to send it as
413File over 8 MB
415Unsupported type, or contents don't match it

Number Health

Every endpoint here needs the numberHealthGuardian feature on the plan.

GET/api/v1/number-health

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

json
{
  "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

GET/api/v1/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

json
{
  "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", "..."]
}
POST/api/v1/webhooks

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

urlrequiredstringPublicly reachable https endpoint
eventsrequiredstring[]At least one. An unknown event is a 400 listing the valid ones
namestringLabel shown in the dashboardDefault API webhook
secretstringOne is generated if you don't supply it
isActivebooleanCreate 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

json
{
  "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."
}
201Registered
400Unknown event, or a URL we won't call
402Webhook limit reached on your plan
PATCH/api/v1/webhooks/:id

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

idrequiredstringWebhook ID

Body

urlstringNew endpoint URL
eventsstring[]Replaces the subscription list
namestringRename
isActivebooleanfalse 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

json
{ "success": true, "data": { "id": "671aa...", "isActive": false } }
DELETE/api/v1/webhooks/:id

Unregister an endpoint

Frees a slot against your plan's webhook quota.

Path parameters

idrequiredstringWebhook ID

Request

curl -X DELETE "https://motherbot.io/api/v1/webhooks/671aa2..." \
  -H "Authorization: Bearer motherbot_xxx"

Response

json
{ "success": true, "data": { "id": "671aa2...", "deleted": true } }

Usage & Plan

GET/api/v1/usage

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

json
{
  "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

GET/api/v1/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

contactIdstringOnly this contact's carts
statusstringFilter by statusAllowed new | pushed_to_store | dismissed
pageintegerPage numberDefault 1
limitintegerMax 100Default 20

Request

curl "https://motherbot.io/api/v1/catalog-orders?status=new" \
  -H "Authorization: Bearer motherbot_xxx"

Response

json
{
  "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/api/v1/catalog-orders/:id

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

idrequiredstringCatalog order ID

Request

curl "https://motherbot.io/api/v1/catalog-orders/670c9f2..." \
  -H "Authorization: Bearer motherbot_xxx"

Response

json
{ "success": true, "data": { "id": "670c9f2...", "status": "new", "totalValue": 1947, "currency": "INR" } }
PATCH/api/v1/catalog-orders/:id

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

idrequiredstringCatalog order ID

Body

statusrequiredstringThe 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

json
{ "success": true, "data": { "id": "670c9f2...", "status": "dismissed" } }

Recurring Broadcasts

Every endpoint here needs the recurringCampaigns feature on the plan.

GET/api/v1/recurring-campaigns

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

statusstringFilter by statusAllowed active | paused

Request

curl "https://motherbot.io/api/v1/recurring-campaigns" \
  -H "Authorization: Bearer motherbot_xxx"

Response

json
{ "success": true, "data": [ { "_id": "670e1...", "name": "Weekly Digest", "status": "active",
  "trigger": { "type": "schedule", "frequency": "weekly", "dayOfWeek": 5, "time": "10:00", "timezone": "Asia/Kolkata" } } ] }
POST/api/v1/recurring-campaigns

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

namerequiredstringBroadcast name
whatsappAccountIdrequiredstringNumber to send from
messageTypestringDefault templateAllowed template | freeform
templateIdstringRequired unless messageType is freeform
audienceTyperequiredstringAllowed all | tags | segment
audienceTagsstring[]Required when audienceType is tags
segmentIdstringRequired when audienceType is segment
triggerrequiredobjectSchedule or date-field trigger — see description
rateLimitPerSecondnumberDefault 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

json
{ "success": true, "data": { "_id": "670e1...", "name": "Weekly Digest", "status": "active" } }
201Created
402Not on your plan, or a plan limit is full
GET/api/v1/recurring-campaigns/:id

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

idrequiredstringRecurring broadcast ID

Request

curl "https://motherbot.io/api/v1/recurring-campaigns/670e1..." \
  -H "Authorization: Bearer motherbot_xxx"

Response

json
{ "success": true, "data": { "_id": "670e1...", "name": "Weekly Digest", "totalRuns": 12, "totalRecipients": 38400 } }
PATCH/api/v1/recurring-campaigns/:id

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

idrequiredstringRecurring broadcast ID

Body

statusrequiredstringAllowed 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

json
{ "success": true, "data": { "_id": "670e1...", "status": "paused" } }
DELETE/api/v1/recurring-campaigns/:id

Delete a recurring broadcast

Stops future occurrences. Broadcasts it already spawned are untouched.

Requires the recurringCampaigns feature — without it this returns 403.

Path parameters

idrequiredstringRecurring broadcast ID

Request

curl -X DELETE "https://motherbot.io/api/v1/recurring-campaigns/670e1..." \
  -H "Authorization: Bearer motherbot_xxx"

Response

json
{ "success": true, "data": { "id": "670e1...", "deleted": true } }

AI Knowledge Base

Every endpoint here needs the aiKnowledgeBase feature on the plan.

GET/api/v1/knowledge-base

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

json
{ "success": true, "data": [ { "_id": "670c120...", "name": "Return Policy", "status": "ready", "documentCount": 4 } ] }
POST/api/v1/knowledge-base

Create a knowledge base

Starts empty — add sources with POST /knowledge-base/:id/documents.

Requires the aiKnowledgeBase feature — without it this returns 403.

Body

namerequiredstring
descriptionstring
embeddingProviderstringFixed 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

json
{ "success": true, "data": { "_id": "670c120...", "name": "Return Policy", "status": "ready" } }
201Created
402Not on your plan, or the knowledge-base limit is full
POST/api/v1/knowledge-base/:id/documents

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

idrequiredstringKnowledge base ID

Body

sourceTyperequiredstringAllowed url | text
urlstringRequired when sourceType is url — crawled and stripped to text
textstringRequired when sourceType is text
titlestringDefaults 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

json
{ "success": true, "data": { "documentId": "670c124...", "chunkCount": 14 } }
201Indexed
400ingest_failed — no usable text found, or the embedding provider has no API key configured

Calendly

GET/api/v1/calendly/bookings

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

contactIdstringOnly this contact's bookings
statusstringAllowed active | canceled | rescheduled
pageintegerPage numberDefault 1
limitintegerMax 100Default 20

Request

curl "https://motherbot.io/api/v1/calendly/bookings?status=active" \
  -H "Authorization: Bearer motherbot_xxx"

Response

json
{
  "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

json
{
  "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.

Messages

5 events

Contacts

5 events

Conversations & Desk

4 events

Broadcasts

3 events

Sequences

3 events

Chatbots & Flows

4 events

Deals & Pipeline

4 events

Quotations

12 events

Store & Coupons

3 events

Links & Ads

2 events

Number Health

2 events

Templates

2 events

Payments

2 events

Calls

2 events

Scheduling

2 events

Lead sources

1 event