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.

Building with an AI coding assistant? Give it /llms-full.txt — this whole reference, every webhook event and every permission as one plain-text file. A shorter guide is at /llms.txt.

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 organisation credential — 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 accountId that 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.

A 202 does not always mean “on its way”. If the account requires approval for bulk sends, the response carries held: true and status: "pending_approval" alongside an approvalId, and its summary.sent is zero while summary.pendingApproval is the whole batch. Nothing has been sent — somebody in the account has to sign it off first, and the request expires if nobody does. Check held before reporting a send as accepted. An API key is never exempt from these rules, whatever role it acts with: a shared credential identifies nobody.

A send that would take the account past a hard monthly spend budget is refused with 402 and error: "budget_exceeded" instead. Both controls are off unless the account owner switches them on under Settings.

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 media header’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 headerVariables takes 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.

Free & Spark60 req/min
Growth300 req/min
Infinity1,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 the estimated WhatsApp cost. billableCount = messages Meta charged for (each sent message counted once, whatever its later delivery status); freeCount = messages Meta didn't charge for. estimatedCost prices each message at the category Meta billed it under, using your rate card. A free-form broadcast is priced at the reply (service) rate — Meta doesn't charge for the first 1,000 replies each month on each WhatsApp number.

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",
      "freeCount": 0
    },
    "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" }
    ]
  }
}

Appointments

GET/api/v1/appointments

List appointments

Bookings, newest slot first. Everything the calendar holds — taken in chat, by your team, in the phone app, or through this API.

Query parameters

statusstringComma-separated. Omit for every status.Allowed pending | confirmed | cancelled | completed | no_show
fromstringEarliest date, YYYY-MM-DD in your timezone (inclusive)
tostringLatest date, YYYY-MM-DD in your timezone (inclusive)
serviceIdstringOnly bookings for this service
resourceIdstringOnly bookings with this staff member or resource. Pass `none` for unassigned ones.
contactIdstringOnly this customer's bookings
sourcestringWhere the booking was madeAllowed chatbot | dashboard | api | flow | workflow | mobile
searchstringName, phone, email, service, staff, or the booking reference
sortBystringSort columnDefault startsAtAllowed startsAt | createdAt | name | status | service | resource
sortDirstringSort directionDefault ascAllowed asc | desc
pageintegerPage numberDefault 1
limitintegerRows per page. Max 100.Default 25Allowed 1-100

Request

curl "https://motherbot.io/api/v1/appointments?from=2026-08-01&status=confirmed" \
  -H "Authorization: Bearer motherbot_xxx"

Response

json
{
  "success": true,
  "data": [
    { "id": "6660b...", "code": "K7M2QX9P", "status": "confirmed",
      "date": "2026-08-03", "time": "10:30", "endTime": "11:00",
      "dateLabel": "Mon, 3 Aug 2026", "timeLabel": "10:30",
      "startsAt": "2026-08-03T05:00:00.000Z", "timezone": "Asia/Kolkata", "duration": 30,
      "service": { "id": "6660c...", "name": "Consultation" },
      "resource": { "id": "6660d...", "name": "Dr. Mehta" },
      "contact": { "id": "664a1...", "name": "Rahul Sharma", "phone": "919876543210" },
      "name": "Rahul Sharma", "phone": "+919876543210", "partySize": 1, "source": "chatbot" }
  ],
  "pagination": { "page": 1, "limit": 25, "total": 42, "pages": 2 }
}
POST/api/v1/appointments

Book an appointment

Books a slot for a contact. The slot is re-checked against opening hours, staff, capacity and buffers inside a lock, so two calls racing for the last seat cannot both win — the loser gets 409. Your notice period and booking window don't apply to API calls (you are the business); `force: true` additionally ignores hours and capacity, for a walk-in. Send an `Idempotency-Key` header and a retry returns the SAME booking instead of a second one.

Body

contactIdrequiredstringWho the booking is for
datestringYYYY-MM-DD in your timezone. Required unless `start` is given.
timestringHH:MM, 24-hour, in your timezone. Required unless `start` is given.
startstringISO 8601 instant, instead of date + time
serviceIdstringSets the length, seats and price
resourceIdstringA specific staff member or resource. Omit to let the calendar pick whoever is free.
partySizeintegerSeats taken — a table, a class bookingDefault 1
namestringDefaults to the contact's name
phonestringDefaults to the contact's number
emailstringEmailed confirmations and your own records
notesstringShown to the customer
staffNotesstringInternal only — never sent to the customer
statusstring`pending` puts it in your approval queue while still holding the slotDefault confirmedAllowed pending | confirmed
forcebooleanBook outside hours and past capacityDefault false
notifyCustomerbooleanSend the confirmation message. Defaults to your Appointments setting.
idempotencyKeystringSame as the `Idempotency-Key` header; the header wins if both are sent.

Request

curl -X POST "https://motherbot.io/api/v1/appointments" \
  -H "Authorization: Bearer motherbot_xxx" \
  -H "Idempotency-Key: booking-4417" \
  -H "Content-Type: application/json" \
  -d '{ "contactId": "664a1f88c2b19d0012ab0001", "date": "2026-08-03", "time": "10:30",
        "serviceId": "6660c0a1c2b19d0012ab0b01" }'

Response

json
{
  "success": true,
  "data": { "id": "6660b...", "code": "K7M2QX9P", "status": "confirmed",
            "date": "2026-08-03", "time": "10:30", "duration": 30 },
  "replayed": false,
  "customerNotified": true
}
201Booked
200Replayed — this Idempotency-Key already booked something
409full / daily_cap / limit_reached — the slot or day is taken
422outside_hours, too_soon, past, service_unavailable, resource_unavailable
503busy — the calendar was locked by another booking; retry
GET/api/v1/appointments/:id

Get one appointment

The full booking, its reminders and which statuses it may move to next.

Path parameters

idrequiredstringAppointment ID

Request

curl "https://motherbot.io/api/v1/appointments/6660b0a1c2b19d0012ab0a01" \
  -H "Authorization: Bearer motherbot_xxx"

Response

json
{
  "success": true,
  "data": { "id": "6660b...", "code": "K7M2QX9P", "status": "confirmed", "date": "2026-08-03",
            "reminders": [{ "key": "r1", "dueAt": "2026-08-02T05:00:00.000Z", "sentAt": null, "skipReason": null }] },
  "allowedStatuses": ["pending", "completed", "no_show", "cancelled"]
}
PATCH/api/v1/appointments/:id

Update details or status

Change the customer's details, your internal notes, or the status. Moving the time has its own endpoint, and so does cancelling. Marking a booking `confirmed` from `pending` sends the approval message.

Path parameters

idrequiredstringAppointment ID

Body

statusstringOnly moves the booking is allowed to make — a completed booking can't go back to pending. `cancelled` behaves exactly like the cancel endpoint.Allowed pending | confirmed | completed | no_show | cancelled
namestringCustomer name on the booking
phonestringContact number on the booking
emailstringEmail on the booking
notesstringShown to the customer
staffNotesstringInternal only
locationstringWhere it happens
reasonstringRecorded in the booking's history with the status change
notifyCustomerbooleanOverride your Appointments setting for this change
forcebooleanWhen reopening a cancelled booking, take the slot even if it has been resold

Request

curl -X PATCH "https://motherbot.io/api/v1/appointments/6660b0a1c2b19d0012ab0a01" \
  -H "Authorization: Bearer motherbot_xxx" \
  -H "Content-Type: application/json" \
  -d '{ "status": "completed" }'

Response

json
{ "success": true, "data": { "id": "6660b...", "status": "completed" }, "customerNotified": false }
200Updated
409invalid_transition — that status change isn't allowed from where it is
POST/api/v1/appointments/:id/reschedule

Move an appointment

Moves the booking to a new time, optionally to a different staff member or service. The reference stays the same, reminders are re-timed for the new time, and the new slot is checked exactly as a fresh booking is.

Path parameters

idrequiredstringAppointment ID

Body

datestringYYYY-MM-DD. Required unless `start` is given.
timestringHH:MM, 24-hour. Required unless `start` is given.
startstringISO 8601 instant, instead of date + time
resourceIdstringMove it to somebody else. Send null to clear.
serviceIdstringChange the service too. Send null to clear.
forcebooleanIgnore hours and capacityDefault false
notifyCustomerbooleanSend the “moved” message
notestringRecorded in the booking's history

Request

curl -X POST "https://motherbot.io/api/v1/appointments/6660b0a1c2b19d0012ab0a01/reschedule" \
  -H "Authorization: Bearer motherbot_xxx" \
  -H "Content-Type: application/json" \
  -d '{ "date": "2026-08-05", "time": "16:00" }'

Response

json
{ "success": true, "data": { "id": "6660b...", "date": "2026-08-05", "time": "16:00", "rescheduleCount": 1 }, "customerNotified": true }
200Moved
409full / unchanged / not_active
422outside_hours, past, resource_unavailable
POST/api/v1/appointments/:id/cancel

Cancel an appointment

Frees the slot and stops its reminders. The booking is kept as a cancelled record, so your reports still see it.

Path parameters

idrequiredstringAppointment ID

Body

reasonstringStored on the booking and available to the cancellation message
notifyCustomerbooleanSend the cancellation message

Request

curl -X POST "https://motherbot.io/api/v1/appointments/6660b0a1c2b19d0012ab0a01/cancel" \
  -H "Authorization: Bearer motherbot_xxx" \
  -H "Content-Type: application/json" \
  -d '{ "reason": "Customer called to cancel" }'

Response

json
{ "success": true, "data": { "id": "6660b...", "status": "cancelled" }, "customerNotified": true }
200Cancelled
409not_active — it was already cancelled or closed
GET/api/v1/appointments/availability

What's free

With `date`, the bookable start times on that day and its named shifts. Without, the dates in range that still have room. This is the same availability the chatbot offers, so anything it returns can actually be booked.

Query parameters

datestringYYYY-MM-DD — returns that day's slots instead of a list of dates
fromstringFirst date to look at (defaults to today)
tostringLast date to look at (defaults to the end of your booking window)
serviceIdstringUse this service's length, seats and staff
resourceIdstringOnly this staff member's or resource's calendar
partySizeintegerSeats neededDefault 1
shiftstringOnly slots in this named session, e.g. Morning
excludeAppointmentIdstringIgnore this booking's own seat — use when offering times for a reschedule
customerViewbooleanApply the notice period and booking window a customer gets. Use this when the answer feeds your own booking page.Default false
limitintegerHow many dates to return. Max 120.Default 60Allowed 1-120

Request

curl "https://motherbot.io/api/v1/appointments/availability?date=2026-08-03&serviceId=6660c0a1c2b19d0012ab0b01" \
  -H "Authorization: Bearer motherbot_xxx"

Response

json
{
  "success": true,
  "data": {
    "date": "2026-08-03",
    "slots": [
      { "time": "10:30", "label": "10:30", "endTime": "11:00",
        "start": "2026-08-03T05:00:00.000Z", "seatsLeft": 1,
        "resources": [{ "id": "6660d...", "name": "Dr. Mehta", "seatsLeft": 1 }] }
    ],
    "shifts": [{ "value": "Morning", "label": "Morning" }]
  }
}
GET/api/v1/appointments/services

List services

What customers can book, with the length, seats, price and which staff deliver each one.

Request

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

Response

json
{
  "success": true,
  "data": [
    { "id": "6660c...", "name": "Consultation", "duration": 30, "bufferAfter": null,
      "capacity": null, "maxPartySize": 1, "price": 500, "currency": "INR",
      "resourceIds": ["6660d..."], "locationType": "in_person", "isActive": true }
  ]
}
GET/api/v1/appointments/resources

List staff & resources

The people, rooms and equipment bookings are made with. Each keeps its own calendar.

Query parameters

serviceIdstringOnly the ones that deliver this service

Request

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

Response

json
{
  "success": true,
  "data": [
    { "id": "6660d...", "name": "Dr. Mehta", "kind": "person", "capacity": 1, "isActive": true }
  ]
}

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

Sales 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. `protectionEnabled` says whether any of this is actually ENFORCED. When it is `false` the organisation has switched number protection off, so `dailyCap`, `warmup` and `sendingPaused` describe what the number can safely do rather than what it will be held to — the cap is still the figure to pace against, but nothing on our side will stop a send going past it. See `PUT /api/v1/number-health/guard`.

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, "protectionEnabled": true,
                  "factors": ["Meta has rated this number YELLOW …"] } }
  ]
}

Number Protection

GET/api/v1/number-health/guard

Is number protection on?

Whether MotherBot is currently allowed to hold your sends back to protect your WhatsApp numbers. Covers the whole organisation, not one number. `enabled: true` is the default for every account and means all of the following apply: the 24-hour cap for each number's messaging tier, the warm-up ramp on a new number, the safe per-second send pace, the circuit breaker that pauses a broadcast when Meta starts withholding it, and the pre-flight that refuses a risky launch.

Request

curl "https://motherbot.io/api/v1/number-health/guard" \
  -H "Authorization: Bearer motherbot_xxx"

Response

json
{
  "success": true,
  "data": { "enabled": true, "updatedAt": null, "updatedBy": null }
}
PUT/api/v1/number-health/guard

Turn number protection on or off

Turns every one of Guardian's stops off — or back on — for the whole organisation. There is ONE switch, not one per integration: setting it here also sets it for the dashboard, the mobile app, the Chrome and spreadsheet extensions, Zoho, WordPress, IndiaMART, the chat widget, your chatbots and automations, and anything you have built against this API. With it off, a broadcast to a list of any size goes out in full, at the rate you asked for. What this does NOT switch off: your billing state, your monthly spend budget, blocked contacts and opt-outs. Those are your money and your customers' consent, and nothing here moves them. **Meta's own limit does not go away.** Each number still has the daily limit Meta enforces, and Meta rejects everything past it. Those rejections count against the number's quality rating, which lowers the limit further and can end in Meta restricting the number. Guardian's cap existed to keep you the safe side of Meta's; with protection off, nothing does — so pace your own sending against `GET /api/v1/number-health`, which reports each number's real allowance either way. Guardian keeps measuring, scoring and alerting regardless. Requires the `number-health:write` scope.

Body

enabledrequiredbooleantrue keeps every protection on (the default for every account); false turns all of them off

Request

curl -X PUT "https://motherbot.io/api/v1/number-health/guard" \
  -H "Authorization: Bearer motherbot_xxx" \
  -H "Content-Type: application/json" \
  -d '{"enabled": false}'

Response

json
{
  "success": true,
  "data": {
    "enabled": false,
    "message": "Number protection is off. Nothing will hold a message back for your number's quality — your billing limits, spend budget, blocked contacts and opt-outs still apply."
  }
}
200Saved — it applies to the next send, from any source
400`enabled` missing, or not true/false
403This API key does not have the number-health:write scope

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. Each cart carries `followUp`, the automatic reminder ladder: `status` is `scheduled` while more reminders are due and `stopped` once they've ended, with `stoppedReason` saying why in plain words (the customer replied, your team answered, the cart was dismissed, the 24-hour window closed). It is `null` when the organisation has cart follow-up switched off, which is a different answer from "reminders finished".

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 } ],
      "followUp": {
        "status": "scheduled",
        "nextReminderAt": "2026-09-03T14:20:00.000Z",
        "remindersSent": 1,
        "stoppedReason": null, "stoppedAt": null,
        "attempts": [
          { "step": 1, "sentAt": "2026-09-03T10:35:00.000Z", "status": "sent",
            "wamid": "wamid.HBgM...", "error": null }
        ]
      } }
  ],
  "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), the automatic reminder ladder's state in `followUp`, 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",
    "followUp": {
      "status": "stopped",
      "nextReminderAt": null,
      "remindersSent": 2,
      "stoppedReason": "The customer replied after sending their cart, so the reminders stopped.",
      "stoppedAt": "2026-09-03T12:04:00.000Z",
      "attempts": [
        { "step": 1, "sentAt": "2026-09-03T10:35:00.000Z", "status": "sent", "wamid": "wamid.HBgM...", "error": null },
        { "step": 2, "sentAt": "2026-09-03T11:50:00.000Z", "status": "sent", "wamid": "wamid.HBgN...", "error": null }
      ]
    }
  }
}
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. Dismissing also stops any automatic cart reminders still queued for it — the response comes back with `followUp.status` already `stopped`.

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",
    "followUp": {
      "status": "stopped", "nextReminderAt": null, "remindersSent": 1,
      "stoppedReason": "The cart was dismissed, so the reminders stopped.",
      "stoppedAt": "2026-09-03T12:10:00.000Z",
      "attempts": [ { "step": 1, "sentAt": "2026-09-03T10:35:00.000Z", "status": "sent", "wamid": "wamid.HBgM...", "error": null } ]
    }
  }
}

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 }
}

Workflows

GET/api/v1/workflows/catalogue

List workflow triggers and steps

Every trigger and step a workflow can be built from. Call this BEFORE creating a workflow — a graph referring to a trigger or step that does not exist is rejected in full. The default response is a summary; pass `step` or `trigger` to get the complete field definitions for the entries you intend to use.

Query parameters

stepstringComma-separated step types to return in full, e.g. send_template,delay. Max 25.
triggerstringComma-separated trigger keys to return in full. Max 25.

Request

curl "https://motherbot.io/api/v1/workflows/catalogue"   -H "Authorization: Bearer motherbot_xxx"

Response

json
{
  "success": true,
  "data": {
    "categories": [ { "id": "messaging", "label": "Messaging", "color": "#16a34a", "blurb": "..." } ],
    "triggers": [ { "key": "contact.created", "kind": "event", "label": "Contact created", "description": "...", "category": "contacts" } ],
    "steps":    [ { "type": "send_template", "label": "Send template", "category": "messaging", "blurb": "...", "branching": false } ],
    "limits":   { "MAX_NODES": 100 }
  },
  "hint": "Call again with ?step=type1,type2 or ?trigger=key1,key2 for the full field definitions of the entries you intend to use."
}
GET/api/v1/workflows

List workflows

Lists workflows newest-changed first. The graph (`nodes`, `edges`) is omitted — fetch a single workflow to get it. Archived workflows are hidden unless `status` asks for them.

Query parameters

statusstringFilter by status. Omitted hides archived workflows.Allowed draft, active, paused, archived, all
pageintegerPage numberDefault 1
limitintegerResults per pageDefault 20Allowed 1-100

Request

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

Response

json
{
  "success": true,
  "data": [
    { "_id": "68f1a...", "name": "Abandoned cart nudge", "status": "active",
      "trigger": { "type": "catalog.order.abandoned" }, "updatedAt": "2026-09-01T10:00:00.000Z" }
  ],
  "pagination": { "page": 1, "limit": 20, "total": 4, "pages": 1 }
}
POST/api/v1/workflows

Create a workflow

Creates a workflow. It is ALWAYS created as a draft, whatever you send — switch it on with a PATCH once you are happy with it. Any validation problems with the graph come back in `issues`; those marked blocking must be fixed before the workflow can be activated.

Body

namerequiredstringWorkflow name. Must be unique within the organisation.
descriptionstringFree-text note about what this workflow does
folderstringFolder name used to group workflows in the builder
triggerobjectWhat starts the workflow: { type, config }. Defaults to a manual trigger. `type` must be a key from the workflow catalogue.
nodesarrayThe steps, as { id, type, position, config }. Each `type` must be a step from the workflow catalogue.
edgesarrayConnections between steps, as { id, source, target, sourceHandle }
settingsobjectPer-workflow settings such as run limits
whatsappAccountIdstringWhatsApp number this workflow acts on

Request

curl -X POST "https://motherbot.io/api/v1/workflows"   -H "Authorization: Bearer motherbot_xxx"   -H "Content-Type: application/json"   -d '{
    "name": "Welcome new contacts",
    "trigger": { "type": "contact.created", "config": {} },
    "nodes": [
      { "id": "n1", "type": "delay", "position": { "x": 0, "y": 120 }, "config": { "duration": { "value": 10, "unit": "minutes" } } }
    ],
    "edges": [ { "id": "e1", "source": "trigger", "target": "n1" } ]
  }'

Response

json
{
  "success": true,
  "data": { "_id": "68f1a...", "name": "Welcome new contacts", "status": "draft", "webhookId": "kR3f..." },
  "issues": []
}
201Created, as a draft
400Unknown trigger or step type, or too many steps
402Workflow limit reached on this plan
409A workflow with that name already exists
GET/api/v1/workflows/:id

Get a workflow

Returns one workflow with its full graph, plus `issues` — the validation problems it has RIGHT NOW. A workflow can become invalid without being edited, when a plan changes or a step is withdrawn, so this is re-checked on every read.

Path parameters

idrequiredstringWorkflow id

Request

curl "https://motherbot.io/api/v1/workflows/68f1a..."   -H "Authorization: Bearer motherbot_xxx"

Response

json
{
  "success": true,
  "data": { "_id": "68f1a...", "name": "Welcome new contacts", "status": "draft",
            "trigger": { "type": "contact.created" }, "nodes": [], "edges": [] },
  "issues": [ { "level": "error", "nodeId": "n1", "message": "Pick a template to send." } ]
}
PATCH/api/v1/workflows/:id

Update or activate a workflow

Applies a partial update. Send `status: "active"` to switch the workflow on — that re-validates the whole graph first and is refused with 422 if anything blocking remains, so activation can never put a broken automation in front of a customer.

Path parameters

idrequiredstringWorkflow id

Body

namestringNew name
descriptionstringNew description
folderstringNew folder
triggerobjectReplacement trigger: { type, config }
nodesarrayReplacement step list
edgesarrayReplacement connection list
settingsobjectReplacement settings
whatsappAccountIdstringMove the workflow to another WhatsApp number. Send an empty string to unset it.
statusstringNew status. `active` re-validates the graph and is refused if blocking issues remain.Allowed draft, active, paused, archived

Request

curl -X PATCH "https://motherbot.io/api/v1/workflows/68f1a..."   -H "Authorization: Bearer motherbot_xxx"   -H "Content-Type: application/json"   -d '{ "status": "active" }'

Response

json
{
  "success": true,
  "data": { "_id": "68f1a...", "status": "active" },
  "issues": []
}
200Updated
404No workflow with that id
422Cannot activate — the graph still has blocking issues

Chatbots

GET/api/v1/chatbots/catalogue

List chatbot node and trigger types

Every node type a chatbot flow can contain, every trigger type, and the channels this API key may build for. Call this BEFORE creating a chatbot — a flow referring to a node type that does not exist fails validation in full. Nodes carrying an `integration` need that integration connected; nodes carrying a `feature` need it on the plan.

Request

curl "https://motherbot.io/api/v1/chatbots/catalogue"   -H "Authorization: Bearer motherbot_xxx"

Response

json
{
  "success": true,
  "data": {
    "channels": [ { "key": "whatsapp", "label": "WhatsApp" }, { "key": "telegram", "label": "Telegram" } ],
    "triggers": [ { "type": "keyword", "label": "Keyword", "description": "..." } ],
    "nodes":    [ { "type": "message", "label": "Message / Ask", "color": "#16a34a", "desc": "..." } ]
  }
}
GET/api/v1/chatbots

List chatbots

Lists chatbots newest-changed first, without their flows. A key restricted to certain channels sees only bots on those channels.

Query parameters

channelstringFilter to one channel, e.g. whatsapp or telegram
isActivebooleanFilter to bots that are switched on (true) or off (false)
pageintegerPage numberDefault 1
limitintegerResults per pageDefault 20Allowed 1-100

Request

curl "https://motherbot.io/api/v1/chatbots?isActive=true"   -H "Authorization: Bearer motherbot_xxx"

Response

json
{
  "success": true,
  "data": [
    { "_id": "68f2b...", "name": "Order status bot", "channel": "whatsapp", "isActive": true,
      "trigger": { "type": "keyword", "keywords": ["order", "track"] } }
  ],
  "pagination": { "page": 1, "limit": 20, "total": 2, "pages": 1 }
}
POST/api/v1/chatbots

Create a chatbot

Creates a chatbot. It is ALWAYS created switched off — activate it with a PATCH once you are happy with the flow. The flow is deep-validated against your plan before it is saved: a node for an integration you have not connected is refused with 422 rather than saved and failed at run time.

Body

namerequiredstringChatbot name
descriptionstringFree-text note about what this bot does
channelstringChannel this bot answers on. Must be one your API key is allowed to use.Default whatsapp
triggerTypestringHow the bot startsDefault keywordAllowed keyword, button_reply, ad_referral, first_message, any_message, opt_in, order
keywordsstring[]Words that start the bot, when triggerType is keyword
keywordMatchTypestringHow a keyword is matched against the inbound messageDefault containsAllowed exact, contains, starts_with
buttonIdsstring[]Button ids that start the bot, when triggerType is button_reply
buttonMatchBystringWhether a button trigger matches on the button's id or its visible textDefault idAllowed id, text
adSourceIdsstring[]Click-to-WhatsApp ad ids that start the bot, when triggerType is ad_referral. WhatsApp only. Omit or send an empty array to answer EVERY ad and organic post.
nodesarrayThe flow's nodes, as { id, position, data: { nodeType, ... } }. Each `nodeType` must come from the chatbot catalogue.
edgesarrayConnections between nodes, as { id, source, target, sourceHandle }
variablesarrayFlow variables, as { name, type, defaultValue }
whatsappAccountIdstringWhatsApp number this bot answers on. Defaults to the first one in scope.
respectBusinessHoursbooleanWhen true the bot only answers inside your configured business hoursDefault false

Request

curl -X POST "https://motherbot.io/api/v1/chatbots"   -H "Authorization: Bearer motherbot_xxx"   -H "Content-Type: application/json"   -d '{
    "name": "Order status bot",
    "channel": "whatsapp",
    "triggerType": "keyword",
    "keywords": ["order", "track"],
    "nodes": [
      { "id": "start", "type": "input", "position": { "x": 0, "y": 0 }, "data": { "label": "Start" } },
      { "id": "n1", "position": { "x": 0, "y": 120 },
        "data": { "nodeType": "message", "msgType": "text", "msgText": "What is your order number?", "waitForReply": true, "saveAs": "orderNo" } }
    ],
    "edges": [ { "id": "e1", "source": "start", "target": "n1" } ]
  }'

Response

json
{
  "success": true,
  "data": { "_id": "68f2b...", "name": "Order status bot", "channel": "whatsapp", "isActive": false },
  "warnings": []
}
201Created, switched off
402Chatbot limit reached on this plan
403This API key may not use that channel
409Another live bot already claims that trigger
422The flow has blocking validation errors
GET/api/v1/chatbots/:id

Get a chatbot

Returns one chatbot with its full flow.

Path parameters

idrequiredstringChatbot id

Request

curl "https://motherbot.io/api/v1/chatbots/68f2b..."   -H "Authorization: Bearer motherbot_xxx"

Response

json
{
  "success": true,
  "data": { "_id": "68f2b...", "name": "Order status bot", "channel": "whatsapp",
            "isActive": false, "nodes": [], "edges": [] }
}
PATCH/api/v1/chatbots/:id

Update or activate a chatbot

Applies a partial update. Send `isActive: true` to switch the bot on — that re-validates the flow AND re-checks that no other live bot claims the same trigger, both of which can have changed since the bot was saved.

Path parameters

idrequiredstringChatbot id

Body

namestringNew name
descriptionstringNew description
channelstringMove the bot to another channel. Both the old and the new channel must be allowed by your key.
triggerTypestringNew trigger typeAllowed keyword, button_reply, ad_referral, first_message, any_message, opt_in, order
keywordsstring[]Replacement keyword list
keywordMatchTypestringNew keyword match modeAllowed exact, contains, starts_with
buttonIdsstring[]Replacement button id list
buttonMatchBystringNew button match modeAllowed id, text
adSourceIdsstring[]Replacement Click-to-WhatsApp ad id list for an ad_referral trigger. An empty array means every ad.
nodesarrayReplacement node list
edgesarrayReplacement edge list
variablesarrayReplacement variable list
respectBusinessHoursbooleanWhether the bot answers only inside business hours
whatsappAccountIdstringMove the bot to another WhatsApp number. Send an empty string to unset it.
isActivebooleanSwitch the bot on or off. Turning it on re-validates the flow and re-checks for trigger conflicts.

Request

curl -X PATCH "https://motherbot.io/api/v1/chatbots/68f2b..."   -H "Authorization: Bearer motherbot_xxx"   -H "Content-Type: application/json"   -d '{ "isActive": true }'

Response

json
{
  "success": true,
  "data": { "_id": "68f2b...", "isActive": true },
  "warnings": []
}
200Updated
404No chatbot with that id
409Another live bot already claims that trigger
422Cannot activate — the flow has blocking errors

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

6 events

Contacts

5 events

Conversations & Desk

6 events

Broadcasts

3 events

Sequences

3 events

Chatbots & Flows

4 events

Sales 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

12 events

Lead sources

6 events