MOMO BUSINESS / DEVELOPERS v3.1.0

API documentation

Build your business into every conversation.

START BUILDING

Your first API request

Create an API token, verify your account, and connect messaging, contacts, commerce, and business data.

GET /api/v3/me
curl --request GET 'https://business.momo.tz/api/v3/me' \
  --header "Authorization: Bearer $MOMO_API_TOKEN" \
  --header 'Accept: application/json'
Connect Momo Business to your AI assistant

Give ChatGPT, Claude, or your own agent access through MCP, with the permissions you choose.

Explore MCP →

Choose your build path

138 documented operations

Everything you need to integrate

26 guides

Authentication, delivery lifecycles, pagination, errors, and practical workflows.

6 languages

Copy request examples in cURL, JavaScript, Python, PHP, Ruby, and Go.

Full contracts

Explore nested fields, validation limits, request payloads, and response schemas.

Explore the API reference

SMS6 operationsWhatsApp4 operationsWhatsApp groups13 operationsContacts17 operationsProfile & Balance2 operationsCatalogue29 operationsData tables12 operationsPayments2 operationsAutomations3 operationsAgent tasks2 operationsOperations1 operationsWebhooks11 operationsFlows10 operationsWhatsApp templates9 operationsPosts11 operationsMCP8 operations
Base URLhttps://business.momo.tz

Use HTTPS and server-side credentials. Endpoint pages identify their authentication requirements and response format.

Start here

Your first integration

Connect your application to the account you use in Momo Business. Start by identifying the account, send to a number you control, then read the saved result. The endpoint reference below provides the exact request fields, response schemas and language examples; this handbook explains how to combine them into a working integration.

Choose the interface

Interface Use it for Authentication
REST /api/v3 SMS, WhatsApp, groups, campaigns, contacts, catalogues, orders and data records An account REST API key
Agent tasks /api/engine Submit a prompt to one of your configured agents and inspect its execution The same REST API key
MCP /mcp or /mcp/v1/{server} Give an assistant access to the tools you authorize An MCP connection credential or OAuth

The two credential kinds are deliberately different. An API key cannot authenticate an MCP connection, and an MCP connection token cannot authenticate REST requests. The public OpenAPI document describes HTTP endpoints; the MCP manifest describes tools available through JSON-RPC.

1. Prepare your account

Open API credentials, create an API key and save its value in your server's secret store. You need permission to manage API keys. Confirm your SMS sending identity or WhatsApp business number is configured before attempting a send. A valid credential alone does not establish a usable sending route.

Use the service origin https://business.momo.tz. All examples use illustrative customer data; replace recipients, IDs and URLs with values from your own account. There is no sandbox flag in the send payload. A successful provider call can send a real message.

2. Identify the account

Set MOMO_API_KEY in your development environment without committing it, then run:

curl --silent --show-error \
  'https://business.momo.tz/api/v3/me' \
  --header "Authorization: Bearer $MOMO_API_KEY" \
  --header 'Accept: application/json'

Check data.id and data.name against the account you intended to connect. The tenant comes from the key; sending a tenant_id in the request is not a way to select another account. GET /api/v3/balance returns wallet balance, currency and billing mode for that same account.

3. Send a controlled SMS

curl --silent --show-error \
  'https://business.momo.tz/api/v3/sms/send' \
  --header "Authorization: Bearer $MOMO_API_KEY" \
  --header 'Accept: application/json' \
  --header 'Content-Type: application/json' \
  --data '{"recipient":"255712345678","sender_id":"MyBrand","message":"Your integration is connected."}'

Replace MyBrand with an approved sender belonging to your account, or omit sender_id to use configured defaults. Keep the returned message uid, numeric id, recipient and status in your application. HTTP 201 means a message record was created. A provider refusal can still produce HTTP 201 with the message's status set to failed.

4. Read the outcome

Call GET /api/v3/sms/{uid} using the saved UID. sent means the provider accepted the message; delivered means a delivery receipt arrived. Some channels do not expose every receipt state. Future scheduled messages return queued until their send attempt.

Add delivery handling before retrying automatically, and use webhooks with polling reconciliation when you need later status changes. Finish by handling 401, 403, 422 and 429 explicitly. The HTTP contracts chapter explains which response envelope applies to each interface.

Start here

Authentication and account access

An API key identifies one account. Keep it on a server you control: a browser bundle, mobile application package or public repository cannot keep an embedded key secret. Your integration should store the plaintext value securely and omit it from request logs, exception reports and copied support examples.

Creating and replacing REST keys

Use Settings → API credentials. Viewing the page requires api-keys.view; creating and revoking keys requires api-keys.manage. Give each integration a recognizable name, up to 160 characters. The create response reveals the plaintext once; subsequent listings do not recover it.

The platform stores a hash of the complete credential. A REST key is generated as a random 64-character string. Do not derive meaning from its contents or build your own key from an account ID. The creation screen does not currently accept a REST expiry setting; if an expiry has been set on a credential, the authentication guard enforces it.

For a planned replacement, create the new key, verify /api/v3/me, update the integration and revoke the old key. Revocation takes effect for subsequent requests. If a key is exposed, revoke it and replace it in every environment that used it. A revoked credential is not restored by sending it again.

Request headers

Authorization: Bearer YOUR_REST_API_KEY
Accept: application/json
Content-Type: application/json

Content-Type describes a request body and is needed when sending JSON. Read-only requests need no body. The REST guard reads the Bearer header, not an API key query parameter. Avoid putting secrets into URLs, where browsers and intermediaries often record them.

Tenant boundaries and permission ceilings

All customer REST endpoints derive their tenant from the key. Resource lookups and mutations stay within that account; changing a resource ID does not change the tenant. A missing foreign record can return 404, while catalogue and order ownership checks can return 403.

Permission checks vary by family:

Family Additional permission checks
Data reads Key issuer must hold data.view; per-table visibility and field masks also apply
Data writes Key issuer must hold data.records.edit; table write grants also apply
Payment reads payments.view
Automation reads automations.view
Group reads communications.groups.view
Group changes communications.groups.manage
Group message sends Both communications.groups.manage and communications.send
Legacy messaging, campaigns, contacts, catalogue, profile and balance Tenant API credential; these controllers do not apply the issuer-permission map used by data and groups

Data, group, payment and automation endpoints reject a key whose issuer cannot be established or whose issuing user is deactivated. Their permission checks use the issuer's current permissions, so a later role change can remove access. Treat REST keys as powerful account credentials; they do not expose a per-endpoint scope picker.

Authentication failures

Missing, unknown, revoked and expired REST keys return HTTP 401. An MCP credential sent to REST also returns 401. A suspended or inactive account returns 403. Read the response message to distinguish credential failure from account suspension or an endpoint permission refusal.

{"status":"error","message":"API token has been revoked."}

Do not retry these failures in a tight loop. Correct the credential, account state or permission first. MCP has a different authorization model with selected servers and OAuth capabilities; follow MCP connections when connecting an assistant.

Start here

HTTP, identifiers and pagination

Use the documented HTTP method and path exactly. Several compatibility contact operations use POST for reads; a GET to the same URL is not an equivalent request. Send JSON objects for documented JSON bodies and preserve numeric IDs, UUIDs and opaque provider IDs in their original roles.

Response envelopes

Most /api/v3 endpoints wrap successful data:

{"status":"success","data":{"id":42,"name":"Example account"}}

Their errors generally use:

{"status":"error","message":"Validation failed.","errors":{"recipient":["Provide recipient or recipients."]}}

errors is optional. Framework failures on v3, such as unsupported methods and rate limiting, are normalized to this error envelope. Do not require an errors map to recognize an error.

Data endpoints use native objects: {tables:[...]}, {record:{...}}, {records:[...],next_cursor:...} and {ok:true,deleted:1}. Their expected domain refusals carry {error:{code,message,retryable,...},message,code}. Authentication and request-envelope validation may still return the standard v3 error shape. Check for both when building a shared data client.

Payment lists return {data:[...],meta:{current_page,per_page,total,last_page}} and details return {data:{...}}. Automation reads return {events,...}, {subscriptions:[...]} or {schedules:[...]}. These native envelopes do not add a success status.

Agent tasks use their own run status objects, while MCP uses JSON-RPC responses. Do not apply a single response.data accessor across these interfaces.

Identifiers

Resource Identifier to retain
SMS/WhatsApp messages Public uid such as msg_…; numeric id also accepted by message lookup
Campaigns Public uid such as cmp_…; numeric ID also accepted
Contacts Public uid such as ctc_…, within a group
Contact groups Group UUID or numeric ID
Catalogues, products, orders, WhatsApp groups Numeric local ID in resource paths
Data tables, data groups, records UUID
Payments UUID or human reference; retain UUID for reconciliation
Business events and schedules UUID
Event subscriptions Numeric ID
Agent runs run_uuid / run uuid
WhatsApp replies and reactions Provider gateway_message_id, often beginning wamid.

Public UID examples illustrate the prefix only; do not validate them as a fixed ULID format. A provider catalogue ID, product retailer ID and local product ID also name different things.

Standard page-number pagination

Message, contact and group lists default to 20 rows. Catalogue, product and order lists default to 25. Use limit or per_page, capped at 100; if both appear, limit wins. A nonpositive value uses the endpoint default. Set page=2 for the next page.

{"status":"success","data":{"items":[],"pagination":{"current_page":1,"per_page":20,"last_page":1,"total":0,"has_more_pages":false}}}

Read has_more_pages and increment page until false. These lists can change while you read them; deduplicate by stable resource ID when building a local index. Data records instead use cursor pagination, and history uses a timestamp cursor. Payments use their own meta pagination shape. Automation events use before; subscription and schedule lists have fixed caps and no pagination.

Status handling and rate limits

HTTP status Integration action
200 / 201 Parse the family-specific body; message/run status can still describe failure
202 An agent task was accepted for background execution
401 Replace or correct credentials
403 Resolve ownership, account state or permission
404 / 405 Check resource ID, tenant, path and method
409 Resolve an idempotency, unique-value or state conflict
422 Correct the request; inspect field messages
429 Wait as instructed by Retry-After
5xx Inspect the endpoint contract before deciding whether repetition is safe

The v3 limit is 120 requests per minute per Bearer token across its endpoints. Throttled requests return 429; rate headers include X-RateLimit-Limit, X-RateLimit-Remaining and, when throttled, Retry-After and X-RateLimit-Reset. Authentication happens first, so unauthenticated responses may not carry these headers. Provider throughput limits are separate and can defer a message after the HTTP request was accepted.

Messaging

Delivery, retries and duplicate prevention

A send request creates a record and attempts to hand it to the selected provider. Track the record's lifecycle separately from the HTTP request that created it. This distinction prevents both false delivery claims and accidental duplicate messages.

What a send response means

POST /api/v3/sms/send and POST /api/v3/whatsapp/send normally attempt delivery inside the request. Each normalized recipient produces its own message record and its own outcome. A multi-recipient request is processed sequentially; it is not an atomic delivery transaction.

{"status":"success","data":{"messages":[{"id":101,"uid":"msg_example_a","recipient":"255712345678","status":"sent","gateway_message_id":"provider-id-a","error_message":null},{"id":102,"uid":"msg_example_b","recipient":"255754000111","status":"failed","gateway_message_id":null,"error_message":"Recipient rejected by provider"}]}}

This abbreviated example is a successful HTTP 201 response with a mixed delivery result. Save every returned message, including failed ones. A future schedule_time queues the message. Throughput admission can defer an immediate send, and an unexpected inline failure can cause a queue fallback, so an unscheduled request can also return queued.

Status progression

Status Meaning
queued Waiting for a due time, worker or available throughput
processing A worker or immediate request claimed the message
sent Provider accepted the send
checking_delivery Delivery status is being checked
delivered Provider reported delivery
read A supported read receipt arrived
failed Send or later delivery failed; inspect error_message
received Inbound message recorded by the platform

Not every channel provides delivered/read receipts. A sent result is not proof that the person read the message. A missing later receipt is also not enough evidence to resend.

Use GET /api/v3/sms/{uid} or GET /api/v3/whatsapp/{uid} for current state. Lists accept status and direction filters and newest-first pagination. Keep numeric IDs as well as UIDs because message callbacks identify records by numeric message_id.

Retry deliberately

The messaging send endpoints do not implement a request idempotency key. Adding an Idempotency-Key header does not suppress a duplicate send here. Each repeated POST may create a new message, even if your request body is identical.

Maintain an integration-side dispatch ledger keyed by your business event, such as an invoice reminder ID. Record the attempt before sending, then attach returned message IDs. If a connection times out without a response, mark the attempt as uncertain and reconcile against message history and your business records before sending again. Use a recognizable business reference in the message where appropriate; arbitrary metadata is not a supported send input.

The internal message job declares a three-attempt budget, but ordinary provider refusals and most send exceptions mark the record failed. Those are not a promise of three automatic provider retries. Correct invalid numbers, sender configuration, templates or provider account problems before initiating a new send.

Reconcile asynchronous updates

Receive supported message webhooks for timely changes, and poll saved IDs when a callback is missing. Callback delivery is currently best effort with no automatic delivery retry. Spread polling across your rate budget and reduce its frequency after terminal outcomes. Treat each recipient independently; never retry the whole batch because one recipient failed.

Agent tasks do support an idempotency header. That is a different contract and should be implemented separately in your client.

Messaging

SMS and sending identities

Use the SMS API for a transactional message or a small set of explicit recipients. Use a campaign when the audience comes from stored contact groups. Sending identities and account defaults determine the route; callers do not select a provider channel ID.

Sending identity selection

sender_id is optional. When provided, it must unambiguously match one of:

Identity Requirement
Alphanumeric sender ID Approved and owned by this account
Phone number Assigned to this account, not released, with SMS capability
Short code An active account assignment

An identity-specific route is used when configured. Otherwise, the platform uses the account's default SMS channel, or the system default when the account has none. Unknown, ambiguous, inactive or invalid configurations return a validation-style error. An API key cannot make an unapproved sender usable.

GET /api/v3/sms/senders lists every identity this workspace may use, with value being exactly what to pass:

{"status":"success","data":{"items":[{"type":"sender_id","value":"AMINA","label":"AMINA","status":"approved"},{"type":"phone_number","value":"+255700000001","label":"Main line","status":"active"},{"type":"short_code","value":"15551","label":"Promotions","status":"active"}]}}

Only usable identities are listed — a sender ID still under review is not. If you omit sender_id, the configured provider/default determines the sender. Test that configuration with a controlled recipient. The request field tenant_channel_id is not supported as a route override and is ignored; the resolved channel fields in a response are for observation.

Recipient and message fields

{"recipients":["255712345678","255754000111"],"sender_id":"MyBrand","message":"Your order is ready for collection.","message_type":"plain"}

recipient accepts a string separated by commas, semicolons or whitespace. recipients accepts an array whose entries can contain the same separators. Both inputs are merged and exact duplicates removed. Send fully qualified numbers with country calling codes to avoid provider-specific assumptions about national numbers. Recipient formatting is not a number-validity guarantee.

Field Limit or default
recipient String, at most 4,000 characters
recipients[] Each string at most 191 characters
sender_id At most 64 characters
message / body At most 4,096 characters each; message takes precedence
message_type / type At most 60 characters; message_type takes precedence; default plain
schedule_time At most 100 characters; use an ISO8601 date with offset

plain, text and sms select ordinary SMS. A compatibility request to /sms/send with message_type:"whatsapp" routes to WhatsApp; new integrations should use the explicit WhatsApp endpoint.

Always supply a meaningful nonempty message. The current SMS controller substitutes a generic body for an empty input; this is not a useful validation mechanism for a business integration.

Length, encoding and cost

The 4,096-character API ceiling is not a promise that the message fits one SMS. A common text encoding uses 160 characters for one segment and 153 per part for multipart SMS; Unicode estimates use 70 and 67. Character encoding, extension characters and provider behavior affect the actual count. Some drivers return a provider segment count; others estimate or use a fallback.

Keep verification codes and notifications concise, and test the exact text you intend to send, especially punctuation, non-Latin text and emoji. A long message can produce multiple billable segments even though it creates one API message record.

Media and results

A media_url can request an MMS-style send on a gateway that supports media. The URL is limited to 2,048 characters; media_type to 32. Supplying media does not guarantee that the chosen SMS provider supports it. For predictable rich media delivery, use an appropriately configured WhatsApp account and its documented payload.

Read every returned status and store its UID. See delivery and duplicate prevention before implementing retries.

Messaging

WhatsApp messages and templates

POST /api/v3/whatsapp/send supports text, approved templates, media links, interactive messages and reactions. Configure a WhatsApp Cloud connection first. sender_id selects the business number to send from; omitting it uses the workspace default.

Which number: the accounts list

GET /api/v3/whatsapp/accounts is where every WhatsApp identifier in this API comes from:

{"status":"success","data":{"default_phone_number_id":"104512345678901","items":[{"waba_id":"102290129340398","name":"Duka la Amina","is_active":true,"is_default":true,"phone_numbers":[{"id":"104512345678901","phone_number":"+255700000001","display":"+255 700 000 001","is_default":true}],"quality_rating":"GREEN","messaging_tier":"TIER_1K","templates_count":12,"catalogues_count":1,"last_synced_at":"2026-09-13T09:14:02+00:00"}]}}

phone_numbers[].id (Meta's phone_number_id) is what sender_id takes here and on the group endpoints, and what from takes on the catalogue sends and flow sessions — the number itself, as phone_number or display, is accepted in the same places. waba_id is what the template endpoints take. default_phone_number_id is the number a send without sender_id goes out from; when it is null there is no usable default and every send must name one. Never pass a local database channel ID. The SMS equivalent is GET /api/v3/sms/senders, described under SMS.

Text and the conversation window

{"recipient":"255712345678","message":"Hello Asha. Your collection is ready.","message_type":"text"}

Text/body is limited to 4,096 characters. For ordinary customer conversations, plan free-form replies within the supported customer-service window and use approved templates for initiating or reopening conversations. The generic REST send route passes the request to the provider; a provider window-policy refusal can appear as HTTP 201 with status:"failed". It is not always an HTTP validation error.

To reply to a particular message, include in_reply_to_gateway_id with its provider message ID. The application's msg_… UID is not a valid substitute. Link previews may be enabled by the driver when a text body contains an HTTP(S) URL.

Templates

{"recipient":"255712345678","message_type":"template","template":{"name":"collection_ready","language":"en","components":[{"type":"body","parameters":[{"type":"text","text":"Asha"},{"type":"text","text":"ORD-1042"}]}]}}

template.name identifies a template on the sending WhatsApp account. The name is at most 191 characters; language is at most 20 and defaults to en. Supply the language code of an approved translation and the exact component/parameter structure required by that template. Creating or approving a provider template is not part of this REST send request.

The controller accepts a components array and forwards it. Provider validation still decides whether the template exists, is approved and has the correct parameters. A top-level message with a template serves as local preview text; it does not replace the approved template body sent by the provider. A template cannot be combined with top-level media or an interactive payload.

Media links

{"recipient":"255712345678","message_type":"image","media_type":"image","media_url":"https://assets.example.com/orders/1042.jpg","message":"Your packed order"}

Use image, video, audio, document or sticker as appropriate. The corresponding media message type requires media_url, a URL up to 2,048 characters. Set media_type explicitly to avoid relying on type inference. The remote content must remain accessible when the provider fetches it, particularly for scheduled delivery.

This REST route takes a link; it does not accept a multipart upload. File size, MIME type and caption support are ultimately provider constraints. The link-send implementation forwards the message body as a caption, including its generated preview fallback when no body was supplied. Test audio and sticker behavior against your connection rather than assuming captions are ignored.

Interactive replies

{"recipient":"255712345678","message_type":"interactive","interactive":{"type":"button","body":{"text":"How would you like to receive your order?"},"action":{"buttons":[{"type":"reply","reply":{"id":"collect","title":"Collect"}},{"type":"reply","reply":{"id":"deliver","title":"Delivery"}}]}}}

Buttons need a body and usable reply IDs/titles. The driver normalizes a maximum of three buttons, with titles capped at 20 characters and IDs at 200. Interactive body text is capped at 1,024. Lists use action.button and sections of rows; row titles are capped at 24 and descriptions at 72. Supply concise valid payloads instead of relying on truncation.

The driver also recognizes cta_url, flow, product, product_list, catalog_message and location_request_message interactive types, passing their supported structure through for provider validation. Interactive and top-level media payloads cannot be combined.

Reactions

{"recipient":"255712345678","message_type":"reaction","reaction":{"message_id":"wamid.EXAMPLE_PROVIDER_ID","emoji":"👍"}}

The target is a provider message ID, up to 191 characters; emoji is required and limited to 16. in_reply_to_gateway_id can supply the target when reaction.message_id is omitted. Reactions cannot include text, media, templates or interactive content in the same request. Empty-emoji reaction removal is not exposed by this route.

All variants return message records. Preserve their local UID for tracking and provider ID for future context. For group conversations, use the separate group message endpoint.

Messaging

WhatsApp template lifecycle and review

WhatsApp lets a business start a conversation — a delivery notice, a payment reminder, an offer — only with a message template Meta has approved first. /api/v3/whatsapp/templates is the lifecycle of those templates over an API key: create one from your own system, it is submitted to Meta for review, and you read the verdict back three ways. Once approved, POST /api/v3/whatsapp/send sends it by template.name as described in WhatsApp messages.

Reads need a key whose issuer holds communications.templates.view; writes need communications.templates.manage. Every response is the v3 envelope: {"status":"success","data":…} or {"status":"error","message":…,"errors":…}.

One template is one name, one language, one account

A template is identified by three things: its name (order_shipped), its language (sw), and the WhatsApp Business Account (waba_id) it lives on. order_shipped in Swahili and order_shipped in English are two templates, each reviewed on its own. The same template on a second business account is a deployment, reviewed separately there too.

Start by reading the accounts, because waba_id is what everything else takes: GET /api/v3/whatsapp/accounts (described in WhatsApp messages) lists every connected business account with its waba_id and numbers. A workspace with one account may omit waba_id everywhere; with several, name one or the request answers 422 with the list.

Create, and let Meta answer in the same request

POST /api/v3/whatsapp/templates creates the template and submits it to Meta inside the request. The common case needs only the flat fields:

{"name":"order_shipped","language":"sw","category":"utility","body":"Habari {{1}}, oda yako {{2}} imetumwa leo.","header_text":"Oda {{1}}","footer":"Duka la Amina","buttons":[{"type":"url","text":"Fuatilia","url":"https://amina.co.tz/track/{{1}}","example":"ORD-1042"}],"variable_samples":{"1":"Asha","2":"ORD-1042"}}

Three rules Meta enforces, so this API enforces them before the call: name is lowercase letters, digits and underscores (it is not slugged for you — you get back the name you sent); every placeholder needs a sample value, which is what variable_samples is for; and a url button whose link ends in {{1}} needs an example too. category is utility for transactional notices, marketing for promotions, authentication for one-time codes; Meta may reclassify it during review.

The answer is 201 either way. What Meta said is in whatsapp_status:

{"status":"success","data":{"id":418,"name":"order_shipped","language":"sw","category":"utility","status":"active","whatsapp_status":"in_review","approved":false,"sendable":false,"rejection_reason":null,"whatsapp_business_account_id":"102290129340398","whatsapp_template_id":"1189456212345678","variables":["1","2"],"components":[{"type":"HEADER","format":"TEXT","text":"Oda {{1}}","example":{"header_text":["ORD-1042"]}},{"type":"BODY","text":"Habari {{1}}, oda yako {{2}} imetumwa leo.","example":{"body_text":[["Asha","ORD-1042"]]}},{"type":"FOOTER","text":"Duka la Amina"},{"type":"BUTTONS","buttons":[{"type":"URL","text":"Fuatilia","url":"https://amina.co.tz/track/{{1}}","example":["ORD-1042"]}]}],"deployments":[],"submission":{"submitted":true,"queued":false,"message":null}}}

in_review with a whatsapp_template_id means Meta has it; review takes minutes to a day. rejected means Meta would not take the payload, and rejection_reason is Meta's own words — a body that starts with a variable, a name already in use, a token without the permission — so fix it and POST /api/v3/whatsapp/templates/{id}/submit. pending with submission.queued: true means Meta could not be reached; the submission is queued and retried, and the status moves on its own.

Send "submit": false to save without submitting — a draft you will finish, or a batch you want to review before it goes to Meta.

Everything Meta supports: post components

The flat fields cover text headers, bodies, footers and the three plain button types. For a media header (Meta wants the sample handle its upload API issues, under example.header_handle), a carousel, a limited-time offer, copy-code, flow or catalogue buttons, or named parameters, describe the template in WhatsApp's own components[] shape — exactly what Meta's Business Management API takes, so a system that already speaks to Meta posts the same payload here:

{"name":"ofa_ijumaa","language":"sw","category":"marketing","waba_id":"102290129340398","components":[{"type":"HEADER","format":"IMAGE","example":{"header_handle":["4::aW1hZ2UvcG5n:ARZ…"]}},{"type":"BODY","text":"Ofa ya {{1}}: punguzo la {{2}} hadi {{3}}.","example":{"body_text":[["Ijumaa","20%","30 Sept"]]}},{"type":"FOOTER","text":"Jibu STOP kuacha"},{"type":"BUTTONS","buttons":[{"type":"QUICK_REPLY","text":"Nataka"},{"type":"COPY_CODE","example":"OFA20"}]}]}

components wins when both descriptions are sent. Whichever way a template was created, the read returns it as components — what was (or would be) submitted — so you can copy one to a new name, or edit and PATCH it back.

Reading the verdict

There are three ways, and they are not alternatives — use the one that fits the moment.

Read the row. GET /api/v3/whatsapp/templates/{id} and GET /api/v3/whatsapp/templates return the status this platform holds, which Meta's own status webhook and a fifteen-minute poll keep current. updated_since on the list makes an incremental mirror cheap: every status move changes the row.

Ask Meta now. POST /api/v3/whatsapp/templates/{id}/refresh reads the template from Meta in the request — status, rejection reason, quality score — and writes it on the row. This is for the moment you need the answer, such as right after a submission, and it needs only the view permission. data.refresh.found_on_whatsapp is false when Meta has no such template on the account.

Be told. Subscribe a webhook to template.status_changed and every move — by Meta's webhook, by the poll, by a refresh — arrives with the previous and new status, the business account, and the reason when rejected:

{"event":"template.status_changed","template_id":418,"name":"order_shipped","language":"sw","category":"utility","whatsapp_business_account_id":"102290129340398","whatsapp_template_id":"1189456212345678","previous_status":"in_review","whatsapp_status":"approved","rejection_reason":null,"timestamp":"2026-09-13T09:14:02+00:00"}

sendable on the template is the one bit that matters at send time: the row is active here and approved by Meta. paused and disabled are Meta stopping a template for quality; they come back the same way.

Pull what the account already has

Templates created in Meta Business Manager, or by another tool, are not here until you ask. POST /api/v3/whatsapp/templates/sync (optionally {"waba_id":…}) imports every approved template on the account as a local row — name, language, category and full components — refreshes the ones already here from Meta's copy, and re-reads the review status of everything already submitted:

{"status":"success","data":{"imported":3,"updated":9,"statuses_updated":1,"errors":[]}}

It runs against Meta's paginated list inside the request, so on an account with hundreds of templates allow a few seconds. Templates still in review on Meta's side that were not submitted through this platform appear once approved.

Changing, deploying, deleting

PATCH /api/v3/whatsapp/templates/{id} takes any subset of the create fields. A content change — body, header_text, footer, buttons, components or category — is re-submitted to Meta as an edit of the template it already holds, which puts it back in review; display_name and status changes never touch Meta. A template Meta is still reviewing cannot be edited on Meta's side: the change is saved here and goes out on the next …/submit once the review is over. name, language and waba_id cannot change, because Meta knows the template by them. A template described by components may hold parts the flat fields cannot express, so to change part of it send the edited components; sending a flat body re-describes the whole content.

POST /api/v3/whatsapp/templates/{id}/deployments with {"waba_ids":[…]} (from GET /api/v3/whatsapp/accounts) puts an authored template on other business accounts of the workspace. Each gets its own review, its own Meta id and its own row under deployments on the template; each outcome is announced by template.status_changed with that account's id.

DELETE /api/v3/whatsapp/templates/{id} removes this language of the template from the business account (Meta keeps other languages of the same name) and archives the row, so campaign history that pointed at it still reads. Add ?permanent=true to delete the row as well.

Templates are per business account, so a template only sends from that account's numbers: the sender_id on a send must be a phone number of the account the template is on.

Messaging

WhatsApp group workflows

WhatsApp groups have their own lifecycle: create the group, wait for provider confirmation, distribute invitations, manage join requests, then send into the active conversation. The group API uses numeric local group IDs in its URLs; a provider group ID is a separate opaque value and may be absent during creation.

Eligibility and permissions

The connected business number must be eligible for the provider's Groups API. The implementation recognizes provider error 131215 as an eligibility refusal and describes the requirement as an Official Business Account. Use the actual connection's eligibility result instead of assuming all WhatsApp numbers can create groups.

Read operations require communications.groups.view. Changes require communications.groups.manage. Sending a group message also requires communications.send. These permissions are evaluated against the current user who issued the API key. An old key with no attributable issuer is refused.

Create, then inspect

{"sender_id":"123456789012345","subject":"Order coordination","description":"Collection arrangements for this order.","join_approval_mode":"approval_required","invite_template":"group_invitation","invitees":["255712345678"]}

POST this to /api/v3/whatsapp/groups. Subject is required, maximum 128 characters; description maximum 2,048. join_approval_mode is auto_approve or approval_required, defaulting to auto approval. invite_template names a stored WhatsApp template. The provider limit represented by this implementation is eight participants including the business, so at most seven initial invitees may be supplied.

A 201 response creates the local group; it does not prove that the group is already active. Read GET /api/v3/whatsapp/groups/{id} until provider confirmation arrives or a failure is recorded. States are creating, active, suspended, deleted and failed.

List with GET /api/v3/whatsapp/groups. By default deleted groups are excluded. status=all includes all states; a specific status filters them. The list's sender_id filter matches the stored provider phone-number ID. Pagination follows the ordinary 20-row default and 100-row cap.

Invitations and membership

Operation Body or result
POST {id}/invites recipients array, 1–7 numbers, optional template name
POST {id}/invite-link/reset Invalidates/replaces the invitation link and returns the new link
GET {id}/join-requests Returns pending request items
POST {id}/join-requests/approve join_requests array of provider request identifiers
POST {id}/join-requests/reject Same identifier array
DELETE {id}/participants participants array, 1–8 provider participant identifiers
PATCH {id} Subject and/or description
DELETE {id} Requests deletion and returns group state

Keep invitation sending distinct from joining: recipients decide whether to join, and approval may be required. Inspect per-recipient successes and failures in invitation and membership results instead of treating a batch response as universal success.

Sending and pinning

POST {id}/messages with message/body, media_url plus media_type, or a stored template name with language/components. Text is limited to 4,096 characters. If no participant has written in the local 24-hour window, a free-form send is refused with 422 and an approved template is required. Group sends return the application's group-message presentation; consult this endpoint's response schema rather than assuming it matches /whatsapp/send exactly.

POST {id}/pin accepts message_uid, required Boolean pin, and optional expiration_days from 1 to 30. Here message_uid is the local public message UID. It is not the provider ID used for reactions.

Subscribe to group lifecycle callbacks where configured, then reconcile important operations by reading the group. Webhook delivery is best effort, and local group state may lag the initial provider request.

Messaging

Campaigns and scheduled messages

Use POST /api/v3/sms/campaign to dispatch an SMS audience stored in contact groups. This endpoint creates one-time SMS campaigns. Recurrence, campaign editing and scheduled-message cancellation are not exposed as fields or operations in this REST family.

Create a campaign

{"contact_list_id":"12,19","sender_id":"MyBrand","name":"Collection reminders","message":"Hello {{name}}, order {{cf:order_ref}} is ready.","schedule_time":"2030-10-12T09:00:00+03:00"}
Field Contract
contact_list_id Required string, maximum 2,000 characters; numeric group IDs or group UUIDs
message Required string, maximum 4,096 characters before personalization
sender_id Optional account-owned sending identity, maximum 64
name Optional campaign name, maximum 160
schedule_time Optional date/time string, maximum 100

Commas, semicolons and whitespace separate group identifiers. Duplicate identifiers resolving to the same group are collapsed. One campaign is created for each group found, and the response is data.campaigns, even for one group. If no group resolves, the endpoint returns 404. If some resolve and others do not, it creates the campaigns for those found. Verify the returned group references against your intended audience.

Sending identity resolution happens before campaigns are created. The same SMS ownership and default-route rules apply as for individual sends. An omitted name becomes an API campaign name derived from the contact group.

Scheduling

Use an explicit ISO8601 offset, such as 2030-10-12T09:00:00+03:00, or UTC ending in Z. Replace the illustrative future date with your intended delivery date. A malformed schedule returns 422. A past date does not establish a future delay; it becomes eligible to run immediately.

A scheduled campaign starts as scheduled; an immediate campaign starts as draft and is dispatched for processing. Queue availability and provider throughput determine when individual sends happen, so the schedule is a due time rather than a guaranteed arrival time for every recipient.

For an explicit individual recipient, /sms/send and /whatsapp/send also accept schedule_time. Those requests queue future messages and return their UIDs. There is no customer REST operation here to cancel or reschedule the saved message later.

Audience and personalization

The campaign reads its group's contacts when it executes. If you edit that group after scheduling, the eventual audience may differ. The current dispatch loop skips missing phone numbers and numbers matching the account/channel blacklist. It does not filter on the contact's is_subscribed flag; your integration must prepare the intended subscribed audience rather than assuming that flag suppresses dispatch.

Template variables are {{name}}, {{phone}}, {{phone_number}}, {{country_code}} and {{cf:your_custom_key}}. Unknown or missing variables become empty text. Preview representative contacts before scheduling, especially when custom values make messages longer or change encoding. An individual send does not perform contact-group template expansion.

Track progress correctly

Use GET /api/v3/campaign/{uid}/view; numeric campaign ID is also accepted. The response includes group reference, schedule, message, sender, status, total recipients, sent_count and failed_count.

completed means the campaign finished dispatching its recipient jobs. Delivery can still be in progress, and counts can continue to change. An unaffordable prepaid campaign can pause; inspect account balance and campaign state before assuming the scheduler failed. Campaigns without a usable route can be cancelled.

Poll campaign state and message results for reconciliation. Although the dashboard contains campaign event labels, this campaign path does not currently emit the corresponding completion/failure callbacks. Repeating campaign creation is not idempotent: record the returned campaign IDs against your own campaign request.

Business data

Contacts and personalization

The contact directory is two things: groups, and the people in them. Every contact belongs to exactly one group, and a group also declares the extra fields its contacts may carry. Reading needs the key issuer's contacts.view; writing needs contacts.create / contacts.edit; deleting needs contacts.delete; the bulk import needs contacts.import.

Start with the groups

GET /api/v3/contact-groups is where the {group_id} every contact path takes comes from:

{"status":"success","data":{"items":[{"id":42,"uid":"9f3a1c20-5d6e-4a71-9c3b-2f1a0e7b8c44","name":"Wateja wa Dar","status":"active","contacts_count":1180,"subscribed_count":1094,"custom_fields":[{"key":"order_ref","label":"Order reference","type":"text"}],"created_at":"2026-09-14T09:14:02+00:00","updated_at":"2026-09-14T09:14:02+00:00"}],"pagination":{"current_page":1,"per_page":25,"last_page":1,"total":1,"has_more_pages":false}}}

subscribed_count is the number a campaign would actually reach; contacts_count includes the people who have opted out. Either the numeric id or the uid works anywhere a group is named — store the uid, it is stable.

POST /api/v3/contact-groups creates one. custom_fields declares the extra fields its contacts may carry: a contact can still be given an undeclared key, but a declared one is what the dashboard shows a column for and what a campaign addresses as {{cf:order_ref}}. Keys are letters, digits, _, . and -; duplicates are refused; label defaults to the key and type to text.

{"name":"Wateja wa Dar","custom_fields":[{"key":"order_ref","label":"Order reference","type":"text"},{"key":"branch","type":"select"}]}

PATCH /api/v3/contact-groups/{group} renames it, retires it with "status":"inactive", or changes the declaration. custom_fields replaces the declaration rather than merging into it — a schema is not a meaningful thing to half-send — and removing a field does not touch values already stored on contacts; they are simply no longer declared.

DELETE /api/v3/contact-groups/{group} deletes the group and every contact in it. A group that still holds people answers 409 with the count; repeat with ?force=true if you mean it. There is no undo and the contacts do not move anywhere.

Two paths to the same contacts

REST Original Does
GET /contact-groups/{group}/contacts POST /contacts/{group_id}/all List
POST /contact-groups/{group}/contacts POST /contacts/{group_id}/store Create
GET /contact-groups/{group}/contacts/{contact} POST /contacts/{group_id}/search/{uid} Read one
PATCH /contact-groups/{group}/contacts/{contact} PATCH /contacts/{group_id}/update/{uid} Change
DELETE /contact-groups/{group}/contacts/{contact} DELETE /contacts/{group_id}/delete/{uid} Delete
POST /contact-groups/{group}/contacts/batch Import up to 500
GET /contacts Search across groups

Both columns reach the same records with the same behaviour. The original paths are not deprecated; the REST ones exist because that is the shape the rest of this API uses and what a generated client expects. A contact is named by its uid or its numeric id, and only within its own group.

Creating a contact

{"PHONE":"0712345678","country_code":"255","name":"Asha Mwinyi","is_subscribed":true,"order_ref":"ORD-1042","branch":"Mlimani"}

PHONE is required (phone_number is an alias; PHONE wins if both are sent). Send country_code explicitly when you want the split stored — a +255… prefix alone is not inferred. With the example above the stored country code is 255 and the national number 712345678; full_phone_number comes back as 255712345678.

The name comes from name, or NAME, or FIRST_NAME + LAST_NAME. A contact created with no name at all is named after their number. is_subscribed defaults to true.

Custom fields go at the top level, as order_ref and branch do above. Every field that is not one of the reserved names — PHONE, phone_number, country_code, name, NAME, FIRST_NAME, LAST_NAME, is_subscribed, _token — is stored as a custom field. Wrapping them in a custom_field_values object would store that wrapper as a key called custom_field_values.

PATCH changes what you send, and nothing else

{"is_subscribed":false}

That call unsubscribes the person and touches nothing else. Custom fields merge into what is already stored; send a key as null to remove it. The name, the number and the subscription each keep their current value when the payload is silent about them, and PHONE is required only when the number itself is changing.

This changed on 14 September 2026. Until then a PATCH replaced the entire custom-field map and renamed the contact after their own phone number whenever name was absent — so the call above would have wiped every custom field and renamed Asha to 255712345678. If you followed the old advice and read-merge-send the whole record, nothing changes for you. If you built around the replacement behaviour, send null for the keys you want gone.

Importing in bulk

POST /api/v3/contact-groups/{group}/contacts/batch takes up to 500 rows, each shaped exactly like a single create:

{"contacts":[{"PHONE":"0712345678","country_code":"255","name":"Asha Mwinyi","order_ref":"ORD-1042"},{"PHONE":"0754000111","country_code":"255","name":"Juma Ally"}],"skip_existing":false}

A number already in the group is updated — its custom fields merge, and a row with no name does not rename anybody. A new number is created. skip_existing: true leaves the ones already there completely untouched.

Rows are judged one at a time. A row that fails validation lands in problems with its index and the others still apply, so the answer is 207 when anything was rejected and 200 when nothing was:

{"status":"success","data":{"group_id":42,"received":3,"created":1,"updated":1,"skipped":0,"rejected":1,"results":[{"index":0,"uid":"ctc_7f2ab1c93de04a6b8f10","outcome":"updated"}],"problems":[{"index":2,"errors":{"PHONE":["The PHONE field is required."]}}]}}

results names what happened to each row that landed, so you know what you changed without diffing anything afterwards.

Finding people

Within a group, GET /contact-groups/{group}/contacts?search=… matches the name, the national number, and the full number with punctuation stripped — a number pasted as +255 712 345 678 finds them. subscribed=true|false narrows to who still accepts messages.

Across groups, GET /api/v3/contacts?q=… answers "who is this number?" when you do not know which group holds them; group_id narrows it back to one. Both are paginated with limit and page, default 20, maximum 100.

Keys issued before 14 September 2026

Reading and writing contacts now requires the permissions above, held by the person whose account issued the key. One exception is deliberate: a key created before 7 September 2026 is not linked to an issuer at all — the platform did not record one — and those keys keep the five original /contacts/{group_id}/… paths they were issued under, because integrations were written against a contract that had no per-key permissions. They are refused on everything added since: the groups, the REST paths, the bulk import and the cross-group search.

Rotate such a key from Settings → API credentials to bring it under the permission model. Check which permissions its issuer holds first, or the rotated key will lose access the old one had.

Using them

Campaigns address these records as {{name}} and {{cf:order_ref}} — see campaigns. The subscription flag records your intent; it is not applied automatically by the campaign dispatch loop, so maintain your audience and your blacklist deliberately. Repeated POSTs of the same number create duplicate rows: use the batch endpoint, which matches on the number, when you mean "make sure these people are in this group".

Business data

Catalogues, products and orders

The commerce API is how an online store, a point-of-sale system or a spreadsheet-driven back office keeps its products and orders in step with Momo Business — and through Momo Business, with WhatsApp and every other platform a shop is published on.

A catalogue (a shop) holds products, brands, categories and orders. It is complete and useful with no platform attached at all: the assistant, the phone menus and the staff app all read it. A channel is that shop's presence on one platform. A listing is one product as one platform sees it. That separation is why the same product can be live on your storefront and blocked on WhatsApp, and why each says so in its own words instead of leaving you to guess.

Create the shop, then fill it

POST /api/v3/catalogues makes a shop from your own system, so an integration can finish its setup without anyone opening a dashboard. Connecting that shop to WhatsApp stays a dashboard step — it needs a consent a bearer token cannot give on a person's behalf. Read channels[] on any catalogue response to see where it is published.

{"name":"Mango Electronics","default_currency":"TZS","sku_prefix":"MNG","stock_policy":"external"}

stock_policy decides who owns the stock count. external means your system owns it: we mirror what you send and tell you what sold, and we never decide a new number on your behalf. momo means we keep the count. A shop created through this API defaults to external; one created in the dashboard or from a spreadsheet defaults to momo. PATCH /api/v3/catalogues/{catalogue} changes any of it afterwards. sku_prefix is accepted on create only — it is stamped into every code the shop has already issued.

Product identity and money

Value Meaning
sku Your product code, and the identity this API addresses a product by
retailer_id The older name for sku, kept in step with it
Catalogue id / product id Our numeric ids, still valid in every path
price / sale_price Whole numbers of the minor unit — 4500000 is TZS 45,000.00

You never have to invent a code. Send no sku and the shop issues one — MNG-00042 — readable, sortable and unique per shop. Once a product is live on any platform its code is frozen: platforms treat it as the item's identity, and changing it would orphan the remote item and silently create a duplicate.

Only name and price are required. An image, a non-zero price and the rest are what individual platforms require, and a product missing them is stored and reported as blocked on that platform rather than refused outright. A shop selling only on its own storefront should not have to satisfy Meta's rules.

{"sku":"MNG-45W","name":"Charger Mango 45W","description":"USB-C PD, 1 m cable","price":4500000,"currency":"TZS","sale_price":3900000,"image_url":"https://cdn.example.com/mng45.jpg","inventory":12,"availability":"in stock","brand":"Mango","category":"Chargers"}

Leave currency out and the shop's own is used. Leave availability out and it is derived from inventory: a count of zero means out of stock unless the shop allows backorders. Send availability explicitly and what you send wins. Brand and category text becomes records on first use, so one import fills the pick lists for everything typed afterwards.

The bulk sync

POST /api/v3/catalogues/{catalogue}/products/batch is the endpoint a store integration lives on. Up to 5,000 products, every documented field stored — not just the identity ones.

It answers 202, not 200. The rows are accepted, and applying them and pushing them to a platform happens in the background, because that takes longer than an HTTP request should. The response carries a sync id.

POST /api/v3/catalogues/42/products/batch
Idempotency-Key: nightly-2026-09-11

{"mode":"upsert","products":[ ... ]}

Send Idempotency-Key on anything scheduled. Repeating a key returns the run that already owns it with "replayed": true and imports nothing, so a cron that times out is safe to retry.

mode is upsert by default: products your payload does not mention are left alone. mode: "replace" says this payload is the catalogue and retires everything missing from it — correct for a full nightly export, destructive for a partial one.

Rows are matched on sku when you send one, and on the product name when you do not, so a sheet with no code column updates its rows on a re-import instead of duplicating them.

One bad row does not refuse the file. A row that cannot be stored becomes one rejected row in the report; the other 4,999 land.

No developer? Point us at a feed

Every store platform — Shopify, WooCommerce, Magento, PrestaShop, Wix — can publish a Google Shopping feed or a Meta product feed with a setting, no code. POST /api/v3/catalogues/{catalogue}/feeds points a shop at that URL and we pull it on a schedule.

{"url":"https://shop.example.com/google-feed.xml","schedule":"daily"}

The first pull runs at once. Column names are guessed from the feed's own headers (g:id, g:price, g:availability and the rest are all known), kept on the feed, and can be corrected with mapping. Every pull is a sync like any other — read last_sync_id at GET /catalogues/{catalogue}/syncs/{sync} for the same report the batch endpoint produces. mode defaults to replace because a feed is normally the whole catalogue; set upsert if yours is partial.

A feed is one-way, so put the shop under stock_policy: external: the store owns the count, the feed states it, and orders here are reported back through webhooks for the store to act on. CSV and JSON feeds work the same way. After ten failed pulls in a row the feed is switched off and the account is told; fix it and switch it back on. POST …/feeds/{feed}/run pulls now, whatever the schedule.

Read the report

GET /api/v3/catalogues/{catalogue}/syncs/{sync} is the other half of the contract. Poll it until status is completed or failed.

{"status":"completed","received":2000,"created":12,"updated":1982,"rejected":6,"retired":0,
 "platforms":{"whatsapp":{"synced":1960,"blocked":34}},
 "problems":[{"sku":"MNG-CABLE","stage":"ingest","reason":"Price must be a whole number of minor units, 0 or more."},
             {"sku":"MNG-KNIFE","stage":"whatsapp","reason":"WhatsApp needs a product image it can fetch."}]}

stage is the field to read first. ingest means the row was not stored at all — fix it and send it again. Any other value is a platform name: the product is stored and correct on our side, and that platform will not show it until the stated problem is fixed. Both look like "my product is not live", and they need different actions. At most 200 problems are kept; problems_truncated says when there were more.

GET /api/v3/catalogues/{catalogue}/syncs lists past runs, newest first, including feed pulls and spreadsheet imports.

Changing one product

PUT /api/v3/catalogues/{catalogue}/products/by-sku/{sku} addresses a product by the code your system already knows it by, so a sync never has to keep a map of our ids. It creates the product when there is none, so a store that has just added an item does not have to know whether we have seen it before.

Only the fields you send are touched. {"inventory": 0} is a stock update and nothing else — it does not blank the description.

GET and DELETE work the same way on the same path. Deleting takes the product off every platform it is on, then off the shelf. The numeric-id paths (/products/{product}) do the same thing and are unchanged.

Product reads carry listings[]: one row per platform with its state (pending, syncing, synced, failed, blocked, drifted) and the problem where there is one. sync_status on the product is a roll-up of those rows — a product in a shop with no platforms is synced, because there is genuinely nothing to sync.

Platforms, and who is right

A shop is published to platforms through channels; a product is one listing per platform. The same product can be synced on your storefront, blocked on WhatsApp (no image), and pending on Instagram at the same time, and each row says so.

Drift. A merchant who edits a price directly in Meta Commerce Manager has made a decision, and the next push from your system or from momo used to overwrite it in silence. Every hour, each platform's copy is compared with ours. A difference marks the listing drifted, records the field-by-field drift, sends product.drifted to your webhooks, and then applies the shop's rule:

source_of_truth What happens
api (default for shops made through this API) or momo Our copy wins: it is pushed back and the platform falls into line.
platform Their copy wins: the changed fields are pulled into our row.

Stock is never part of the comparison — it is a ledger, and platforms only ever see the availability word derived from it. image_url is not compared either, because Meta re-hosts every image it fetches.

Sending a product. POST /api/v3/catalogues/send-product takes a platform. On WhatsApp it is Meta's interactive product card, as before. On any other platform the honest answer is a link: the response carries the product's address there and the endpoint to send it with, so there is one send path for every platform rather than one per platform. A product not published on that platform is refused with a 422 that says so. send-product-list and send-catalogue are WhatsApp's own interactive types with no equivalent elsewhere, and stay WhatsApp-only.

Stock, in both directions

Stock is a ledger, and every shop declares who owns it.

Under stock_policy: external — the default for a shop created through this API — your system owns the count. Push levels with the endpoint below, and we mirror them and tell you what sold. A confirmed sale never decrements on our side, because your system is about to state the new level and the two must not both subtract. We still hold units for a pending order between your syncs, so the same last unit is never promised to two customers.

Under momo — the default for a shop made in the dashboard or from a spreadsheet — we keep the count. A pending order reserves units; confirming it commits them; cancelling releases them; staff adjust by hand with a reason.

Either way, a product with no inventory at all is not tracked. Nobody typed a count, so nothing is held back and nothing is ever refused. Most products start this way and that is not a fault.

Push levels

POST /api/v3/catalogues/{catalogue}/inventory is what to call the moment something sells on your own site. Up to 5,000 rows, keyed by your own codes, nothing but the count.

{"levels":[{"sku":"MNG-45W","inventory":0},{"sku":"MNG-20W","inventory":37}]}

Levels are absolute, never deltas: your system is stating what it has, and a delta would drift the first time a message was delivered twice.

Availability follows the count. Unless the shop allows backorders, 0 sets the product to out of stock and stock coming back lifts it to in stock. The response names every product where that happened under derived, and one push goes to every platform the shop is on — so a sell-out reaches WhatsApp without you sending six whole products one at a time. A discontinued or preorder product is never quietly put back on sale by a delivery arriving.

Codes we do not have come back in unknown_skus rather than being ignored. A level already at that value is counted unchanged and nothing is re-published for it.

Read levels and history

GET /api/v3/catalogues/{catalogue}/inventory returns, per product, inventory (on hand), reserved (held by pending orders) and available — the difference, and the number that decides whether a customer may buy. Filter with sku[] or tracked_only=true.

GET /api/v3/catalogues/{catalogue}/products/by-sku/{sku}/movements is the audit trail: every event that moved the count, newest first, with kind (reserve, release, commit, adjust, sync), the signed quantity, what the figures became, and who or what caused it.

What an order does to stock

When an order is created — from a WhatsApp cart, your storefront, a flow, or the phone menus — units are reserved for every line we can match. If there was not enough to hold, the line is flagged stock_short, the order is flagged needs_attention, and the order is still recorded: a customer asked for it, and losing that would be worse. A line whose code is not in the catalogue is flagged unresolved for the same reason.

Moving the order to confirmed (or any later state) commits the hold — a real decrement under momo, a released hold under external. cancelled or refunded releases it. A pending order that is never answered releases on its own once the shop's reservation_ttl_hours (48 by default) has passed.

Set low_stock_threshold on the shop and you are told, once a day per product, when available reaches it.

Who may do what

An API key inherits the permissions of the person who issued it, never more. Reading needs communications.catalogue.view; writing needs communications.catalogue.manage. A key issued before that link was recorded carries no permission set to check: it keeps working on the endpoints that existed before this contract and is refused on the ones added by it. If a write starts answering 403, issue a new key from Settings → API credentials under a user who holds the permission.

Sending products into WhatsApp

/catalogues/send-product, /catalogues/send-product-list and /catalogues/send-catalogue put products into a chat. They take to for the recipient and an optional from for the sending identity, and they use the provider catalogue id and product_retailer_id, not our numeric ids.

Single product requires catalogue_id and product_retailer_id; optional body ≤ 1,024 and footer ≤ 60. Product list requires header_text ≤ 60, body ≤ 1,024 and 1–10 sections, each with a title ≤ 24 and product_items. Whole catalogue requires body and can name a thumbnail_product_retailer_id.

These call the provider directly and return data.message_id, a gateway id — not the ordinary REST message uid. No WhatsApp account configured returns 422; a provider refusal returns 502.

Orders

GET /api/v3/catalogues/orders lists orders from every platform, not only WhatsApp. platform says where each came from — whatsapp, storefront, ivr, manual — and customer_handle is whatever identifies the customer there: a WhatsApp id, a handle, an email, a typed phone number. customer_wa_id is the older name for it and has not held only WhatsApp ids for a long time.

Filter with status, platform and catalogue_id. updated_since is the cursor to poll from when you need to recover from a callback you missed; results then come newest-change-first.

PUT /api/v3/catalogues/orders/{order}/status moves an order through pending, confirmed, processing, shipped, delivered, cancelled, refunded. It records the change and moves the stock the order is holding (see above); it does not enforce a linear graph and setting refunded does not move money.

Read needs_attention before fulfilling. When it is true, at least one line is stock_short (we could not hold the whole quantity) or unresolved (the code is not in the catalogue), and a person has to decide what to do.

An order's lines and total are frozen when it is created. A later price change in the catalogue never re-prices an open order — the customer bought at the price they were shown. Keep fulfilment status separate from payment settlement: paid means money settled, not that a request was sent.

Business data

Payment states and reconciliation

The payment API lets an integration inspect money requests, settlement, refunds and the accounting entries attached to them. Both endpoints require a REST key whose issuing user currently has payments.view. They return records belonging to the key's account. A payment from another account is not returned even when you know its UUID or reference.

Find payments to reconcile

Call GET /api/v3/payments for a list ordered newest first. Pass state=open to select draft, pending and authorised payments, an individual state to select that state, or state=all for every state. An omitted state also includes everything. State input is trimmed and lowercased; an unrecognized state currently leaves the list unfiltered instead of returning a validation error. Validate state names in your client so a spelling mistake does not broaden your reconciliation query.

subject_id restricts results to a business record identifier, such as an order or data record. There is no accompanying subject-type query parameter. Inspect each result's subject_type when IDs from several record types might overlap.

curl --get 'https://business.momo.tz/api/v3/payments' \
  --header "Authorization: Bearer $MOMO_API_KEY" \
  --header 'Accept: application/json' \
  --data-urlencode 'state=open' \
  --data-urlencode 'per_page=25' \
  --data-urlencode 'page=1'

Pagination defaults to 25 and caps at 100. limit overrides per_page if both are supplied. The native response is {data:[...],meta:{current_page,per_page,total,last_page}}; there is no outer success status or data.items wrapper. Advance the page until current_page reaches last_page, and deduplicate by payment ID if records are arriving while you read.

Interpret amounts and states

Use the integer amount_minor for arithmetic and the returned amount string for display. This payment layer defines 100 minor units per major currency unit, including TZS: 4,000,000 minor units represents TZS 40,000. It does not change scale according to a currency's normal decimal convention. currency names the currency; refunded_minor and refundable_minor use the same scale. refunded is a display string when money has been refunded and null otherwise. Do not parse formatted strings back into numbers.

State Meaning and possible next states
draft Written down, not yet requested; pending, cancelled or expired can follow
pending Provider asked, customer has not settled; authorised, paid, failed, expired or cancelled can follow
authorised Provider holding money; paid, failed, expired or cancelled can follow
paid Settled; partly_refunded or refunded can follow
partly_refunded Settled with some returned; another partial refund or refunded can follow
failed Attempt failed; pending or cancelled can follow
expired, cancelled, refunded No further state transition

is_open is true only for draft, pending and authorised. is_settled means the money arrived at some point: paid, partly_refunded and refunded. It does not mean the original amount remains retained. Use refunded_minor and refundable_minor alongside it. The next_states list describes the payment state machine; it does not grant a REST write capability.

Read the full payment story

Call GET /api/v3/payments/{payment} with either the UUID or human reference, such as PAY-20301012-0001. The response is {data:{...}}. Keep the stable UUID as your primary integration key; a human reference is useful on receipts and support screens.

The summary includes payer, method, provider, attempts, last_error, expiry and settlement timestamps, subject identity, and nullable customer checkout handles. The detail adds created_by, a timeline ordered oldest first, ledger entries ordered by entry_no, and refunds ordered newest first. Timeline entries identify the old state, new state, source, message and occurrence time. Ledger entries expose their kind, debit/credit direction, account, amount and occurrence time. Read these when a state change needs explanation rather than treating last_error as a complete history.

A refund is a separate payment intent linked through refund_of, with is_refund=true and an optional reason. The original payment retains its own ID and accumulates refunded_minor. Process each refund ID once in your local accounting integration, while updating the original payment's remaining refundable amount from its current detail.

Operational boundaries

The customer REST surface provides these two reads. Collection, retry, cancellation, refund and payout actions use the authorized application or assistant workflows; there is no payment POST/PATCH action to construct from next_states. A checkout URL or token can be null when the provider has not supplied one.

Poll important pending payments with a sensible interval and respect the shared v3 rate limit. Business-event discovery currently declares payment event names with live=false; do not assume an automation payment event will replace reconciliation polling. Automation events explains how to inspect publisher availability. The messaging order.paid callback has its own payload and represents an order payment, which should be matched to payment records using the identifiers actually present.

Business data

Data tables and field types

Data tables hold the business records your account defines: bookings, customers, stock, deliveries or other structured information. Each table has its own field keys and validation rules. Discover those rules before reading or writing; a field label in the dashboard is not necessarily the key your API request must use.

Discover tables and schemas

GET /api/v3/data/tables returns {tables:[...]}. Each summary includes UUID, name, slug, description, icon, record/column counts and updated_at. The list is limited by the issuing user's permissions and table visibility.

GET /api/v3/data/tables/{table}/schema returns:

Block What to use it for
table Identity, title column, group, counts, retention and legal-hold information
columns Ordered field keys, types, required/unique flags, configuration and index status
types Supported types and the operators/UI hints each advertises
system_columns Built-in $id, $created_at, $updated_at, $source
access Current per-table access
unique_sets Combinations of fields whose values must be unique together
limits Current quota use and allowances
sort_index_threshold Size at which a user-field sort needs an index
actions Available action summaries
can Module-level management, record editing and report-management permissions

Schema reads require data.view. Record writes require data.records.edit and table write access. Table-specific grants can narrow module permissions; a hidden table is returned as 404. Re-fetch the schema after a field change or a validation response suggesting your cached rules are stale.

Field values

Type Request value and behavior
text / long_text Text within configured field limits
number JSON number or numeric string, normalized to a number; configured min/max/precision apply
currency Numeric business amount, displayed using configured currency and precision
boolean Boolean value; send JSON true/false for clarity
date / datetime A valid date/time in the field's supported format; use explicit timezone for datetime
phone Phone normalized to E.164 using the field's default region, TZ when unspecified
email Address validated by the field type
select / multi_select Configured option value, or list of option values
status Configured state key, subject to the table's state rules
relation Related record UUID; an object containing id is also accepted and normalized
file A platform file descriptor, or list when the field allows multiple
auto_number Platform-assigned text identifier; omit it from writes

The data-table currency type is not catalogue minor-unit pricing. It stores an ordinary numeric amount, defaults to TZS with zero decimal places, and can use configured precision. For example, 15000 in a TZS currency field is displayed as TZS 15,000, while catalogue product price follows its separate scaling contract.

File records contain storage metadata and return signed download URLs where allowed. A JSON record write does not upload bytes. Use the account's file tooling or an authorized MCP file operation; this REST table family does not expose a file-upload endpoint. Treat returned signed URLs as expiring access links.

Sensitive data and limits

Field masks apply to record values and titles for the current caller. A masked string is a display value, not the original secret. Avoid sending an entire read response back as an update: select only fields your workflow intends to change.

Each serialized record must fit in 8,192 bytes. Table and account quota allowances can vary; read schema limits rather than hardcoding a sample allowance. Reaching a storage/record quota produces a structured quota refusal. Tables and columns are managed through the dashboard or appropriate MCP tools, while this REST family exposes records and schema discovery.

Business data

Filters, cursors and group reports

Data-record lists use cursor pagination and a typed condition tree. Build queries with column keys from the schema. Pass values with their natural JSON types: a numeric comparison should contain a number, and an opt-in comparison should contain a Boolean.

Build a condition tree

{"all":[{"column":"opt_in","op":"equals","value":true},{"column":"balance","op":"greater_than","value":10000},{"any":[{"column":"region","op":"equals","value":"dar"},{"column":"region","op":"equals","value":"arusha"}]}]}

Send this as the URL-encoded JSON filter query parameter to GET /api/v3/data/tables/{table}/records. An absent filter or {} selects all visible records. all combines conditions with AND; any with OR. A leaf names column, op and, except for empty checks, value. Trees allow up to 40 leaves and nesting depth six.

Operator Typical value
equals / not_equals One scalar value
contains / starts_with Text
greater_than / less_than Number or supported temporal value
between Two range endpoints
in Array of accepted values
is_empty / is_not_empty No value required

Only operators advertised by the field's type are valid. A file field, for example, advertises empty checks, not text search. Unsupported operators return a structured not_supported refusal; an unknown field or malformed tree is a validation error.

Encode query parameters safely

curl --get 'https://business.momo.tz/api/v3/data/tables/TABLE_UUID/records' \
  --header "Authorization: Bearer $MOMO_API_KEY" \
  --header 'Accept: application/json' \
  --data-urlencode 'filter={"column":"opt_in","op":"equals","value":true}' \
  --data-urlencode 'sort=$updated_at' \
  --data-urlencode 'dir=desc' \
  --data-urlencode 'limit=50' \
  --data-urlencode 'with_count=1'

Replace TABLE_UUID. Single quotes preserve the literal $updated_at system key in a shell. Add q for case-insensitive search over up to the first six text, long_text, phone and email columns. A table without those fields ignores q. Search combines with the explicit filter.

Sort defaults to newest created first. A user column key, $created_at or $updated_at can be selected; dir is asc or desc, default desc, and nulls sort last. On tables at or above the schema's sort_index_threshold, a user-column sort needs its configured index. The current refusal is HTTP 501 with code not_supported and reason sort_needs_index. System timestamp sorting remains available.

Walk pages

{"records":[],"next_cursor":null,"has_more":false,"count":0,"served_at":"2030-10-12T06:00:00Z"}

Default limit is 50, clamped between 1 and 200. When has_more is true, send next_cursor back as cursor with the same filter, q, sort and dir. Treat the token as opaque. Invalid cursor text currently restarts at the first page rather than returning an error, so store it exactly and deduplicate record IDs if restarting.

count is null unless with_count is enabled. Counting runs an additional query; request it only when the workflow needs a total. Cursor pages are not a frozen export snapshot. Concurrent writes can change membership or order, so a synchronization process should retain IDs and timestamps and reconcile overlap.

Table groups and overviews

GET /api/v3/data/groups lists group metadata. GET /data/groups/{group} returns group, member tables with columns, and saved report summaries. These groups organize data tables; they are unrelated to SMS contact groups and WhatsApp chat groups.

GET /data/groups/{group}/overview accepts a named range such as last_30_days, a JSON range object, or explicit from/to. It returns totals, member-table cards, amount headlines, time series and relationship summaries. The default is last_30_days. Use the returned window and bucket when labeling charts. These REST group operations are read-only; do not infer a report-write endpoint from a saved report summary.

Business data

Records, states and audit history

Create and update records using a data object keyed by the table's column keys. Read the schema first, then send only the values your workflow controls. Required values, field types, unique constraints, status rules and table access are checked before a valid record is saved.

Create and patch

{"data":{"name":"Asha Mwinyi","phone":"0712345678","region":"dar","opt_in":true,"balance":15000}}

POST /api/v3/data/tables/{table}/records returns HTTP 201 with {record:{...}}. Unknown keys are refused. Phone, numeric and other supported fields are coerced to their canonical stored types. The record is stamped source api; its returned source records where it was created and does not change on a later edit.

PATCH /data/tables/{table}/records/{record} merges just the submitted keys:

{"data":{"region":"arusha","optional_note":null}}

Here region changes and optional_note is cleared. Other values remain. Clearing a required field fails validation. The updated response contains the full current record, including id, data, source, created_at, updated_at and title; relations may add a titles map.

The platform serializes concurrent updates to a record, but this REST API does not expose an optimistic version or If-Match precondition. If two clients update the same field, a later accepted update can overwrite it. Keep patches narrow and coordinate business decisions in your integration rather than treating updated_at as an enforced lock token.

State transitions

GET /data/tables/{table}/states describes every status field. It includes its key, label, required/strict flags, initial states and possible next moves. Each move's allowed flag reflects the caller's permission. Use state keys, not translated display labels.

A record must start in an allowed initial state when the state machine requires one. A PATCH to a status field is checked against the current record: illegal transitions return a conflict, and transitions requiring a permission the caller lacks return a permission refusal. Reading the states endpoint does not reserve a transition; another writer may change the record before your PATCH.

Use an authorized MCP transition tool when you need its explicit transition/reason workflow. This REST family does not expose a separate transition or rollback POST endpoint.

Structured failures

{"error":{"code":"conflict","message":"The record was refused. A unique value is already used. Nothing was saved.","field":"phone","retryable":false},"message":"A record with this Phone already exists.","code":"conflict","errors":{"phone":["A record with this Phone already exists."]}}

This abbreviated example shows the shape; human messages and optional details depend on the refusal. Branch on the nested error code and retryable flag, not English text.

Code HTTP Action
validation_error 422 Correct keys, values or request structure
conflict 409 Resolve unique-value/state conflict before trying again
not_found 404 Recheck table and record identity
permission_denied 403 Resolve access or transition permission
quota_exceeded 402 Resolve the allowance; details identify quota/used/limit
not_supported 501 Change operation or prepare the needed index
rate_limited 429 Wait for retry_after_seconds / Retry-After
provider_failure 502 Inspect the external operation and its outcome
temporary_failure 503 Retry only when safe for that operation

Missing data itself is framework validation and can use {status:"error",message,errors} instead. Handle both error families.

Delete and audit

DELETE the record URL returns {ok:true,deleted:1}. It soft-deletes the record, removes it from ordinary lists/reads and frees its record quota contribution. Repeating deletion returns 404; this is not evidence that the earlier delete failed.

GET {record}/history returns history entries, has_more, next_before and field-label columns. Pages contain up to 50 entries, newest first; return next_before as before without reformatting it. Entries name the record, action, changed fields, before/after values, actor, source, optional reason and timestamp. History remains readable after deletion, subject to table access. It is an audit trail, not a recoverable record-version API, and an empty trail does not establish that a current record exists.

Integration operations

Receiving and verifying webhooks

This chapter documents communication webhooks — messages, orders, catalogue, groups. Automation subscription webhooks have their own signature, envelope and retry rules.

Webhooks are HTTP requests from Momo Business to a receiver you operate. They are not endpoints you call on /api/v3. Use them to learn about events the moment they happen, then reconcile important state through the corresponding read endpoints.

What we can send

GET /api/v3/webhooks/events lists every event, grouped, with a complete sample payload for each. The picker in Settings → Webhooks shows the same list, and the event enum in this API's delivery schema is generated from it — so a name you see in any of the three is one the platform actually sends.

Group Events
Orders order.received, order.status_changed, order.cancelled, order.paid
Catalogue sync.completed, stock.low, product.blocked
Messages message.received, message.sent, message.delivered, message.read, message.failed, message.echoed, message.updated
Campaigns campaign.completed, campaign.failed
WhatsApp groups group.created and eleven more
Webhooks webhook.paused — sent to your other endpoints when one is paused

order.received fires for an order from any platform — a WhatsApp cart, your storefront, a flow, the phone menus — and carries order.stock_policy, which tells you whether you are expected to decrement. order.status_changed fires from every path that changes a status: the dashboard, the staff app, the API, an agent. Neither existed as a reliable signal before; both are what a store integration lives on.

Register a receiver

From the dashboard: Settings → Webhooks, needs webhooks.manage. From the API:

POST /api/v3/webhooks
{"url":"https://store.example.com/momo","events":["order.received","order.status_changed","stock.low"]}

Private and internal targets are refused; the hostname is checked again on every delivery attempt; redirects are not followed.

The response carries secret — the one time it is ever shown. Store it in your receiver's environment. POST /api/v3/webhooks/{webhook}/rotate-secret issues a new one the same way; the old one stops verifying immediately, so update the receiver first and rotate second. GET, PATCH and DELETE on /api/v3/webhooks/{webhook} do what they say; a key needs webhooks.view to read and webhooks.manage to change.

Verify a delivery

Every delivery carries five headers:

X-Signature:     <hex HMAC-SHA256 of the raw body>
X-Signature-V2:  sha256=<hex HMAC-SHA256 of "{X-Timestamp}.{X-Delivery-Id}.{raw body}">
X-Timestamp:     1757581442
X-Delivery-Id:   dlv_01j9qk3v8x2m7n4p5r6s
X-Event:         order.received
Content-Type:    application/json

X-Signature is unchanged from the first version of this contract, so a receiver written against it keeps working. Prefer X-Signature-V2: checking it gives you replay protection for free — refuse a timestamp more than five minutes old, and remember delivery ids you have already processed. X-Delivery-Id is the same across every retry of one delivery and different for a replay, so it is the right key to dedupe on.

Verify over the raw bytes, before parsing or re-serialising:

$raw = file_get_contents('php://input');
$secret = getenv('MOMO_WEBHOOK_SECRET');
$ts = $_SERVER['HTTP_X_TIMESTAMP'] ?? '';
$id = $_SERVER['HTTP_X_DELIVERY_ID'] ?? '';
$given = $_SERVER['HTTP_X_SIGNATURE_V2'] ?? '';

if (abs(time() - (int) $ts) > 300) { http_response_code(401); exit; }

$expected = 'sha256='.hash_hmac('sha256', "$ts.$id.$raw", $secret);
if (!hash_equals($expected, $given)) { http_response_code(401); exit; }

if (alreadySeen($id)) { http_response_code(200); exit; }   // a retry you already handled

$event = json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
enqueue($event);              // persist first, process later
markSeen($id);
http_response_code(200);
// Node
const crypto = require('crypto');
const ts = req.get('X-Timestamp'), id = req.get('X-Delivery-Id');
const expected = 'sha256=' + crypto.createHmac('sha256', process.env.MOMO_WEBHOOK_SECRET)
  .update(`${ts}.${id}.${rawBody}`).digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(req.get('X-Signature-V2') || ''))) return res.sendStatus(401);
# Python
import hmac, hashlib
expected = 'sha256=' + hmac.new(SECRET.encode(), f"{ts}.{delivery_id}.".encode() + raw_body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, request.headers.get('X-Signature-V2', '')): abort(401)

Order payloads

{"event":"order.received","timestamp":"2026-09-11T09:14:02+00:00",
 "order":{"id":9182,"catalogue_id":42,"platform":"whatsapp","status":"pending","needs_attention":false,
          "stock_policy":"external","customer_handle":"255712345678","customer_name":"Asha Mrisho",
          "lines":[{"sku":"MNG-45W","name":"Charger Mango 45W","quantity":1,"unit_price_minor":3900000,
                    "line_total_minor":3900000,"currency":"TZS","reserved":1,"stock_short":false,"unresolved":false}],
          "total_minor":3900000,"currency":"TZS","conversation_id":771,"priced_at":"2026-09-11T09:14:02+00:00"}}

Read order.lines[].sku and quantity to decrement your own stock when stock_policy is external. needs_attention means a line is stock_short (we could not hold the whole quantity) or unresolved (the code is not in the catalogue) — a person has to look before it ships. priced_at records that the lines and total were frozen at creation; a later catalogue price change never re-prices an open order.

order.status_changed carries the same order object plus previous_status. order.cancelled is the same again, sent only on cancellation, for receivers that care about nothing else. Any stock the order was holding has already been released by the time you receive it.

The older top-level names — order_id, customer_wa_id, product_items, total_amount, total_currency — are still present in every order event for one release. Move to order.*; they are marked deprecated in the spec.

order.paid is unchanged: order_id, payment_id, method, amount_minor, currency, payer_msisdn, paid_at, conversation_id. It means money settled, not that a request was sent.

Catalogue payloads

sync.completed carries the same report as GET /api/v3/catalogues/{catalogue}/syncs/{sync} under sync, minus problems — that list can be two hundred rows, so read it from the endpoint when rejected or a platform's blocked count is non-zero. product.blocked fires once per product per platform that refused it, with the problem in words. stock.low fires once per product per day when available reaches the shop's low_stock_threshold.

Message and group payloads

Message events are flat: message_id, direction, sender, recipient, status, body, media_url, channel_type. Use the numeric message_id with the SMS/WhatsApp read endpoint for the full record. Group events carry a group summary; use its id to fetch current state.

Delivery, retries and the log

Every delivery is a record. GET /api/v3/webhooks/{webhook}/deliveries lists them newest first — what was sent, how many times, the last HTTP status and the first kilobyte of what your receiver said. Filter by status, event or since. The same log is under each endpoint in Settings → Webhooks, with per-endpoint counts for the last 24 hours.

Retries. A 5xx, a timeout or a connection failure is retried five times over about six hours: after 1 minute, 5, 30, 2 hours, 6 hours. A 4xx other than 408, 425 or 429 is treated as "understood and refused" and is not retried — retrying a 401 five times changes nothing.

Pausing. After fifty consecutive failures the endpoint is paused: deliveries are recorded as skipped rather than sent, the account is notified, and your other endpoints receive webhook.paused. Fix the receiver, then replay any failed delivery — POST /api/v3/webhooks/{webhook}/deliveries/{delivery}/replay, or the Replay button in the log — and the endpoint resumes. Setting is_active: true again does the same.

Answer 2xx quickly. Persist or enqueue the event, then acknowledge; keep downstream work outside the request. Delivery is at-least-once and ordering across events is not guaranteed, so make repeated status updates harmless and dedupe on X-Delivery-Id. When you need to recover from an outage on your side, GET /api/v3/catalogues/orders?updated_since= is the cursor to poll from.

Integration operations

Business events, subscriptions and schedules

The automation REST API provides three reads: business events, event subscriptions and recurring schedules. Each requires automations.view on the REST key's current issuing user. Configure or change automations through the authorized application or MCP tools; these REST routes do not create, pause, retry or delete them.

Poll the business event log

GET /api/v3/automations/events returns {events,has_more,next_before,event_keys}. Each event carries a stable UUID, key and label, subject_type and subject_id, payload, actor, occurred_at, delivered_at and delivered_count. Store the event UUID to make repeated polling harmless. The payload depends on the event key; read the relevant subject resource when you need its current complete state. Payloads exceeding the publisher’s 64,000-byte budget lose the largest top-level keys and carry __truncated listing removed keys. A record change can emit both record.transitioned and record.updated when it changes a status and another field; a no-op emits neither.

curl --get 'https://business.momo.tz/api/v3/automations/events' \
  --header "Authorization: Bearer $MOMO_API_KEY" \
  --header 'Accept: application/json' \
  --data-urlencode 'key=record.transitioned' \
  --data-urlencode 'limit=50'

The optional key must be a known event name or the API returns 422. Omit it to read all keys; * is for subscriptions, not this query. subject_id narrows events to one subject ID. limit is clamped to 1–50 and defaults to 50. Results are newest first. To go backwards, pass next_before as before, preserving its timezone and URL encoding.

The cursor is a strict timestamp comparison, not an opaque unique position. An unreadable before value currently restarts at the newest page. A full page sets has_more=true even when there may be no subsequent rows, so an additional empty page is normal. Events sharing the boundary timestamp are not distinguished by an ID tie-breaker; do not use this API as a guaranteed lossless bulk export under high event volume. Keep a local event audit, deduplicate refreshed pages and reconcile important source records. delivered_at indicates fan-out processing, while delivered_count counts successful dispatch outcomes, including queued webhook/agent jobs; an external webhook job can still be pending or later fail.

Discover which events actually publish

Every page includes event_keys metadata: key, label, group, subject, publisher and live. A declared event with live=false is part of the vocabulary but its publisher is not currently active. Read this flag instead of assuming every listed key emits.

Family Declared keys Current publisher availability
Data records record.created, record.updated, record.deleted, record.transitioned Live
Approvals approval.requested, approval.settled Live
Payments and orders payment.paid, payment.failed, payment.refunded, order.completed Declared, not live
Other operations booking.confirmed, ticket.opened, ticket.closed, call.completed, message.received Declared, not live

The business-event message.received name belongs to this bus. The existing messaging webhook dispatcher can independently emit its own message.received callback; the availability and delivery rules of the two systems are separate.

Inspect subscriptions and failures

GET /api/v3/automations/subscriptions returns {subscriptions:[...]}, ordered by key and label, capped at 200 without pagination. key=record.created includes both matching subscriptions and wildcard subscriptions. enabled_only=true leaves out disabled subscriptions. Unknown key strings are not rejected on this read; matching wildcard rows can still appear.

Each subscription has an ID, key, kind, target, label, optional filter/config, enabled flag and counters. Kinds are flow, notification, webhook and agent. A target means a flow or agent ID, a notification audience, or a webhook URL according to kind. Conditions apply to the event payload. signed reports that a signing secret exists; the secret itself is never returned by this read.

Inspect last_error, last_failed_at and failure_count together. A failure count reaching ten switches a subscription off. Successful fan-out dispatch resets the count; for a webhook this includes being queued, before the HTTP delivery result is known. Confirm external receipt in your receiver audit. Paused subscriptions require an authorized change through the application or MCP. For webhook subscriptions, intermediate retries do not each increment failure_count: the final failed delivery does. Watch enabled as well as last_fired_at so an old successful timestamp does not hide a disabled subscription.

Business webhook signature and retry contract

Automation webhook subscriptions use X-Momo-Signature, a different protocol from communication webhooks:

X-Momo-Signature: t=1918015200,v1=HEX_HMAC_SHA256
X-Momo-Event: record.created
X-Momo-Event-Id: EVENT_UUID
X-Momo-Subscription: 12
X-Momo-Attempt: 1

Compute HMAC-SHA256 over <timestamp>.<raw request body> with the subscription secret. Compare hexadecimal digests in constant time and check the signed timestamp against a short clock tolerance; the platform verification helper defaults to 300 seconds. Use the original bytes, not re-encoded JSON. Provision the secret when the subscription is authored or rotated; the REST list only reports its presence.

{"id":"01953b60-4ce0-7000-8000-000000000001","event":"record.created","occurred_at":"2030-10-12T06:00:00+00:00","tenant_id":42,"subject":{"type":"data_record","id":"01953b60-4ce0-7000-8000-000000000002"},"actor":{"kind":"api","label":"ERP integration","id":7},"data":{"table":{"id":"01953b60-4ce0-7000-8000-000000000003","name":"Customers","slug":"customers"},"record_id":"01953b60-4ce0-7000-8000-000000000002","record":{"name":"Example"},"source":"api"},"subscription":{"id":12,"label":"Forward record changes"}}

This example illustrates the envelope; data and actor contents depend on the publisher. Deduplicate by event ID for each subscription you process. Acknowledge with a 2xx response after durable acceptance. The job makes up to six attempts, with a 15-second HTTP timeout. Transport failures, HTTP 408, 429 and 5xx can retry. Other refusals, including 401/403/404 and validation errors, are not retryable. Default delays are 10, 20, 40, 80 and 160 seconds between attempts. A positive numeric Retry-After overrides the next delay, capped at 300 seconds; HTTP-date Retry-After is not interpreted. A new signature timestamp is generated for each attempt. Queue availability can extend actual delivery time.

Read recurring schedules

GET /api/v3/automations/schedules returns {schedules:[...]}, ordered by name, capped at 300 without pagination. Filter by kind=flow, report, record, call or message and optionally enabled_only=true. An unknown kind matches no rows rather than producing validation errors.

Show describes as the readable rhythm and retain spec for structured inspection: every, unit, at, weekdays, day_of_month, timezone and optional until/count. The occurrence timestamps are separate from the configured timezone. Read enabled and next_run_at together; a null next run can indicate a disabled, exhausted or invalid schedule. An invalid stored specification remains readable with an explanatory describes sentence so an owner can repair it.

last_result carries the latest execution outcome, with optional misfire details; last_error and run_count provide operational context. A run receipt is not necessarily the final delivery status of the message, call or flow it started. Follow the resulting reference into that resource's API when available.

The misfire policy controls missed occurrences: run_once performs one late run then advances; skip advances without running missed work; run_all replays missed slots, capped at 12 per runner tick. A slot less than or equal to 90 seconds late is still considered on time. These recurring schedules are distinct from a one-off scheduled message or campaign. Inspect the correct resource when diagnosing an unexpected send time.

Integration operations

Agent tasks and run tracking

Agent tasks let an integration ask a configured account agent to perform work and then inspect the execution. They use the same REST Bearer key as /api/v3, but their URLs and response envelopes are separate: POST /api/engine/tasks and GET /api/engine/runs/{uuid}.

Prepare an agent

Choose an agent belonging to the authenticated account. The account's engine must be enabled, the agent must have an enabled engine profile, and its API availability must be enabled. Agent tools, permissions and spending limits remain governed by its configuration. A run can be denied even when authentication and the request fields are valid.

Agent tasks execute the configured agent and may incur model/tool usage. This is an execution interface, not a free validation endpoint. Read the returned usage and run cost fields when reconciling your integration's activity.

Submit work

{"agent_id":42,"prompt":"Summarize the supplied order details and return the next action.","context":{"order_reference":"ORD-1042","customer_note":"Please arrange collection tomorrow."},"mode":"queued","max_duration_ms":30000}
Field Contract
agent_id Required integer; agent must belong to the key's account
prompt Required string, maximum 20,000 characters
context Optional JSON object/array of context supplied to the task
mode sync or queued, default sync
max_duration_ms Optional positive integer execution budget

Use queued mode when the caller can poll. A successful queued acceptance returns 202 with status, run_uuid, execution_state, delivery_state, deadline_at, status_url and denial_reason. A denied or already timed-out submission can return 422 instead. Retain the run UUID regardless of the status so you can inspect what happened.

The API adapter has a 60-second ceiling. The requested duration and configured profile duration can reduce that budget; a large max_duration_ms does not extend it. Queued work still has a deadline. Scheduling it in the background is not a request to wait indefinitely for capacity.

Idempotency belongs to this endpoint

curl 'https://business.momo.tz/api/engine/tasks' \
  --header "Authorization: Bearer $MOMO_API_KEY" \
  --header 'Accept: application/json' \
  --header 'Content-Type: application/json' \
  --header 'Idempotency-Key: order-1042-next-action-v1' \
  --data '{"agent_id":42,"prompt":"Summarize this order.","context":{"order_reference":"ORD-1042"},"mode":"queued"}'

Idempotency-Key is optional, 1–191 characters when supplied. The platform scopes it to the tenant and task trigger. Repeating the key with the same request fingerprint returns the existing run; using it for different agent/prompt/context or other fingerprinted task inputs returns 409. Reuse the same key after an uncertain response, and use a new key for genuinely new work. No fixed expiry period for this key is promised by this contract.

Do not transfer that assumption to ordinary SMS/WhatsApp POSTs, which lack request-key deduplication. See delivery retries.

Poll and distinguish outcome from transport

GET status_url or /api/engine/runs/{uuid} returns run, children and steps. The run includes execution/delivery state, deadline, output, denial_reason, provider/model, token counts, cost/currency, duration and creation time. Steps expose position, kind, tool_name, arguments, result_preview, status and duration. Treat these as potentially sensitive business data when logging.

Sync mode returns HTTP 200 with status, run_uuid, output, denial_reason and usage. A successful run reports succeeded. A 200 can also contain failed, denied, timed_out or handoff; inspect status. Background progress can include queued, running, waiting_tools, waiting_children, awaiting_human, retry_scheduled and finalizing. An awaiting-human state needs its approval workflow, not repeated submission.

Capacity refusal returns 429 with Retry-After 5; temporary ingress contention returns 503 with Retry-After 1. A different payload under an existing key returns 409. Correct account/profile denials before retrying. Agent responses are not wrapped in the ordinary v3 success envelope, and the shared v3 120/minute route throttle is not the rate contract for this separate route group.

Integration operations

MCP connections and OAuth

MCP connects an assistant to account tools through Streamable HTTP and JSON-RPC. The REST reference lists transport endpoints; the tool catalogue below lists tool names, arguments and permissions. A tool is invoked through tools/call, not through an invented REST URL named after the tool.

Choose a connection URL

URL Purpose
/mcp Aggregate account connection, composed from granted domains
/mcp/v1/{server} A narrower connection to one domain
GET /mcp/v1 Authenticated server-discovery information
GET /api-docs/mcp.json Public tool manifest with schemas

Use /mcp when the client supports one connector URL for the product. Use a domain URL when you want a focused connection such as messaging, contacts, data, calls or orders. Read available server keys from discovery/the catalogue rather than assuming a product label is the URL key.

The public manifest describes the platform's catalogue. The tools actually available to a connection depend on its granted servers, scopes, current issuer permissions and feature availability. Call tools/list after connecting and use the returned names and schemas. Aggregate tool names can differ from their per-domain names; do not move a domain tool name to /mcp without discovering it there.

Static MCP credentials

Open API credentials and create an MCP connection. Choose a name, server list and permission preset. Optional expiry is 1–730 days; omitted expiry leaves no expiry date. The token starts with momo_mcp_ and is revealed once. The page returns configuration for the selected domains.

{"mcpServers":{"momo-messaging":{"type":"http","url":"https://business.momo.tz/mcp/v1/messaging","headers":{"Authorization":"Bearer YOUR_MCP_CONNECTION_TOKEN"}}}}

Keep the credential in your client's supported secret configuration. Its permissions are intersected with what the issuing user may currently do. Revoking the connection stops subsequent access. A REST API key is rejected here even if it belongs to the same tenant.

OAuth connection flow

Hosted clients can discover OAuth metadata at /.well-known/oauth-protected-resource and /.well-known/oauth-authorization-server. The authorization-server document publishes the actual authorize, token and registration URLs. Use those values rather than constructing provider URLs yourself.

  1. Register the client through /oauth/register when dynamic registration is needed. The registration endpoint is anonymous but subject to redirect-URI restrictions and a 10/minute/IP throttle.
  2. Generate a PKCE verifier and S256 challenge, then open the authorization endpoint with response_type=code, client_id, redirect_uri, scope, state and the challenge parameters.
  3. The account user signs in and selects capabilities/elevations at consent. Verify the returned state before accepting the authorization code.
  4. Exchange the code at /oauth/token using application/x-www-form-urlencoded, including grant_type=authorization_code, code, client_id, redirect_uri and code_verifier.
  5. Send the resulting access token as Bearer to the MCP URL. Refresh through the same token endpoint with grant_type=refresh_token and the returned refresh token.

OAuth access tokens are configured for one hour and refresh tokens for 30 days. Respect returned expires_in and any replacement refresh token. This is an authorization-code flow with PKCE; do not invent a client-credentials grant or treat a static API key as an OAuth client secret.

Capabilities and elevations

OAuth requires mcp:use and grants capabilities such as messaging, calls, data or commerce. Elevations add actions with wider consequences: mcp:send, mcp:publish, mcp:spend, mcp:delete, and data-specific mcp:write/mcp:shape. A capability alone does not grant every sensitive action in that domain.

Data record writes need the data capability and write elevation; schema changes need shape. Deletion tools require their underlying write/shape permission and the delete elevation. The consenting user's current permissions remain the ceiling, so an owner cannot grant a connection more authority than the user actually holds.

Tool calls and errors

Follow the MCP client's initialization handshake and negotiated protocol version, then discover tools. A JSON-RPC request has jsonrpc 2.0, an id, method and params. Read the tool's inputSchema and annotations before calling it; some writes create drafts, others publish, send or spend immediately.

The MCP route limiter allows 120 requests/minute per static credential, falling back to the OAuth user's identity where applicable. A throttled response is HTTP 429 with JSON-RPC error code -32003. Back off rather than looping. Permission, server-grant, expired-token and account-state failures need configuration changes. For a successful HTTP response, also inspect JSON-RPC errors and the tool's isError result.

Cache the public manifest with its ETag if useful, but refresh runtime tool discovery when a connection or grant changes. Per-tool schema/version metadata helps detect contract changes; it is not a promise that an old input remains valid after a schema update.

Guide

Authentication

Send Authorization: Bearer <REST key>. Create and revoke keys at /app/api-credentials; the plaintext is returned once. Tenant identity comes from the key, never a request tenant_id.

Data and WhatsApp group endpoints check the key issuer's current permissions; a missing/deactivated issuer is refused there. Legacy message, campaign, contact, catalogue and profile controllers use the tenant credential without this per-action permission map. Treat REST keys as powerful credentials.

Missing, invalid, revoked, expired or wrong-kind REST credentials return 401. A suspended/inactive account returns 403. REST API keys and MCP credentials cannot be interchanged.

Guide

WhatsApp groups

Groups of up to 8 people created from a business number. Invite-only: you send the link, they choose to join. Needs an Official Business Account. Group events also arrive as webhooks (group.created, group.participant_joined, …).

A key here inherits the permissions of the user who created it: communications.groups.view to read, communications.groups.manage to change a group, and both that and communications.send to post into one. A key with no creator on record is refused.

Guide

Catalogue

Read existing shops, manage products, send WhatsApp product messages and inspect customer orders. Resource paths use local numeric IDs; commerce sends use provider catalogue IDs and product retailer IDs. Product price/sale_price and order total_amount use integer hundredths; these differ from data-table currency fields.

Product send endpoints call the provider directly and return its message_id, not a local Message UID. Product synchronization is not an atomic transaction across the local store and provider. Order status updates accept the documented enum and record history without enforcing a linear transition graph or performing a payment refund.

Guide

Data tables

Read schemas and operate on account-defined business records. Tables, groups and records use UUIDs. Read permissions are data.view; record writes need data.records.edit plus table access. Schema responses describe types, masks, grants, unique sets, quotas and state rules.

Record pages use next_cursor/has_more, with a default limit of 50 and maximum 200. Record PATCH merges submitted keys, with null clearing an optional value. Source stays fixed at creation. State changes are enforced; history is an audit trail, not a record-version rollback API.

Data responses have native envelopes. Expected refusals contain a structured error object: validation_error 422, conflict 409, quota_exceeded 402, not_found 404, permission_denied 403, not_supported 501, rate_limited 429, provider_failure 502 or temporary_failure 503. Framework/authentication failures can use the standard v3 error envelope.

Guide

Operations

The named things this business can do — create a booking, register a customer, process a refund — each written down once by the business and callable from a chat flow, a phone menu, an assistant or your own code. An operation validates its inputs before anything happens, runs its steps inside a compensating transaction, and records every run with its inputs, its outputs and per-step timing.

This is the one write these platform phases added to v3, and deliberately: an operation can only do what somebody in the workspace already defined for it, so a key calling create_booking cannot make it do anything but create a booking. The definitions themselves are written on the Operations page or through an MCP connection — never with a long-lived key.

Guide

Agent tasks

Submit prompts to configured account agents and poll run progress. Uses a REST API key at /api/engine, with native run envelopes and optional Idempotency-Key protection.

The account engine, agent profile and API availability must permit execution. Tasks may incur usage. Queued acceptance is 202; sync HTTP 200 still requires checking the run status. This route group is separate from the v3 shared throttle and error renderer.

Guide

MCP

The same account, the same permissions, reached by a language model instead of by your own code.

MCP — the Model Context Protocol — is not a REST API, and this document does not pretend that it is. One MCP server is one HTTP endpoint speaking JSON-RPC 2.0: the operation is the method in the body rather than the URL, the tools are discovered at runtime with tools/list, and each tool's arguments are a JSON Schema rather than path, query and body parameters.

Writing 305 tools as 305 near-identical POST operations would validate perfectly and teach nobody anything. So what is documented under this tag is the transport — the 29 servers' endpoints, the envelope, the OAuth handshake and where the tool contract lives. The tool contract itself is GET /api-docs/mcp.json, which carries a full JSON Schema per tool and is generated from the same code as this section.

Which one should I use

The REST API when your own code drives the interaction — a cron job, a webhook handler, your backend. You know before you deploy which call you want to make, so a fixed contract is exactly what you want.

MCP when a language model drives it — Claude, ChatGPT, or an agent you built. It chooses the call at runtime from what tools/list told it, which is only possible because the tool list is negotiated rather than compiled in.

They reach the same data and enforce the same permissions. What differs is who is holding the wheel.

Where each REST tag lands in MCP

REST tag MCP server Endpoint
Authentication account /mcp/v1/account
SMS messaging /mcp/v1/messaging
WhatsApp messaging, inbox /mcp/v1/messaging, /mcp/v1/inbox
WhatsApp groups groups /mcp/v1/groups
WhatsApp templates messaging /mcp/v1/messaging
Contacts contacts /mcp/v1/contacts
Catalogue shop, orders /mcp/v1/shop, /mcp/v1/orders
Profile & Balance overview, account /mcp/v1/overview, /mcp/v1/account
Webhooks

Webhooks have no MCP equivalent, and will not. MCP is request/response with the model asking; Momo Business calling you when something happens stays an HTTP callback.

Reachable only over MCP today: ivr, flows, data, approvals, payments, automations, alerts, operations, studio, numbers, agents, tickets, kb, content, calls, routing, meetings, comments, posts, accounts, navigate.

Authenticating

Two credentials reach the same endpoints, and both resolve to the account's identity narrowed to what was actually granted.

  • Authorization: Bearer momo_mcp_… — an MCP connection from Dashboard → Settings → API credentials. For Claude Code, Claude Desktop, a self-hosted agent or curl. A v3 API key is refused here: same table, very different blast radius.
  • OAuth 2.1 with dynamic client registration — for claude.ai and ChatGPT, which have nowhere to paste a static token. Discovery, registration, authorization code with PKCE (S256), then the same bearer header.

Scopes come in two kinds, and the split is the safety model: a capability says which part of the business, an elevation says how far — publish, send, spend, delete, and for the data tables write and shape — and crosses every capability granted.

Scope Grants On the consent screen
mcp:overview Overview and analytics ticked
mcp:calls Calls ticked
mcp:routing Call routing off
mcp:numbers Phone numbers off
mcp:meetings Meetings off
mcp:builders Call flows and chat flows off
mcp:data Data tables off
mcp:studio Voice and audio off
mcp:contacts Contacts ticked
mcp:agents AI agents off
mcp:commerce Orders and shop off
mcp:support Support tickets ticked
mcp:accounts Connected accounts ticked
mcp:approvals Approvals off
mcp:payments Payments off
mcp:automations Automations off
mcp:alerts Alerts and service levels off
mcp:operations Operations off
mcp:navigate Finding things ticked
mcp:messaging Messaging off
mcp:inbox Inbox off
mcp:comments Comments off
mcp:posts Posts off
mcp:groups WhatsApp groups off
mcp:publish Publish things never ticked
mcp:send Send messages and place calls never ticked
mcp:spend Start purchases and ask customers to pay never ticked
mcp:delete Delete things never ticked
mcp:write Save and change records never ticked
mcp:shape Change tables and fields never ticked
mcp:automate Set up things that run without you never ticked
mcp:approve Answer approvals for you never ticked

Whatever is granted is still intersected with what the consenting person can do. Scopes are a request; permissions are the ceiling.

API REFERENCE / SMS

Send an SMS

POST/api/v3/sms/send

Creates one record per normalized recipient and normally attempts provider delivery synchronously, sequentially. HTTP 201 can contain sent or failed records; inspect every status/error_message. Future schedules, admission deferral or fallback can return queued. Repeating this POST is not protected by a request Idempotency-Key. Use a nonempty message. SMS types plain/text/sms are supported; other types require media_url and provider media support.

AuthenticationTenant API token

Request body

application/json · required

recipientstringoptional
Recipients separated by commas, semicolons or whitespace. Combined with recipients and deduplicated by exact string.
maxLength
4000
recipientsarray<string>optional
Additional recipient strings; entries also split on commas, semicolons and whitespace. Can be used together with recipient.
items.maxLength
191
sender_idstringoptional
Optional approved sender ID, tenant-owned SMS-capable number, or active short code. Unknown or ambiguous identities are rejected. `GET /api/v3/sms/senders` lists every usable identity, with `value` being what to pass here.
maxLength
64
typestringoptional
Message type (e.g. plain).
maxLength
60
messagestringoptional
Message text, maximum 4096 characters. Supply meaningful nonempty text; the current controller substitutes a generic body if absent.
maxLength
4096
schedule_timestringoptional
Optional ISO datetime for scheduled send.
maxLength
100
bodystringoptional
Alias of `message`, for clients that already speak that field. `message` wins if both are sent.
maxLength
4096
message_typestringoptional
Takes precedence over type. Defaults to plain for SMS and text for WhatsApp; supplied payload objects determine provider send behavior.
maxLength
60
media_urlstringoptional
Publicly reachable media to attach. Turns the send into an MMS-style message on gateways that support one.
format
uri
maxLength
2048
media_typestringoptional
Media kind (image, video, document…). Defaults to the message type.
maxLength
32
Provide at least one of these alternatives

recipient

recipients

Complete request schema
{
    "type": "object",
    "properties": {
        "recipient": {
            "type": "string",
            "description": "Recipients separated by commas, semicolons or whitespace. Combined with recipients and deduplicated by exact string.",
            "maxLength": 4000
        },
        "recipients": {
            "type": "array",
            "items": {
                "type": "string",
                "maxLength": 191
            },
            "description": "Additional recipient strings; entries also split on commas, semicolons and whitespace. Can be used together with recipient."
        },
        "sender_id": {
            "type": "string",
            "description": "Optional approved sender ID, tenant-owned SMS-capable number, or active short code. Unknown or ambiguous identities are rejected. `GET /api/v3/sms/senders` lists every usable identity, with `value` being what to pass here.",
            "maxLength": 64
        },
        "type": {
            "type": "string",
            "description": "Message type (e.g. plain).",
            "maxLength": 60
        },
        "message": {
            "type": "string",
            "description": "Message text, maximum 4096 characters. Supply meaningful nonempty text; the current controller substitutes a generic body if absent.",
            "maxLength": 4096
        },
        "schedule_time": {
            "type": "string",
            "description": "Optional ISO datetime for scheduled send.",
            "maxLength": 100
        },
        "body": {
            "type": "string",
            "maxLength": 4096,
            "description": "Alias of `message`, for clients that already speak that field. `message` wins if both are sent."
        },
        "message_type": {
            "type": "string",
            "description": "Takes precedence over type. Defaults to plain for SMS and text for WhatsApp; supplied payload objects determine provider send behavior.",
            "maxLength": 60
        },
        "media_url": {
            "type": "string",
            "format": "uri",
            "maxLength": 2048,
            "description": "Publicly reachable media to attach. Turns the send into an MMS-style message on gateways that support one."
        },
        "media_type": {
            "type": "string",
            "maxLength": 32,
            "description": "Media kind (image, video, document\u2026). Defaults to the message type."
        }
    },
    "anyOf": [
        {
            "required": [
                "recipient"
            ]
        },
        {
            "required": [
                "recipients"
            ]
        }
    ],
    "description": "Supply recipient and/or recipients. Inputs are merged and exact duplicates removed. Message/body and payload combinations follow this endpoint description."
}
Single recipient (most common)
{
    "recipient": "255700111222",
    "sender_id": "MyBrand",
    "message": "Hello from Momo Business \u2014 your verification code is 4821."
}
Multiple recipients (comma-separated)
{
    "recipient": "255700111222,255700111223,255700111224",
    "sender_id": "MyBrand",
    "message": "Branch closed early today \u2014 back tomorrow at 8am."
}
Multiple recipients (array form)
{
    "recipients": [
        "255700111222",
        "255700111223",
        "255700111224"
    ],
    "sender_id": "MyBrand",
    "message": "Reminder: payment due tomorrow."
}
Scheduled send (queue for later)
{
    "recipient": "255700111222",
    "sender_id": "MyBrand",
    "message": "Good morning! Your appointment is at 10am.",
    "schedule_time": "2030-10-12T09:00:00+03:00"
}
Long Unicode message (will be split into multiple SMS segments)
{
    "recipient": "255700111222",
    "sender_id": "MyBrand",
    "message": "Mteja mpendwa, asante kwa kutembelea duka letu. Tunakushukuru kwa upendeleo wako wa kuendelea kununua bidhaa zetu. Tafadhali piga 0700123456 kwa msaada zaidi."
}

Responses

201One message record per recipient. `data.messages[].status` is the delivery state at the moment we answered; watch the `message.*` webhooks for what happens after.
statusstringrequired
Always "success" on a 2xx response.
enum
["success"]
dataobjectrequired
The messages this call created.
Show child properties
messagesarray<object>required
One record per recipient, in the order they were given.
Show child properties
idintegerrequired
Internal numeric id.
uidstringrequired
Public unique identifier (e.g. msg_01JXYZSMS01).
directionstringrequired
Whether you sent the message (`outbound`) or received it (`inbound`).
enum
["inbound","outbound"]
channel_typestringrequired
Channel: sms or whatsapp.
enum
["sms","whatsapp"]
tenant_channel_idintegeroptional
The account channel selected automatically by the outbound routing policy.
channel_codestring | nulloptional
Resolved channel code returned for observability; it is not caller-selectable.
senderstring | nulloptional
Sender identity; inbound messages can contain the customer phone or provider identity.
recipientstringrequired
Recipient phone number (E.164 or national).
bodystringrequired
Message text content.
statusstringrequired
Delivery status. Outbound messages walk queued → processing → sent → delivered → read, or stop at failed with `error_message` set; `received` is what inbound messages carry.
enum
["queued","processing","sent","checking_delivery","delivered","read","failed","received"]
media_urlstring | nulloptional
The attached file, when the message carries one.
media_typestring | nulloptional
The kind of attached media (image, video, audio, document, sticker).
gateway_message_idstring | nulloptional
Provider message ID, used for replies/reactions. Customer message webhooks identify local records with numeric message_id instead.
error_messagestring | nulloptional
Why the send failed, straight from the gateway. Null unless `status` is `failed`.
metadataobject | nulloptional
Anything extra recorded with the message — the interactive or reaction payload, the id it replies to, the source that created it.
additionalProperties
true
template_paramsobject | nulloptional
The template name, language and variables used, when the message was sent from a template.
additionalProperties
true
created_atstring | nulloptional
ISO 8601 timestamp of when the message record was created.
updated_atstring | nulloptional
ISO 8601 timestamp of the last change to the record.
sent_atstring | nulloptional
When the gateway accepted the message. Null until then.
delivered_atstring | nulloptional
When the gateway confirmed delivery to the recipient's device.
read_atstring | nulloptional
When the recipient opened it. WhatsApp only, and only with read receipts on.
{
    "status": "success",
    "data": {
        "messages": [
            {
                "id": 101,
                "uid": "msg_01JXYZSMS01",
                "direction": "outbound",
                "channel_type": "sms",
                "sender": "MyBrand",
                "recipient": "255700111222",
                "body": "Hello from API v3",
                "status": "sent"
            }
        ]
    }
}
default
{
    "status": "success",
    "data": {
        "messages": [
            {
                "id": 101,
                "uid": "msg_01JXYZSMS01",
                "direction": "outbound",
                "channel_type": "sms",
                "sender": "MyBrand",
                "recipient": "255700111222",
                "body": "Hello from API v3",
                "status": "sent"
            }
        ]
    }
}
401Unauthorized.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
default
{
    "status": "error",
    "message": "Invalid API token."
}
422Validation error.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "At least one recipient is required.",
    "errors": {
        "recipient": [
            "Provide recipient or recipients."
        ]
    }
}
default
{
    "status": "error",
    "message": "At least one recipient is required.",
    "errors": {
        "recipient": [
            "Provide recipient or recipients."
        ]
    }
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / SMS

Create an SMS campaign

POST/api/v3/sms/campaign

Creates one one-time SMS campaign per resolved contact group. Numeric IDs and group UUIDs may be separated by whitespace, comma or semicolon. Unknown groups are skipped when at least one resolves; no resolved groups returns 404. Campaign completed means recipient jobs were dispatched, not all messages delivered. Current dispatch skips blacklisted/missing phone numbers but does not filter is_subscribed. No recurrence or request-key idempotency is exposed here.

AuthenticationTenant API token

Request body

application/json · required

contact_list_idstringrequired
Contact group to send to — the numeric id or the group UUID. Comma-separate several, and each one becomes its own campaign.
maxLength
2000
messagestringrequired
The message body. `{name}` and any custom field on the contact are substituted per recipient.
maxLength
4096
sender_idstringoptional
Optional approved sender ID, tenant-owned SMS-capable number, or active short code. `GET /api/v3/sms/senders` lists every usable identity, with `value` being what to pass here.
maxLength
64
schedule_timestringoptional
ISO 8601 datetime to start the campaign. Omit it and the campaign starts immediately.
maxLength
100
namestringoptional
A name for the campaign in the dashboard. Defaults to "API Campaign - <group name>".
maxLength
160
Complete request schema
{
    "type": "object",
    "properties": {
        "contact_list_id": {
            "type": "string",
            "description": "Contact group to send to \u2014 the numeric id or the group UUID. Comma-separate several, and each one becomes its own campaign.",
            "maxLength": 2000
        },
        "message": {
            "type": "string",
            "description": "The message body. `{name}` and any custom field on the contact are substituted per recipient.",
            "maxLength": 4096
        },
        "sender_id": {
            "type": "string",
            "description": "Optional approved sender ID, tenant-owned SMS-capable number, or active short code. `GET /api/v3/sms/senders` lists every usable identity, with `value` being what to pass here.",
            "maxLength": 64
        },
        "schedule_time": {
            "type": "string",
            "description": "ISO 8601 datetime to start the campaign. Omit it and the campaign starts immediately.",
            "maxLength": 100
        },
        "name": {
            "type": "string",
            "description": "A name for the campaign in the dashboard. Defaults to \"API Campaign - <group name>\".",
            "maxLength": 160
        }
    },
    "required": [
        "contact_list_id",
        "message"
    ]
}
default
{
    "contact_list_id": "grp_01JXYZABC",
    "message": "Campaign message",
    "sender_id": "Brand"
}

Responses

201Campaigns created.
statusstringrequired
Always "success" on a 2xx response.
enum
["success"]
dataobjectrequired
The campaigns this call created.
Show child properties
campaignsarray<object>required
One campaign per contact group in `contact_list_id`.
Show child properties
idintegerrequired
Internal numeric id.
uidstringrequired
Public unique identifier.
namestringrequired
Campaign name.
statusstringrequired
Campaign status. A campaign created without `schedule_time` starts as `draft` and begins immediately; one with a schedule waits in `scheduled`.
enum
["draft","scheduled","running","paused","completed","cancelled"]
channel_typestringrequired
Channel type; currently only sms.
enum
["sms"]
tenant_channel_idintegeroptional
The account channel selected automatically when the campaign was created.
channel_codestring | nulloptional
Resolved channel code returned for observability; it is not caller-selectable.
messagestringrequired
Campaign message text.
senderstring | nulloptional
The sender identity the campaign sends from.
scheduled_atstring | nulloptional
When the campaign is due to start. Null for one that started immediately.
total_recipientsintegeroptional
How many contacts the campaign will send to.
sent_countintegeroptional
How many have been sent so far.
failed_countintegeroptional
How many the gateway refused.
contact_groupobject | nulloptional
The contact group this campaign sends to.
additionalProperties
false
Show child properties
idintegeroptional
Numeric group id.
uidstringoptional
Group UUID — the form you can also pass as `contact_list_id`.
namestringoptional
Group name as it appears in the dashboard.
created_atstring | nulloptional
ISO 8601 timestamp of when the campaign was created.
updated_atstring | nulloptional
ISO 8601 timestamp of the last change to it.
{
    "status": "success",
    "data": {
        "campaigns": [
            {
                "id": 15,
                "uid": "cmp_01JXYZ001",
                "name": "API Campaign - VIP List",
                "status": "draft",
                "channel_type": "sms",
                "message": "Campaign message"
            }
        ]
    }
}
default
{
    "status": "success",
    "data": {
        "campaigns": [
            {
                "id": 15,
                "uid": "cmp_01JXYZ001",
                "name": "API Campaign - VIP List",
                "status": "draft",
                "channel_type": "sms",
                "message": "Campaign message"
            }
        ]
    }
}
401Unauthorized.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
default
{
    "status": "error",
    "message": "Invalid API token."
}
404No such record for this tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}
422Validation error.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "contact_list_id must contain at least one group id."
}
default
{
    "status": "error",
    "message": "contact_list_id must contain at least one group id."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / SMS

List SMS messages

GET/api/v3/sms

Returns tenant-scoped SMS message logs with pagination.

AuthenticationTenant API token

Query parameters

statusstringoptional
Only messages in this delivery state.
enum
["queued","processing","sent","checking_delivery","delivered","read","failed","received"]

Example: delivered

directionstringoptional
Only messages you sent (`outbound`) or received (`inbound`).
enum
["inbound","outbound"]

Example: outbound

limitintegeroptional
Rows per page, 1–100. Defaults to 20 (25 for catalogue endpoints). Values above 100 are clamped.
minimum
1
maximum
100
default
20

Example: 20

per_pageintegeroptional
Alias of `limit`, for clients that already speak Laravel pagination. `limit` wins if both are sent.
minimum
1
maximum
100

Example: 25

pageintegeroptional
Page number, 1-based. Read `data.pagination.has_more_pages` to know when to stop.
minimum
1
default
1

Example: 1

Responses

200SMS collection.
statusstringrequired
Always "success" on a 2xx response.
enum
["success"]
dataobjectrequired
A page of messages and its page state.
Show child properties
itemsarray<object>required
The messages on this page, newest first.
Show child properties
idintegerrequired
Internal numeric id.
uidstringrequired
Public unique identifier (e.g. msg_01JXYZSMS01).
directionstringrequired
Whether you sent the message (`outbound`) or received it (`inbound`).
enum
["inbound","outbound"]
channel_typestringrequired
Channel: sms or whatsapp.
enum
["sms","whatsapp"]
tenant_channel_idintegeroptional
The account channel selected automatically by the outbound routing policy.
channel_codestring | nulloptional
Resolved channel code returned for observability; it is not caller-selectable.
senderstring | nulloptional
Sender identity; inbound messages can contain the customer phone or provider identity.
recipientstringrequired
Recipient phone number (E.164 or national).
bodystringrequired
Message text content.
statusstringrequired
Delivery status. Outbound messages walk queued → processing → sent → delivered → read, or stop at failed with `error_message` set; `received` is what inbound messages carry.
enum
["queued","processing","sent","checking_delivery","delivered","read","failed","received"]
media_urlstring | nulloptional
The attached file, when the message carries one.
media_typestring | nulloptional
The kind of attached media (image, video, audio, document, sticker).
gateway_message_idstring | nulloptional
Provider message ID, used for replies/reactions. Customer message webhooks identify local records with numeric message_id instead.
error_messagestring | nulloptional
Why the send failed, straight from the gateway. Null unless `status` is `failed`.
metadataobject | nulloptional
Anything extra recorded with the message — the interactive or reaction payload, the id it replies to, the source that created it.
additionalProperties
true
template_paramsobject | nulloptional
The template name, language and variables used, when the message was sent from a template.
additionalProperties
true
created_atstring | nulloptional
ISO 8601 timestamp of when the message record was created.
updated_atstring | nulloptional
ISO 8601 timestamp of the last change to the record.
sent_atstring | nulloptional
When the gateway accepted the message. Null until then.
delivered_atstring | nulloptional
When the gateway confirmed delivery to the recipient's device.
read_atstring | nulloptional
When the recipient opened it. WhatsApp only, and only with read receipts on.
paginationobjectrequired
Page state for this list: where you are and whether more pages follow.
Show child properties
current_pageintegerrequired
1-based current page index.
per_pageintegerrequired
Number of items per page.
last_pageintegerrequired
1-based index of the last page.
totalintegerrequired
Total number of items across all pages.
has_more_pagesbooleanrequired
True if more pages exist after the current page.
{
    "status": "success",
    "data": {
        "items": [
            {
                "id": 101,
                "uid": "msg_01JXYZSMS01",
                "direction": "outbound",
                "channel_type": "sms",
                "recipient": "255700111222",
                "body": "Hello from API v3",
                "status": "queued"
            }
        ],
        "pagination": {
            "current_page": 1,
            "per_page": 20,
            "last_page": 1,
            "total": 1,
            "has_more_pages": false
        }
    }
}
default
{
    "status": "success",
    "data": {
        "items": [
            {
                "id": 101,
                "uid": "msg_01JXYZSMS01",
                "direction": "outbound",
                "channel_type": "sms",
                "recipient": "255700111222",
                "body": "Hello from API v3",
                "status": "queued"
            }
        ],
        "pagination": {
            "current_page": 1,
            "per_page": 20,
            "last_page": 1,
            "total": 1,
            "has_more_pages": false
        }
    }
}
401Unauthorized.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Missing bearer token."
}
default
{
    "status": "error",
    "message": "Missing bearer token."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / SMS

Get an SMS message

GET/api/v3/sms/{uid}

Fetches one SMS message by public uid with numeric id fallback.

AuthenticationTenant API token

Path parameters

uidstringrequired
The message `uid` returned by the send call (or its numeric `id`).

Example: msg_kuutop7qhc076g316z4k

Responses

200Single SMS message.
statusstringrequired
Always "success" on a 2xx response.
enum
["success"]
dataobjectrequired
The message record.
Show child properties
idintegerrequired
Internal numeric id.
uidstringrequired
Public unique identifier (e.g. msg_01JXYZSMS01).
directionstringrequired
Whether you sent the message (`outbound`) or received it (`inbound`).
enum
["inbound","outbound"]
channel_typestringrequired
Channel: sms or whatsapp.
enum
["sms","whatsapp"]
tenant_channel_idintegeroptional
The account channel selected automatically by the outbound routing policy.
channel_codestring | nulloptional
Resolved channel code returned for observability; it is not caller-selectable.
senderstring | nulloptional
Sender identity; inbound messages can contain the customer phone or provider identity.
recipientstringrequired
Recipient phone number (E.164 or national).
bodystringrequired
Message text content.
statusstringrequired
Delivery status. Outbound messages walk queued → processing → sent → delivered → read, or stop at failed with `error_message` set; `received` is what inbound messages carry.
enum
["queued","processing","sent","checking_delivery","delivered","read","failed","received"]
media_urlstring | nulloptional
The attached file, when the message carries one.
media_typestring | nulloptional
The kind of attached media (image, video, audio, document, sticker).
gateway_message_idstring | nulloptional
Provider message ID, used for replies/reactions. Customer message webhooks identify local records with numeric message_id instead.
error_messagestring | nulloptional
Why the send failed, straight from the gateway. Null unless `status` is `failed`.
metadataobject | nulloptional
Anything extra recorded with the message — the interactive or reaction payload, the id it replies to, the source that created it.
additionalProperties
true
template_paramsobject | nulloptional
The template name, language and variables used, when the message was sent from a template.
additionalProperties
true
created_atstring | nulloptional
ISO 8601 timestamp of when the message record was created.
updated_atstring | nulloptional
ISO 8601 timestamp of the last change to the record.
sent_atstring | nulloptional
When the gateway accepted the message. Null until then.
delivered_atstring | nulloptional
When the gateway confirmed delivery to the recipient's device.
read_atstring | nulloptional
When the recipient opened it. WhatsApp only, and only with read receipts on.
{
    "status": "success",
    "data": {
        "id": 101,
        "uid": "msg_01JXYZSMS01",
        "direction": "outbound",
        "channel_type": "sms",
        "recipient": "255700111222",
        "body": "Hello from API v3",
        "status": "delivered"
    }
}
default
{
    "status": "success",
    "data": {
        "id": 101,
        "uid": "msg_01JXYZSMS01",
        "direction": "outbound",
        "channel_type": "sms",
        "recipient": "255700111222",
        "body": "Hello from API v3",
        "status": "delivered"
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
404Message not found.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Message not found."
}
default
{
    "status": "error",
    "message": "Message not found."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / SMS

View one campaign

GET/api/v3/campaign/{uid}/view

Retrieves one SMS campaign by uid.

AuthenticationTenant API token

Path parameters

uidstringrequired
The campaign `uid` returned when the campaign was created (or its numeric `id`).

Example: cmp_w5aqybtpzqj79ngzqcoh

Responses

200Campaign details.
statusstringrequired
Always "success" on a 2xx response.
enum
["success"]
dataobjectrequired
The campaign record.
Show child properties
idintegerrequired
Internal numeric id.
uidstringrequired
Public unique identifier.
namestringrequired
Campaign name.
statusstringrequired
Campaign status. A campaign created without `schedule_time` starts as `draft` and begins immediately; one with a schedule waits in `scheduled`.
enum
["draft","scheduled","running","paused","completed","cancelled"]
channel_typestringrequired
Channel type; currently only sms.
enum
["sms"]
tenant_channel_idintegeroptional
The account channel selected automatically when the campaign was created.
channel_codestring | nulloptional
Resolved channel code returned for observability; it is not caller-selectable.
messagestringrequired
Campaign message text.
senderstring | nulloptional
The sender identity the campaign sends from.
scheduled_atstring | nulloptional
When the campaign is due to start. Null for one that started immediately.
total_recipientsintegeroptional
How many contacts the campaign will send to.
sent_countintegeroptional
How many have been sent so far.
failed_countintegeroptional
How many the gateway refused.
contact_groupobject | nulloptional
The contact group this campaign sends to.
additionalProperties
false
Show child properties
idintegeroptional
Numeric group id.
uidstringoptional
Group UUID — the form you can also pass as `contact_list_id`.
namestringoptional
Group name as it appears in the dashboard.
created_atstring | nulloptional
ISO 8601 timestamp of when the campaign was created.
updated_atstring | nulloptional
ISO 8601 timestamp of the last change to it.
{
    "status": "success",
    "data": {
        "id": 15,
        "uid": "cmp_01JXYZ001",
        "name": "API Campaign - VIP List",
        "status": "running",
        "channel_type": "sms",
        "message": "Campaign message"
    }
}
default
{
    "status": "success",
    "data": {
        "id": 15,
        "uid": "cmp_01JXYZ001",
        "name": "API Campaign - VIP List",
        "status": "running",
        "channel_type": "sms",
        "message": "Campaign message"
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
404Campaign not found.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Campaign not found."
}
default
{
    "status": "error",
    "message": "Campaign not found."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / WhatsApp

Send a WhatsApp message

POST/api/v3/whatsapp/send

Creates one record per normalized recipient and normally attempts provider delivery synchronously, sequentially. HTTP 201 can contain sent or failed records; inspect every status/error_message. Future schedules, admission deferral or fallback can return queued. Repeating this POST is not protected by a request Idempotency-Key. Supports text, templates, media, interactive payloads and reactions. Provider validation, including conversation-window rules, can appear as a failed message in a 201 response.

AuthenticationTenant API token

Request body

application/json · required

recipientstringoptional
Recipients separated by commas, semicolons or whitespace. Combined with recipients and deduplicated by exact string.
maxLength
4000
recipientsarray<string>optional
Additional recipient strings; entries also split on commas, semicolons and whitespace. Can be used together with recipient.
items.maxLength
191
messagestringoptional
Text or local preview text, maximum 4096 characters. With a template, this does not replace the approved provider template body.
maxLength
4096
bodystringoptional
Alias of `message`. `message` wins if both are sent.
maxLength
4096
message_typestringoptional
Takes precedence over type. Defaults to plain for SMS and text for WhatsApp; supplied payload objects determine provider send behavior.
maxLength
60
typestringoptional
Alias of `message_type`.
maxLength
60
media_urlstringoptional
Publicly reachable file to send as the message. WhatsApp fetches it directly, so it cannot sit behind authentication.
maxLength
2048
format
uri
media_typestringoptional
The kind of media at `media_url` (image, video, audio, document, sticker). Defaults to `message_type`.
maxLength
32
templateobjectoptional
Provider template name, language and components. Requires a nonempty name. Cannot be combined with media or interactive payloads.
additionalProperties
true
Show child properties
namestringrequired
Template name exactly as approved in your WhatsApp Business Account.
maxLength
191
languagestringoptional
Template language code, e.g. `en` or `sw`. Defaults to `en`.
maxLength
20
componentsarray<object>optional
Template variables in WhatsApp's own `components` shape — one entry per header, body or button that takes a parameter.
items.additionalProperties
true
interactiveobjectoptional
Provider-shaped interactive payload. Buttons and lists are normalized; other supported interactive types are passed through for provider validation. Cannot be combined with top-level media or template.
additionalProperties
true
reactionobjectoptional
Requires a nonempty emoji and a target provider message ID in message_id or in_reply_to_gateway_id. Cannot be combined with text/media/template/interactive.
additionalProperties
true
Show child properties
emojistringrequired
Nonempty reaction emoji. Empty-emoji removal is not supported by this REST route.
maxLength
16
message_idstringoptional
The `gateway_message_id` of the message being reacted to.
maxLength
191
in_reply_to_gateway_idstringoptional
Quote an earlier message: the `gateway_message_id` of the message being replied to. It shows in the chat as a reply to that bubble.
maxLength
191
sender_idstringoptional
WhatsApp phone number id to send from, when the account has more than one. Defaults to the account default. `GET /api/v3/whatsapp/accounts` lists the accounts and their numbers (`phone_numbers[].id`, or the number itself).
maxLength
64
schedule_timestringoptional
Optional future send time. Use ISO8601 with an explicit offset; past times do not delay.
maxLength
100
Provide at least one of these alternatives

recipient

recipients

Complete request schema
{
    "type": "object",
    "properties": {
        "recipient": {
            "type": "string",
            "description": "Recipients separated by commas, semicolons or whitespace. Combined with recipients and deduplicated by exact string.",
            "maxLength": 4000
        },
        "recipients": {
            "type": "array",
            "items": {
                "type": "string",
                "maxLength": 191
            },
            "description": "Additional recipient strings; entries also split on commas, semicolons and whitespace. Can be used together with recipient."
        },
        "message": {
            "type": "string",
            "description": "Text or local preview text, maximum 4096 characters. With a template, this does not replace the approved provider template body.",
            "maxLength": 4096
        },
        "body": {
            "type": "string",
            "description": "Alias of `message`. `message` wins if both are sent.",
            "maxLength": 4096
        },
        "message_type": {
            "type": "string",
            "description": "Takes precedence over type. Defaults to plain for SMS and text for WhatsApp; supplied payload objects determine provider send behavior.",
            "maxLength": 60
        },
        "type": {
            "type": "string",
            "description": "Alias of `message_type`.",
            "maxLength": 60
        },
        "media_url": {
            "type": "string",
            "description": "Publicly reachable file to send as the message. WhatsApp fetches it directly, so it cannot sit behind authentication.",
            "maxLength": 2048,
            "format": "uri"
        },
        "media_type": {
            "type": "string",
            "description": "The kind of media at `media_url` (image, video, audio, document, sticker). Defaults to `message_type`.",
            "maxLength": 32
        },
        "template": {
            "type": "object",
            "additionalProperties": true,
            "description": "Provider template name, language and components. Requires a nonempty name. Cannot be combined with media or interactive payloads.",
            "required": [
                "name"
            ],
            "properties": {
                "name": {
                    "type": "string",
                    "maxLength": 191,
                    "description": "Template name exactly as approved in your WhatsApp Business Account."
                },
                "language": {
                    "type": "string",
                    "maxLength": 20,
                    "description": "Template language code, e.g. `en` or `sw`. Defaults to `en`."
                },
                "components": {
                    "type": "array",
                    "items": {
                        "type": "object",
                        "additionalProperties": true
                    },
                    "description": "Template variables in WhatsApp's own `components` shape \u2014 one entry per header, body or button that takes a parameter."
                }
            }
        },
        "interactive": {
            "type": "object",
            "additionalProperties": true,
            "description": "Provider-shaped interactive payload. Buttons and lists are normalized; other supported interactive types are passed through for provider validation. Cannot be combined with top-level media or template."
        },
        "reaction": {
            "type": "object",
            "additionalProperties": true,
            "description": "Requires a nonempty emoji and a target provider message ID in message_id or in_reply_to_gateway_id. Cannot be combined with text/media/template/interactive.",
            "required": [
                "emoji"
            ],
            "properties": {
                "emoji": {
                    "type": "string",
                    "maxLength": 16,
                    "description": "Nonempty reaction emoji. Empty-emoji removal is not supported by this REST route."
                },
                "message_id": {
                    "type": "string",
                    "maxLength": 191,
                    "description": "The `gateway_message_id` of the message being reacted to."
                }
            }
        },
        "in_reply_to_gateway_id": {
            "type": "string",
            "description": "Quote an earlier message: the `gateway_message_id` of the message being replied to. It shows in the chat as a reply to that bubble.",
            "maxLength": 191
        },
        "sender_id": {
            "type": "string",
            "maxLength": 64,
            "description": "WhatsApp phone number id to send from, when the account has more than one. Defaults to the account default. `GET /api/v3/whatsapp/accounts` lists the accounts and their numbers (`phone_numbers[].id`, or the number itself)."
        },
        "schedule_time": {
            "type": "string",
            "description": "Optional future send time. Use ISO8601 with an explicit offset; past times do not delay.",
            "maxLength": 100
        }
    },
    "anyOf": [
        {
            "required": [
                "recipient"
            ]
        },
        {
            "required": [
                "recipients"
            ]
        }
    ],
    "description": "Supply recipient and/or recipients. Inputs are merged and exact duplicates removed. Message/body and payload combinations follow this endpoint description. Reaction excludes every other payload; template excludes top-level media/interactive; interactive excludes top-level media. Media message types require media_url."
}
Text message
{
    "recipient": "255700111222",
    "message": "Hello from the API"
}
Text
{
    "recipient": "255700111222",
    "message_type": "text",
    "message": "Hello, this is a plain text message."
}
Image
{
    "recipient": "255700111222",
    "message_type": "image",
    "media_url": "https://example.com/image.png",
    "message": "Optional caption"
}
Video
{
    "recipient": "255700111222",
    "message_type": "video",
    "media_url": "https://example.com/video.mp4",
    "message": "Optional caption"
}
Audio
{
    "recipient": "255700111222",
    "message_type": "audio",
    "media_url": "https://example.com/audio.ogg"
}
Document
{
    "recipient": "255700111222",
    "message_type": "document",
    "media_url": "https://example.com/file.pdf",
    "message": "Optional filename or caption"
}
Sticker
{
    "recipient": "255700111222",
    "message_type": "sticker",
    "media_url": "https://example.com/sticker.webp"
}
Template
{
    "recipient": "255700111222",
    "message_type": "template",
    "template": {
        "name": "welcome_template",
        "language": "en",
        "components": []
    }
}
Interactive (buttons)
{
    "recipient": "255700111222",
    "message_type": "interactive",
    "interactive": {
        "type": "button",
        "body": {
            "text": "Choose one"
        },
        "action": {
            "buttons": [
                {
                    "id": "yes",
                    "title": "Yes"
                },
                {
                    "id": "no",
                    "title": "No"
                }
            ]
        }
    }
}
Interactive (list)
{
    "recipient": "255700111222",
    "message_type": "interactive",
    "interactive": {
        "type": "list",
        "body": {
            "text": "Select an option"
        },
        "action": {
            "button": "View options",
            "sections": [
                {
                    "title": "Section 1",
                    "rows": [
                        {
                            "id": "opt_1",
                            "title": "Option 1",
                            "description": "First choice"
                        },
                        {
                            "id": "opt_2",
                            "title": "Option 2",
                            "description": "Second choice"
                        }
                    ]
                }
            ]
        }
    }
}
Reaction
{
    "recipient": "255700111222",
    "message_type": "reaction",
    "reaction": {
        "emoji": "\ud83d\udc4d",
        "message_id": "wamid.xxxxx"
    }
}
Location pin
{
    "recipient": "255700111222",
    "message_type": "location",
    "location": {
        "latitude": -6.7924,
        "longitude": 39.2083,
        "name": "Momo Telecom HQ",
        "address": "Dar es Salaam, Tanzania"
    }
}
Contact card (vCard)
{
    "recipient": "255700111222",
    "message_type": "contacts",
    "contacts": [
        {
            "name": {
                "formatted_name": "Asha Mwita",
                "first_name": "Asha",
                "last_name": "Mwita"
            },
            "phones": [
                {
                    "phone": "+255700123456",
                    "type": "WORK",
                    "wa_id": "255700123456"
                }
            ],
            "emails": [
                {
                    "email": "asha@example.com",
                    "type": "WORK"
                }
            ],
            "org": {
                "company": "Momo Telecom",
                "title": "Account Manager"
            }
        }
    ]
}
Interactive — call-to-action URL button
{
    "recipient": "255700111222",
    "message_type": "interactive",
    "interactive": {
        "type": "cta_url",
        "header": {
            "type": "text",
            "text": "Track your order"
        },
        "body": {
            "text": "Your order #4521 has shipped. Tap below to track delivery in real time."
        },
        "footer": {
            "text": "Powered by Momo Business"
        },
        "action": {
            "name": "cta_url",
            "parameters": {
                "display_text": "Track order",
                "url": "https://acme.example.com/orders/4521"
            }
        }
    }
}
Interactive — WhatsApp Flow
{
    "recipient": "255700111222",
    "message_type": "interactive",
    "interactive": {
        "type": "flow",
        "header": {
            "type": "text",
            "text": "Book an appointment"
        },
        "body": {
            "text": "Pick a time slot that works for you."
        },
        "footer": {
            "text": "Takes 60 seconds"
        },
        "action": {
            "name": "flow",
            "parameters": {
                "flow_message_version": "3",
                "flow_token": "FLOW_TOKEN_FROM_BACKEND",
                "flow_id": "1234567890123456",
                "flow_cta": "Book now",
                "flow_action": "navigate",
                "flow_action_payload": {
                    "screen": "APPOINTMENT_SCREEN"
                }
            }
        }
    }
}
Interactive buttons with image header
{
    "recipient": "255700111222",
    "message_type": "interactive",
    "interactive": {
        "type": "button",
        "header": {
            "type": "image",
            "image": {
                "link": "https://cdn.example.com/promo.jpg"
            }
        },
        "body": {
            "text": "Limited-time offer \u2014 30% off today only."
        },
        "action": {
            "buttons": [
                {
                    "type": "reply",
                    "reply": {
                        "id": "shop_now",
                        "title": "Shop now"
                    }
                },
                {
                    "type": "reply",
                    "reply": {
                        "id": "remind_later",
                        "title": "Remind me later"
                    }
                }
            ]
        }
    }
}
Product (single item from a catalogue)
{
    "recipient": "255700111222",
    "message_type": "interactive",
    "interactive": {
        "type": "product",
        "body": {
            "text": "Check out this laptop."
        },
        "action": {
            "catalog_id": "26191517010530753",
            "product_retailer_id": "SKU-LAPTOP-X1"
        }
    }
}
Product list (up to 30 items, 10 sections)
{
    "recipient": "255700111222",
    "message_type": "interactive",
    "interactive": {
        "type": "product_list",
        "header": {
            "type": "text",
            "text": "Top picks"
        },
        "body": {
            "text": "Tap any item to see details and add to cart."
        },
        "footer": {
            "text": "Free delivery on orders over TZS 50,000"
        },
        "action": {
            "catalog_id": "26191517010530753",
            "sections": [
                {
                    "title": "Laptops",
                    "product_items": [
                        {
                            "product_retailer_id": "SKU-LAPTOP-X1"
                        },
                        {
                            "product_retailer_id": "SKU-LAPTOP-AIR"
                        }
                    ]
                },
                {
                    "title": "Phones",
                    "product_items": [
                        {
                            "product_retailer_id": "SKU-PHONE-15"
                        }
                    ]
                }
            ]
        }
    }
}
Full catalogue (storefront entry point)
{
    "recipient": "255700111222",
    "message_type": "interactive",
    "interactive": {
        "type": "catalog_message",
        "body": {
            "text": "Browse our entire catalogue."
        },
        "action": {
            "name": "catalog_message",
            "parameters": {
                "thumbnail_product_retailer_id": "SKU-LAPTOP-X1"
            }
        }
    }
}
Template with header image + body params + URL button
{
    "recipient": "255700111222",
    "message_type": "template",
    "template": {
        "name": "order_shipped",
        "language": "en_US",
        "components": [
            {
                "type": "header",
                "parameters": [
                    {
                        "type": "image",
                        "image": {
                            "link": "https://cdn.example.com/box.jpg"
                        }
                    }
                ]
            },
            {
                "type": "body",
                "parameters": [
                    {
                        "type": "text",
                        "text": "Asha"
                    },
                    {
                        "type": "text",
                        "text": "4521"
                    },
                    {
                        "type": "text",
                        "text": "Tomorrow 9\u201311am"
                    }
                ]
            },
            {
                "type": "button",
                "sub_type": "url",
                "index": "0",
                "parameters": [
                    {
                        "type": "text",
                        "text": "4521"
                    }
                ]
            }
        ]
    }
}
Reply that quotes a previous message
{
    "recipient": "255700111222",
    "message_type": "text",
    "message": "Got it \u2014 see you tomorrow!",
    "in_reply_to_gateway_id": "wamid.HBgMMjU1NzAwMTExMjIyFQIAERgSREYx..."
}

Responses

201One message record per recipient. `data.messages[].status` is the delivery state at the moment we answered; watch the `message.*` webhooks for what happens after.
statusstringrequired
Always "success" on a 2xx response.
enum
["success"]
dataobjectrequired
The messages this call created.
Show child properties
messagesarray<object>required
One record per recipient, in the order they were given.
Show child properties
idintegerrequired
Internal numeric id.
uidstringrequired
Public unique identifier (e.g. msg_01JXYZSMS01).
directionstringrequired
Whether you sent the message (`outbound`) or received it (`inbound`).
enum
["inbound","outbound"]
channel_typestringrequired
Channel: sms or whatsapp.
enum
["sms","whatsapp"]
tenant_channel_idintegeroptional
The account channel selected automatically by the outbound routing policy.
channel_codestring | nulloptional
Resolved channel code returned for observability; it is not caller-selectable.
senderstring | nulloptional
Sender identity; inbound messages can contain the customer phone or provider identity.
recipientstringrequired
Recipient phone number (E.164 or national).
bodystringrequired
Message text content.
statusstringrequired
Delivery status. Outbound messages walk queued → processing → sent → delivered → read, or stop at failed with `error_message` set; `received` is what inbound messages carry.
enum
["queued","processing","sent","checking_delivery","delivered","read","failed","received"]
media_urlstring | nulloptional
The attached file, when the message carries one.
media_typestring | nulloptional
The kind of attached media (image, video, audio, document, sticker).
gateway_message_idstring | nulloptional
Provider message ID, used for replies/reactions. Customer message webhooks identify local records with numeric message_id instead.
error_messagestring | nulloptional
Why the send failed, straight from the gateway. Null unless `status` is `failed`.
metadataobject | nulloptional
Anything extra recorded with the message — the interactive or reaction payload, the id it replies to, the source that created it.
additionalProperties
true
template_paramsobject | nulloptional
The template name, language and variables used, when the message was sent from a template.
additionalProperties
true
created_atstring | nulloptional
ISO 8601 timestamp of when the message record was created.
updated_atstring | nulloptional
ISO 8601 timestamp of the last change to the record.
sent_atstring | nulloptional
When the gateway accepted the message. Null until then.
delivered_atstring | nulloptional
When the gateway confirmed delivery to the recipient's device.
read_atstring | nulloptional
When the recipient opened it. WhatsApp only, and only with read receipts on.
{
    "status": "success",
    "data": {
        "messages": [
            {
                "id": 300,
                "uid": "msg_01JXYZWA01",
                "direction": "outbound",
                "channel_type": "whatsapp",
                "recipient": "255700111222",
                "body": "Interactive message",
                "status": "sent",
                "metadata": {
                    "interactive": {
                        "type": "button"
                    }
                }
            }
        ]
    }
}
default
{
    "status": "success",
    "data": {
        "messages": [
            {
                "id": 300,
                "uid": "msg_01JXYZWA01",
                "direction": "outbound",
                "channel_type": "whatsapp",
                "recipient": "255700111222",
                "body": "Interactive message",
                "status": "sent",
                "metadata": {
                    "interactive": {
                        "type": "button"
                    }
                }
            }
        ]
    }
}
401Unauthorized.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
default
{
    "status": "error",
    "message": "Invalid API token."
}
422Validation or payload combination error.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Reaction cannot be combined with text, media, template, or interactive payload."
}
default
{
    "status": "error",
    "message": "Reaction cannot be combined with text, media, template, or interactive payload."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / WhatsApp

List WhatsApp messages

GET/api/v3/whatsapp

Returns tenant-scoped WhatsApp message logs with pagination.

AuthenticationTenant API token

Query parameters

statusstringoptional
Only messages in this delivery state.
enum
["queued","processing","sent","checking_delivery","delivered","read","failed","received"]

Example: delivered

directionstringoptional
Only messages you sent (`outbound`) or received (`inbound`).
enum
["inbound","outbound"]

Example: outbound

limitintegeroptional
Rows per page, 1–100. Defaults to 20 (25 for catalogue endpoints). Values above 100 are clamped.
minimum
1
maximum
100
default
20

Example: 20

per_pageintegeroptional
Alias of `limit`, for clients that already speak Laravel pagination. `limit` wins if both are sent.
minimum
1
maximum
100

Example: 25

pageintegeroptional
Page number, 1-based. Read `data.pagination.has_more_pages` to know when to stop.
minimum
1
default
1

Example: 1

Responses

200WhatsApp collection.
statusstringrequired
Always "success" on a 2xx response.
enum
["success"]
dataobjectrequired
A page of messages and its page state.
Show child properties
itemsarray<object>required
The messages on this page, newest first.
Show child properties
idintegerrequired
Internal numeric id.
uidstringrequired
Public unique identifier (e.g. msg_01JXYZSMS01).
directionstringrequired
Whether you sent the message (`outbound`) or received it (`inbound`).
enum
["inbound","outbound"]
channel_typestringrequired
Channel: sms or whatsapp.
enum
["sms","whatsapp"]
tenant_channel_idintegeroptional
The account channel selected automatically by the outbound routing policy.
channel_codestring | nulloptional
Resolved channel code returned for observability; it is not caller-selectable.
senderstring | nulloptional
Sender identity; inbound messages can contain the customer phone or provider identity.
recipientstringrequired
Recipient phone number (E.164 or national).
bodystringrequired
Message text content.
statusstringrequired
Delivery status. Outbound messages walk queued → processing → sent → delivered → read, or stop at failed with `error_message` set; `received` is what inbound messages carry.
enum
["queued","processing","sent","checking_delivery","delivered","read","failed","received"]
media_urlstring | nulloptional
The attached file, when the message carries one.
media_typestring | nulloptional
The kind of attached media (image, video, audio, document, sticker).
gateway_message_idstring | nulloptional
Provider message ID, used for replies/reactions. Customer message webhooks identify local records with numeric message_id instead.
error_messagestring | nulloptional
Why the send failed, straight from the gateway. Null unless `status` is `failed`.
metadataobject | nulloptional
Anything extra recorded with the message — the interactive or reaction payload, the id it replies to, the source that created it.
additionalProperties
true
template_paramsobject | nulloptional
The template name, language and variables used, when the message was sent from a template.
additionalProperties
true
created_atstring | nulloptional
ISO 8601 timestamp of when the message record was created.
updated_atstring | nulloptional
ISO 8601 timestamp of the last change to the record.
sent_atstring | nulloptional
When the gateway accepted the message. Null until then.
delivered_atstring | nulloptional
When the gateway confirmed delivery to the recipient's device.
read_atstring | nulloptional
When the recipient opened it. WhatsApp only, and only with read receipts on.
paginationobjectrequired
Page state for this list: where you are and whether more pages follow.
Show child properties
current_pageintegerrequired
1-based current page index.
per_pageintegerrequired
Number of items per page.
last_pageintegerrequired
1-based index of the last page.
totalintegerrequired
Total number of items across all pages.
has_more_pagesbooleanrequired
True if more pages exist after the current page.
{
    "status": "success",
    "data": {
        "items": [
            {
                "id": 300,
                "uid": "msg_01JXYZWA01",
                "direction": "outbound",
                "channel_type": "whatsapp",
                "recipient": "255700111222",
                "body": "Interactive message",
                "status": "queued"
            }
        ],
        "pagination": {
            "current_page": 1,
            "per_page": 20,
            "last_page": 1,
            "total": 1,
            "has_more_pages": false
        }
    }
}
default
{
    "status": "success",
    "data": {
        "items": [
            {
                "id": 300,
                "uid": "msg_01JXYZWA01",
                "direction": "outbound",
                "channel_type": "whatsapp",
                "recipient": "255700111222",
                "body": "Interactive message",
                "status": "queued"
            }
        ],
        "pagination": {
            "current_page": 1,
            "per_page": 20,
            "last_page": 1,
            "total": 1,
            "has_more_pages": false
        }
    }
}
401Unauthorized.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Missing bearer token."
}
default
{
    "status": "error",
    "message": "Missing bearer token."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / WhatsApp

Get a WhatsApp message

GET/api/v3/whatsapp/{uid}

Fetches one WhatsApp message by public uid.

AuthenticationTenant API token

Path parameters

uidstringrequired
The message `uid` returned by the send call (or its numeric `id`).

Example: msg_kuutop7qhc076g316z4k

Responses

200Single WhatsApp message.
statusstringrequired
Always "success" on a 2xx response.
enum
["success"]
dataobjectrequired
The message record.
Show child properties
idintegerrequired
Internal numeric id.
uidstringrequired
Public unique identifier (e.g. msg_01JXYZSMS01).
directionstringrequired
Whether you sent the message (`outbound`) or received it (`inbound`).
enum
["inbound","outbound"]
channel_typestringrequired
Channel: sms or whatsapp.
enum
["sms","whatsapp"]
tenant_channel_idintegeroptional
The account channel selected automatically by the outbound routing policy.
channel_codestring | nulloptional
Resolved channel code returned for observability; it is not caller-selectable.
senderstring | nulloptional
Sender identity; inbound messages can contain the customer phone or provider identity.
recipientstringrequired
Recipient phone number (E.164 or national).
bodystringrequired
Message text content.
statusstringrequired
Delivery status. Outbound messages walk queued → processing → sent → delivered → read, or stop at failed with `error_message` set; `received` is what inbound messages carry.
enum
["queued","processing","sent","checking_delivery","delivered","read","failed","received"]
media_urlstring | nulloptional
The attached file, when the message carries one.
media_typestring | nulloptional
The kind of attached media (image, video, audio, document, sticker).
gateway_message_idstring | nulloptional
Provider message ID, used for replies/reactions. Customer message webhooks identify local records with numeric message_id instead.
error_messagestring | nulloptional
Why the send failed, straight from the gateway. Null unless `status` is `failed`.
metadataobject | nulloptional
Anything extra recorded with the message — the interactive or reaction payload, the id it replies to, the source that created it.
additionalProperties
true
template_paramsobject | nulloptional
The template name, language and variables used, when the message was sent from a template.
additionalProperties
true
created_atstring | nulloptional
ISO 8601 timestamp of when the message record was created.
updated_atstring | nulloptional
ISO 8601 timestamp of the last change to the record.
sent_atstring | nulloptional
When the gateway accepted the message. Null until then.
delivered_atstring | nulloptional
When the gateway confirmed delivery to the recipient's device.
read_atstring | nulloptional
When the recipient opened it. WhatsApp only, and only with read receipts on.
{
    "status": "success",
    "data": {
        "id": 300,
        "uid": "msg_01JXYZWA01",
        "direction": "outbound",
        "channel_type": "whatsapp",
        "recipient": "255700111222",
        "body": "Interactive message",
        "status": "delivered"
    }
}
default
{
    "status": "success",
    "data": {
        "id": 300,
        "uid": "msg_01JXYZWA01",
        "direction": "outbound",
        "channel_type": "whatsapp",
        "recipient": "255700111222",
        "body": "Interactive message",
        "status": "delivered"
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
404Message not found.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Message not found."
}
default
{
    "status": "error",
    "message": "Message not found."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / WhatsApp groups

List WhatsApp groups

GET/api/v3/whatsapp/groups

Groups created from the account's WhatsApp numbers. Deleted groups are left out unless status=deleted or status=all.

AuthenticationTenant API token

Required permission: communications.groups.view

Query parameters

sender_idstringoptional
Only groups on this business number (phone_number_id or display number). `GET /api/v3/whatsapp/accounts` lists the accounts and their numbers (`phone_numbers[].id`, or the number itself).

Example: 243438852181644

statusstringoptional
creating (waiting for WhatsApp), active, suspended (by WhatsApp, for policy), deleted, or failed (WhatsApp refused to create it; see last_error).
enum
["creating","active","suspended","failed","deleted","all"]

Example: active

limitintegeroptional
limit
minimum
1
maximum
100
default
20

Example: 20

Responses

200A page of groups.
statusstringrequired
Always "success" on a 2xx response.
enum
["success"]
dataobjectrequired
A page of groups.
Show child properties
itemsarray<object>optional
The groups on this page.
Show child properties
idintegerrequired
Platform id of the group; what every group endpoint takes.
meta_group_idstring | nulloptional
WhatsApp's own group id. Null while the group is still being created.
request_idstring | nulloptional
WhatsApp's create request id; how the confirmation webhook is matched.
phone_number_idstringoptional
The business number the group was created from.
waba_idstring | nulloptional
The WhatsApp Business Account the number belongs to.
subjectstringrequired
The group name, up to 128 characters.
maxLength
128
descriptionstring | nulloptional
What the group is for; members see it before joining. Up to 2048 characters.
maxLength
2048
join_approval_modestringoptional
auto_approve: anyone with the link joins. approval_required: the business approves each request.
enum
["auto_approve","approval_required"]
invite_linkstring | nulloptional
The chat.whatsapp.com link people tap to join. Null until WhatsApp confirms the group.
statusstringrequired
creating (waiting for WhatsApp), active, suspended (by WhatsApp, for policy), deleted, or failed (WhatsApp refused to create it; see last_error).
enum
["creating","active","suspended","deleted","failed"]
participant_countintegerrequired
Members besides the business.
max_participantsintegerrequired
8, the business counted in.
seats_leftintegeroptional
How many more people can join.
pending_join_requestsintegeroptional
People waiting for approval on an approval_required group.
conversation_idinteger | nulloptional
The inbox thread for the group.
invite_template_idinteger | nulloptional
The approved template used for invites from this group.
last_message_atstring | nulloptional
When the thread last had a message, either way.
format
date-time
last_errorobject | array | nulloptional
WhatsApp's last refusal, when there was one.
last_synced_atstring | nulloptional
When the roster and settings were last read back from WhatsApp.
format
date-time
created_atstring | nulloptional
When the platform created the record.
format
date-time
updated_atstring | nulloptional
When it last changed.
format
date-time
paginationobjectoptional
Paging information.
Show child properties
current_pageintegerrequired
1-based current page index.
per_pageintegerrequired
Number of items per page.
last_pageintegerrequired
1-based index of the last page.
totalintegerrequired
Total number of items across all pages.
has_more_pagesbooleanrequired
True if more pages exist after the current page.
{
    "status": "success",
    "data": {
        "items": [
            {
                "id": 12,
                "meta_group_id": "Y2FwaV9ncm91cDo6MTIzNDU2",
                "request_id": "b5c1\u2026",
                "phone_number_id": "243438852181644",
                "waba_id": "1029384756",
                "subject": "VIP customers \u2014 September",
                "description": "Offers first.",
                "join_approval_mode": "auto_approve",
                "invite_link": "https://chat.whatsapp.com/AbCdEf123",
                "status": "active",
                "participant_count": 5,
                "max_participants": 8,
                "seats_left": 2,
                "pending_join_requests": 0,
                "conversation_id": 8812,
                "invite_template_id": 41,
                "last_message_at": "2026-09-07T10:12:00+03:00",
                "last_error": null,
                "last_synced_at": "2026-09-07T09:00:00+03:00",
                "created_at": "2026-09-01T08:00:00+03:00",
                "updated_at": "2026-09-07T10:12:00+03:00"
            }
        ],
        "pagination": {
            "current_page": 1,
            "per_page": 20,
            "last_page": 1,
            "total": 1,
            "has_more_pages": false
        }
    }
}
default
{
    "status": "success",
    "data": {
        "items": [
            {
                "id": 12,
                "meta_group_id": "Y2FwaV9ncm91cDo6MTIzNDU2",
                "request_id": "b5c1\u2026",
                "phone_number_id": "243438852181644",
                "waba_id": "1029384756",
                "subject": "VIP customers \u2014 September",
                "description": "Offers first.",
                "join_approval_mode": "auto_approve",
                "invite_link": "https://chat.whatsapp.com/AbCdEf123",
                "status": "active",
                "participant_count": 5,
                "max_participants": 8,
                "seats_left": 2,
                "pending_join_requests": 0,
                "conversation_id": 8812,
                "invite_template_id": 41,
                "last_message_at": "2026-09-07T10:12:00+03:00",
                "last_error": null,
                "last_synced_at": "2026-09-07T09:00:00+03:00",
                "created_at": "2026-09-01T08:00:00+03:00",
                "updated_at": "2026-09-07T10:12:00+03:00"
            }
        ],
        "pagination": {
            "current_page": 1,
            "per_page": 20,
            "last_page": 1,
            "total": 1,
            "has_more_pages": false
        }
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The key's issuer does not hold communications.groups.view, or the key has no issuer on record.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This API key was issued by a user without the \"communications.groups.view\" permission."
}
default
{
    "status": "error",
    "message": "This API key was issued by a user without the \"communications.groups.view\" permission."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / WhatsApp groups

Create a WhatsApp group

POST/api/v3/whatsapp/groups

Creates a group from a business number. WhatsApp confirms it a moment later: the group starts as creating and becomes active, with its meta_group_id and invite_link, when the confirmation webhook arrives. Invitees, if given, are sent the invite template once it is active.

Needs an Official Business Account (the green tick); otherwise WhatsApp answers code 131215 and this endpoint returns 422. Nobody can be added to a group directly — people join by tapping the link.

AuthenticationTenant API token

Required permission: communications.groups.manage

Request body

application/json · required

sender_idstringoptional
The business number; the account's default WhatsApp number when omitted. `GET /api/v3/whatsapp/accounts` lists the accounts and their numbers (`phone_numbers[].id`, or the number itself).
subjectstringrequired
The group name, up to 128 characters.
maxLength
128
descriptionstringoptional
What the group is for; members see it before joining. Up to 2048 characters.
maxLength
2048
join_approval_modestringoptional
auto_approve: anyone with the link joins. approval_required: the business approves each request.
enum
["auto_approve","approval_required"]
default
auto_approve
invite_templatestringoptional
Name of an approved group-invite template.
inviteesarray<string>optional
Phones to invite once the group is confirmed.
maxItems
7
Complete request schema
{
    "type": "object",
    "properties": {
        "sender_id": {
            "type": "string",
            "description": "The business number; the account's default WhatsApp number when omitted. `GET /api/v3/whatsapp/accounts` lists the accounts and their numbers (`phone_numbers[].id`, or the number itself)."
        },
        "subject": {
            "type": "string",
            "maxLength": 128,
            "description": "The group name, up to 128 characters."
        },
        "description": {
            "type": "string",
            "maxLength": 2048,
            "description": "What the group is for; members see it before joining. Up to 2048 characters."
        },
        "join_approval_mode": {
            "type": "string",
            "enum": [
                "auto_approve",
                "approval_required"
            ],
            "default": "auto_approve",
            "description": "auto_approve: anyone with the link joins. approval_required: the business approves each request."
        },
        "invite_template": {
            "type": "string",
            "description": "Name of an approved group-invite template."
        },
        "invitees": {
            "type": "array",
            "maxItems": 7,
            "items": {
                "type": "string"
            },
            "description": "Phones to invite once the group is confirmed."
        }
    },
    "required": [
        "subject"
    ]
}
default
{
    "subject": "VIP customers \u2014 September",
    "description": "Offers first.",
    "join_approval_mode": "auto_approve",
    "invite_template": "group_invite_link",
    "invitees": [
        "255711000001",
        "255711000002"
    ]
}

Responses

201Group requested (or, rarely, created at once).
statusstringrequired
Always "success" on a 2xx response.
enum
["success"]
dataobjectrequired
Group requested (or, rarely, created at once).
Show child properties
idintegerrequired
Platform id of the group; what every group endpoint takes.
meta_group_idstring | nulloptional
WhatsApp's own group id. Null while the group is still being created.
request_idstring | nulloptional
WhatsApp's create request id; how the confirmation webhook is matched.
phone_number_idstringoptional
The business number the group was created from.
waba_idstring | nulloptional
The WhatsApp Business Account the number belongs to.
subjectstringrequired
The group name, up to 128 characters.
maxLength
128
descriptionstring | nulloptional
What the group is for; members see it before joining. Up to 2048 characters.
maxLength
2048
join_approval_modestringoptional
auto_approve: anyone with the link joins. approval_required: the business approves each request.
enum
["auto_approve","approval_required"]
invite_linkstring | nulloptional
The chat.whatsapp.com link people tap to join. Null until WhatsApp confirms the group.
statusstringrequired
creating (waiting for WhatsApp), active, suspended (by WhatsApp, for policy), deleted, or failed (WhatsApp refused to create it; see last_error).
enum
["creating","active","suspended","deleted","failed"]
participant_countintegerrequired
Members besides the business.
max_participantsintegerrequired
8, the business counted in.
seats_leftintegeroptional
How many more people can join.
pending_join_requestsintegeroptional
People waiting for approval on an approval_required group.
conversation_idinteger | nulloptional
The inbox thread for the group.
invite_template_idinteger | nulloptional
The approved template used for invites from this group.
last_message_atstring | nulloptional
When the thread last had a message, either way.
format
date-time
last_errorobject | array | nulloptional
WhatsApp's last refusal, when there was one.
last_synced_atstring | nulloptional
When the roster and settings were last read back from WhatsApp.
format
date-time
created_atstring | nulloptional
When the platform created the record.
format
date-time
updated_atstring | nulloptional
When it last changed.
format
date-time
participantsarray<object>optional
Everyone ever invited into or seen in the group, with their current state.
Show child properties
idintegeroptional
Platform id of the group; what every group endpoint takes.
wa_idstringoptional
The person, as WhatsApp identifies them (digits, international format).
display_namestring | nulloptional
The name WhatsApp showed with their last message, when known.
statusstringoptional
creating (waiting for WhatsApp), active, suspended (by WhatsApp, for policy), deleted, or failed (WhatsApp refused to create it; see last_error).
enum
["invited","member","left","removed","failed"]
invited_atstring | nulloptional
When the invite template was sent to them.
format
date-time
joined_atstring | nulloptional
When they joined.
format
date-time
left_atstring | nulloptional
When they left or were removed.
format
date-time
reasonstring | nulloptional
How they got here: invite_link, left, removed_by_business, sync, group_deleted.
last_errorobject | array | nulloptional
WhatsApp's last refusal, when there was one.
join_requestsarray<object>optional
Join requests, newest first.
Show child properties
idintegeroptional
Platform id of the group; what every group endpoint takes.
join_request_idstringoptional
WhatsApp's id for the request; what approve and reject take.
wa_idstringoptional
The person, as WhatsApp identifies them (digits, international format).
statusstringoptional
creating (waiting for WhatsApp), active, suspended (by WhatsApp, for policy), deleted, or failed (WhatsApp refused to create it; see last_error).
enum
["pending","approved","rejected","revoked","failed"]
requested_atstring | nulloptional
When they asked to join.
format
date-time
resolved_atstring | nulloptional
When the request was approved, rejected or withdrawn.
format
date-time
invite_templateobject | nulloptional
Name of an approved group-invite template on this account.
Show child properties
idintegeroptional
Platform id of the group; what every group endpoint takes.
namestringoptional
Template name.
languagestring | nulloptional
Template language code.
whatsapp_statusstring | nulloptional
The template's approval state on WhatsApp.
eventsarray<object>optional
Recent activity, newest first.
Show child properties
idintegeroptional
Platform id of the group; what every group endpoint takes.
typestringoptional
What happened, e.g. group.participant_joined.
actorstring | nulloptional
Who did it: business, participant, meta, or a user of this platform.
enum
["business","participant","meta","user",null]
payloadobject | array | nulloptional
Event-specific detail.
occurred_atstring | nulloptional
When it happened.
format
date-time
{
    "status": "success",
    "data": {
        "id": 12,
        "meta_group_id": null,
        "request_id": "b5c1\u2026",
        "phone_number_id": "243438852181644",
        "waba_id": "1029384756",
        "subject": "VIP customers \u2014 September",
        "description": "Offers first.",
        "join_approval_mode": "auto_approve",
        "invite_link": null,
        "status": "creating",
        "participant_count": 5,
        "max_participants": 8,
        "seats_left": 2,
        "pending_join_requests": 0,
        "conversation_id": 8812,
        "invite_template_id": 41,
        "last_message_at": "2026-09-07T10:12:00+03:00",
        "last_error": null,
        "last_synced_at": "2026-09-07T09:00:00+03:00",
        "created_at": "2026-09-01T08:00:00+03:00",
        "updated_at": "2026-09-07T10:12:00+03:00",
        "participants": [],
        "join_requests": [],
        "invite_template": {
            "id": 41,
            "name": "group_invite_link",
            "language": "en",
            "whatsapp_status": "approved"
        },
        "events": []
    }
}
default
{
    "status": "success",
    "data": {
        "id": 12,
        "meta_group_id": null,
        "request_id": "b5c1\u2026",
        "phone_number_id": "243438852181644",
        "waba_id": "1029384756",
        "subject": "VIP customers \u2014 September",
        "description": "Offers first.",
        "join_approval_mode": "auto_approve",
        "invite_link": null,
        "status": "creating",
        "participant_count": 5,
        "max_participants": 8,
        "seats_left": 2,
        "pending_join_requests": 0,
        "conversation_id": 8812,
        "invite_template_id": 41,
        "last_message_at": "2026-09-07T10:12:00+03:00",
        "last_error": null,
        "last_synced_at": "2026-09-07T09:00:00+03:00",
        "created_at": "2026-09-01T08:00:00+03:00",
        "updated_at": "2026-09-07T10:12:00+03:00",
        "participants": [],
        "join_requests": [],
        "invite_template": {
            "id": 41,
            "name": "group_invite_link",
            "language": "en",
            "whatsapp_status": "approved"
        },
        "events": []
    }
}
422WhatsApp refused the request, or the group cannot take it right now (not confirmed yet, suspended, deleted). Meta's error code, when there is one, is under errors.meta[0].code.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "WhatsApp suspended this group; nothing can be sent until the suspension clears."
}
default
{
    "status": "error",
    "message": "WhatsApp suspended this group; nothing can be sent until the suspension clears."
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The key's issuer does not hold communications.groups.manage, or the key has no issuer on record.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This API key was issued by a user without the \"communications.groups.manage\" permission."
}
default
{
    "status": "error",
    "message": "This API key was issued by a user without the \"communications.groups.manage\" permission."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / WhatsApp groups

Get a WhatsApp group

GET/api/v3/whatsapp/groups/{id}

One group with its members, pending join requests, invite link and recent activity.

AuthenticationTenant API token

Required permission: communications.groups.view

Path parameters

idintegerrequired
The platform id of the group (from the list).

Example: 12

Responses

200The group.
statusstringrequired
Always "success" on a 2xx response.
enum
["success"]
dataobjectrequired
The group.
Show child properties
idintegerrequired
Platform id of the group; what every group endpoint takes.
meta_group_idstring | nulloptional
WhatsApp's own group id. Null while the group is still being created.
request_idstring | nulloptional
WhatsApp's create request id; how the confirmation webhook is matched.
phone_number_idstringoptional
The business number the group was created from.
waba_idstring | nulloptional
The WhatsApp Business Account the number belongs to.
subjectstringrequired
The group name, up to 128 characters.
maxLength
128
descriptionstring | nulloptional
What the group is for; members see it before joining. Up to 2048 characters.
maxLength
2048
join_approval_modestringoptional
auto_approve: anyone with the link joins. approval_required: the business approves each request.
enum
["auto_approve","approval_required"]
invite_linkstring | nulloptional
The chat.whatsapp.com link people tap to join. Null until WhatsApp confirms the group.
statusstringrequired
creating (waiting for WhatsApp), active, suspended (by WhatsApp, for policy), deleted, or failed (WhatsApp refused to create it; see last_error).
enum
["creating","active","suspended","deleted","failed"]
participant_countintegerrequired
Members besides the business.
max_participantsintegerrequired
8, the business counted in.
seats_leftintegeroptional
How many more people can join.
pending_join_requestsintegeroptional
People waiting for approval on an approval_required group.
conversation_idinteger | nulloptional
The inbox thread for the group.
invite_template_idinteger | nulloptional
The approved template used for invites from this group.
last_message_atstring | nulloptional
When the thread last had a message, either way.
format
date-time
last_errorobject | array | nulloptional
WhatsApp's last refusal, when there was one.
last_synced_atstring | nulloptional
When the roster and settings were last read back from WhatsApp.
format
date-time
created_atstring | nulloptional
When the platform created the record.
format
date-time
updated_atstring | nulloptional
When it last changed.
format
date-time
participantsarray<object>optional
Everyone ever invited into or seen in the group, with their current state.
Show child properties
idintegeroptional
Platform id of the group; what every group endpoint takes.
wa_idstringoptional
The person, as WhatsApp identifies them (digits, international format).
display_namestring | nulloptional
The name WhatsApp showed with their last message, when known.
statusstringoptional
creating (waiting for WhatsApp), active, suspended (by WhatsApp, for policy), deleted, or failed (WhatsApp refused to create it; see last_error).
enum
["invited","member","left","removed","failed"]
invited_atstring | nulloptional
When the invite template was sent to them.
format
date-time
joined_atstring | nulloptional
When they joined.
format
date-time
left_atstring | nulloptional
When they left or were removed.
format
date-time
reasonstring | nulloptional
How they got here: invite_link, left, removed_by_business, sync, group_deleted.
last_errorobject | array | nulloptional
WhatsApp's last refusal, when there was one.
join_requestsarray<object>optional
Join requests, newest first.
Show child properties
idintegeroptional
Platform id of the group; what every group endpoint takes.
join_request_idstringoptional
WhatsApp's id for the request; what approve and reject take.
wa_idstringoptional
The person, as WhatsApp identifies them (digits, international format).
statusstringoptional
creating (waiting for WhatsApp), active, suspended (by WhatsApp, for policy), deleted, or failed (WhatsApp refused to create it; see last_error).
enum
["pending","approved","rejected","revoked","failed"]
requested_atstring | nulloptional
When they asked to join.
format
date-time
resolved_atstring | nulloptional
When the request was approved, rejected or withdrawn.
format
date-time
invite_templateobject | nulloptional
Name of an approved group-invite template on this account.
Show child properties
idintegeroptional
Platform id of the group; what every group endpoint takes.
namestringoptional
Template name.
languagestring | nulloptional
Template language code.
whatsapp_statusstring | nulloptional
The template's approval state on WhatsApp.
eventsarray<object>optional
Recent activity, newest first.
Show child properties
idintegeroptional
Platform id of the group; what every group endpoint takes.
typestringoptional
What happened, e.g. group.participant_joined.
actorstring | nulloptional
Who did it: business, participant, meta, or a user of this platform.
enum
["business","participant","meta","user",null]
payloadobject | array | nulloptional
Event-specific detail.
occurred_atstring | nulloptional
When it happened.
format
date-time
{
    "status": "success",
    "data": {
        "id": 12,
        "meta_group_id": "Y2FwaV9ncm91cDo6MTIzNDU2",
        "request_id": "b5c1\u2026",
        "phone_number_id": "243438852181644",
        "waba_id": "1029384756",
        "subject": "VIP customers \u2014 September",
        "description": "Offers first.",
        "join_approval_mode": "auto_approve",
        "invite_link": "https://chat.whatsapp.com/AbCdEf123",
        "status": "active",
        "participant_count": 5,
        "max_participants": 8,
        "seats_left": 2,
        "pending_join_requests": 0,
        "conversation_id": 8812,
        "invite_template_id": 41,
        "last_message_at": "2026-09-07T10:12:00+03:00",
        "last_error": null,
        "last_synced_at": "2026-09-07T09:00:00+03:00",
        "created_at": "2026-09-01T08:00:00+03:00",
        "updated_at": "2026-09-07T10:12:00+03:00",
        "participants": [
            {
                "id": 1,
                "wa_id": "255711000001",
                "display_name": "Asha",
                "status": "member",
                "invited_at": "2026-09-01T08:05:00+03:00",
                "joined_at": "2026-09-01T08:09:00+03:00",
                "left_at": null,
                "reason": "invite_link",
                "last_error": null
            }
        ],
        "join_requests": [
            {
                "id": 3,
                "join_request_id": "JR-1",
                "wa_id": "255711000005",
                "status": "pending",
                "requested_at": "2026-09-07T10:00:00+03:00",
                "resolved_at": null
            }
        ],
        "invite_template": {
            "id": 41,
            "name": "group_invite_link",
            "language": "en",
            "whatsapp_status": "approved"
        },
        "events": [
            {
                "id": 90,
                "type": "group.participant_joined",
                "actor": "participant",
                "payload": {
                    "wa_ids": [
                        "255711000001"
                    ],
                    "reason": "invite_link"
                },
                "occurred_at": "2026-09-01T08:09:00+03:00"
            }
        ]
    }
}
default
{
    "status": "success",
    "data": {
        "id": 12,
        "meta_group_id": "Y2FwaV9ncm91cDo6MTIzNDU2",
        "request_id": "b5c1\u2026",
        "phone_number_id": "243438852181644",
        "waba_id": "1029384756",
        "subject": "VIP customers \u2014 September",
        "description": "Offers first.",
        "join_approval_mode": "auto_approve",
        "invite_link": "https://chat.whatsapp.com/AbCdEf123",
        "status": "active",
        "participant_count": 5,
        "max_participants": 8,
        "seats_left": 2,
        "pending_join_requests": 0,
        "conversation_id": 8812,
        "invite_template_id": 41,
        "last_message_at": "2026-09-07T10:12:00+03:00",
        "last_error": null,
        "last_synced_at": "2026-09-07T09:00:00+03:00",
        "created_at": "2026-09-01T08:00:00+03:00",
        "updated_at": "2026-09-07T10:12:00+03:00",
        "participants": [
            {
                "id": 1,
                "wa_id": "255711000001",
                "display_name": "Asha",
                "status": "member",
                "invited_at": "2026-09-01T08:05:00+03:00",
                "joined_at": "2026-09-01T08:09:00+03:00",
                "left_at": null,
                "reason": "invite_link",
                "last_error": null
            }
        ],
        "join_requests": [
            {
                "id": 3,
                "join_request_id": "JR-1",
                "wa_id": "255711000005",
                "status": "pending",
                "requested_at": "2026-09-07T10:00:00+03:00",
                "resolved_at": null
            }
        ],
        "invite_template": {
            "id": 41,
            "name": "group_invite_link",
            "language": "en",
            "whatsapp_status": "approved"
        },
        "events": [
            {
                "id": 90,
                "type": "group.participant_joined",
                "actor": "participant",
                "payload": {
                    "wa_ids": [
                        "255711000001"
                    ],
                    "reason": "invite_link"
                },
                "occurred_at": "2026-09-01T08:09:00+03:00"
            }
        ]
    }
}
404Group not found.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Group not found"
}
default
{
    "status": "error",
    "message": "Group not found"
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The key's issuer does not hold communications.groups.view, or the key has no issuer on record.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This API key was issued by a user without the \"communications.groups.view\" permission."
}
default
{
    "status": "error",
    "message": "This API key was issued by a user without the \"communications.groups.view\" permission."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / WhatsApp groups

Update a group's subject or description

PATCH/api/v3/whatsapp/groups/{id}

Applied optimistically; WhatsApp confirms through the settings webhook and the group is re-synced if it refused.

AuthenticationTenant API token

Required permission: communications.groups.manage

Path parameters

idintegerrequired
The platform id of the group (from the list).

Example: 12

Request body

application/json · required

subjectstringoptional
The group name, up to 128 characters.
maxLength
128
descriptionstringoptional
What the group is for; members see it before joining. Up to 2048 characters.
maxLength
2048
Complete request schema
{
    "type": "object",
    "properties": {
        "subject": {
            "type": "string",
            "maxLength": 128,
            "description": "The group name, up to 128 characters."
        },
        "description": {
            "type": "string",
            "maxLength": 2048,
            "description": "What the group is for; members see it before joining. Up to 2048 characters."
        }
    }
}
default
{
    "subject": "VIP customers \u2014 October"
}

Responses

200The group.
statusstringrequired
Always "success" on a 2xx response.
enum
["success"]
dataobjectrequired
The group.
Show child properties
idintegerrequired
Platform id of the group; what every group endpoint takes.
meta_group_idstring | nulloptional
WhatsApp's own group id. Null while the group is still being created.
request_idstring | nulloptional
WhatsApp's create request id; how the confirmation webhook is matched.
phone_number_idstringoptional
The business number the group was created from.
waba_idstring | nulloptional
The WhatsApp Business Account the number belongs to.
subjectstringrequired
The group name, up to 128 characters.
maxLength
128
descriptionstring | nulloptional
What the group is for; members see it before joining. Up to 2048 characters.
maxLength
2048
join_approval_modestringoptional
auto_approve: anyone with the link joins. approval_required: the business approves each request.
enum
["auto_approve","approval_required"]
invite_linkstring | nulloptional
The chat.whatsapp.com link people tap to join. Null until WhatsApp confirms the group.
statusstringrequired
creating (waiting for WhatsApp), active, suspended (by WhatsApp, for policy), deleted, or failed (WhatsApp refused to create it; see last_error).
enum
["creating","active","suspended","deleted","failed"]
participant_countintegerrequired
Members besides the business.
max_participantsintegerrequired
8, the business counted in.
seats_leftintegeroptional
How many more people can join.
pending_join_requestsintegeroptional
People waiting for approval on an approval_required group.
conversation_idinteger | nulloptional
The inbox thread for the group.
invite_template_idinteger | nulloptional
The approved template used for invites from this group.
last_message_atstring | nulloptional
When the thread last had a message, either way.
format
date-time
last_errorobject | array | nulloptional
WhatsApp's last refusal, when there was one.
last_synced_atstring | nulloptional
When the roster and settings were last read back from WhatsApp.
format
date-time
created_atstring | nulloptional
When the platform created the record.
format
date-time
updated_atstring | nulloptional
When it last changed.
format
date-time
participantsarray<object>optional
Everyone ever invited into or seen in the group, with their current state.
Show child properties
idintegeroptional
Platform id of the group; what every group endpoint takes.
wa_idstringoptional
The person, as WhatsApp identifies them (digits, international format).
display_namestring | nulloptional
The name WhatsApp showed with their last message, when known.
statusstringoptional
creating (waiting for WhatsApp), active, suspended (by WhatsApp, for policy), deleted, or failed (WhatsApp refused to create it; see last_error).
enum
["invited","member","left","removed","failed"]
invited_atstring | nulloptional
When the invite template was sent to them.
format
date-time
joined_atstring | nulloptional
When they joined.
format
date-time
left_atstring | nulloptional
When they left or were removed.
format
date-time
reasonstring | nulloptional
How they got here: invite_link, left, removed_by_business, sync, group_deleted.
last_errorobject | array | nulloptional
WhatsApp's last refusal, when there was one.
join_requestsarray<object>optional
Join requests, newest first.
Show child properties
idintegeroptional
Platform id of the group; what every group endpoint takes.
join_request_idstringoptional
WhatsApp's id for the request; what approve and reject take.
wa_idstringoptional
The person, as WhatsApp identifies them (digits, international format).
statusstringoptional
creating (waiting for WhatsApp), active, suspended (by WhatsApp, for policy), deleted, or failed (WhatsApp refused to create it; see last_error).
enum
["pending","approved","rejected","revoked","failed"]
requested_atstring | nulloptional
When they asked to join.
format
date-time
resolved_atstring | nulloptional
When the request was approved, rejected or withdrawn.
format
date-time
invite_templateobject | nulloptional
Name of an approved group-invite template on this account.
Show child properties
idintegeroptional
Platform id of the group; what every group endpoint takes.
namestringoptional
Template name.
languagestring | nulloptional
Template language code.
whatsapp_statusstring | nulloptional
The template's approval state on WhatsApp.
eventsarray<object>optional
Recent activity, newest first.
Show child properties
idintegeroptional
Platform id of the group; what every group endpoint takes.
typestringoptional
What happened, e.g. group.participant_joined.
actorstring | nulloptional
Who did it: business, participant, meta, or a user of this platform.
enum
["business","participant","meta","user",null]
payloadobject | array | nulloptional
Event-specific detail.
occurred_atstring | nulloptional
When it happened.
format
date-time
{
    "status": "success",
    "data": {
        "id": 12,
        "meta_group_id": "Y2FwaV9ncm91cDo6MTIzNDU2",
        "request_id": "b5c1\u2026",
        "phone_number_id": "243438852181644",
        "waba_id": "1029384756",
        "subject": "VIP customers \u2014 September",
        "description": "Offers first.",
        "join_approval_mode": "auto_approve",
        "invite_link": "https://chat.whatsapp.com/AbCdEf123",
        "status": "active",
        "participant_count": 5,
        "max_participants": 8,
        "seats_left": 2,
        "pending_join_requests": 0,
        "conversation_id": 8812,
        "invite_template_id": 41,
        "last_message_at": "2026-09-07T10:12:00+03:00",
        "last_error": null,
        "last_synced_at": "2026-09-07T09:00:00+03:00",
        "created_at": "2026-09-01T08:00:00+03:00",
        "updated_at": "2026-09-07T10:12:00+03:00",
        "participants": [
            {
                "id": 1,
                "wa_id": "255711000001",
                "display_name": "Asha",
                "status": "member",
                "invited_at": "2026-09-01T08:05:00+03:00",
                "joined_at": "2026-09-01T08:09:00+03:00",
                "left_at": null,
                "reason": "invite_link",
                "last_error": null
            }
        ],
        "join_requests": [
            {
                "id": 3,
                "join_request_id": "JR-1",
                "wa_id": "255711000005",
                "status": "pending",
                "requested_at": "2026-09-07T10:00:00+03:00",
                "resolved_at": null
            }
        ],
        "invite_template": {
            "id": 41,
            "name": "group_invite_link",
            "language": "en",
            "whatsapp_status": "approved"
        },
        "events": [
            {
                "id": 90,
                "type": "group.participant_joined",
                "actor": "participant",
                "payload": {
                    "wa_ids": [
                        "255711000001"
                    ],
                    "reason": "invite_link"
                },
                "occurred_at": "2026-09-01T08:09:00+03:00"
            }
        ]
    }
}
default
{
    "status": "success",
    "data": {
        "id": 12,
        "meta_group_id": "Y2FwaV9ncm91cDo6MTIzNDU2",
        "request_id": "b5c1\u2026",
        "phone_number_id": "243438852181644",
        "waba_id": "1029384756",
        "subject": "VIP customers \u2014 September",
        "description": "Offers first.",
        "join_approval_mode": "auto_approve",
        "invite_link": "https://chat.whatsapp.com/AbCdEf123",
        "status": "active",
        "participant_count": 5,
        "max_participants": 8,
        "seats_left": 2,
        "pending_join_requests": 0,
        "conversation_id": 8812,
        "invite_template_id": 41,
        "last_message_at": "2026-09-07T10:12:00+03:00",
        "last_error": null,
        "last_synced_at": "2026-09-07T09:00:00+03:00",
        "created_at": "2026-09-01T08:00:00+03:00",
        "updated_at": "2026-09-07T10:12:00+03:00",
        "participants": [
            {
                "id": 1,
                "wa_id": "255711000001",
                "display_name": "Asha",
                "status": "member",
                "invited_at": "2026-09-01T08:05:00+03:00",
                "joined_at": "2026-09-01T08:09:00+03:00",
                "left_at": null,
                "reason": "invite_link",
                "last_error": null
            }
        ],
        "join_requests": [
            {
                "id": 3,
                "join_request_id": "JR-1",
                "wa_id": "255711000005",
                "status": "pending",
                "requested_at": "2026-09-07T10:00:00+03:00",
                "resolved_at": null
            }
        ],
        "invite_template": {
            "id": 41,
            "name": "group_invite_link",
            "language": "en",
            "whatsapp_status": "approved"
        },
        "events": [
            {
                "id": 90,
                "type": "group.participant_joined",
                "actor": "participant",
                "payload": {
                    "wa_ids": [
                        "255711000001"
                    ],
                    "reason": "invite_link"
                },
                "occurred_at": "2026-09-01T08:09:00+03:00"
            }
        ]
    }
}
404Group not found.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Group not found"
}
default
{
    "status": "error",
    "message": "Group not found"
}
422WhatsApp refused the request, or the group cannot take it right now (not confirmed yet, suspended, deleted). Meta's error code, when there is one, is under errors.meta[0].code.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "WhatsApp suspended this group; nothing can be sent until the suspension clears."
}
default
{
    "status": "error",
    "message": "WhatsApp suspended this group; nothing can be sent until the suspension clears."
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The key's issuer does not hold communications.groups.manage, or the key has no issuer on record.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This API key was issued by a user without the \"communications.groups.manage\" permission."
}
default
{
    "status": "error",
    "message": "This API key was issued by a user without the \"communications.groups.manage\" permission."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / WhatsApp groups

Delete a WhatsApp group

DELETE/api/v3/whatsapp/groups/{id}

Removes everyone and closes the thread. The thread and its history stay readable.

AuthenticationTenant API token

Required permission: communications.groups.manage

Path parameters

idintegerrequired
The platform id of the group (from the list).

Example: 12

Responses

200The group, now deleted.
statusstringrequired
Always "success" on a 2xx response.
enum
["success"]
dataobjectrequired
The group, now deleted.
Show child properties
idintegerrequired
Platform id of the group; what every group endpoint takes.
meta_group_idstring | nulloptional
WhatsApp's own group id. Null while the group is still being created.
request_idstring | nulloptional
WhatsApp's create request id; how the confirmation webhook is matched.
phone_number_idstringoptional
The business number the group was created from.
waba_idstring | nulloptional
The WhatsApp Business Account the number belongs to.
subjectstringrequired
The group name, up to 128 characters.
maxLength
128
descriptionstring | nulloptional
What the group is for; members see it before joining. Up to 2048 characters.
maxLength
2048
join_approval_modestringoptional
auto_approve: anyone with the link joins. approval_required: the business approves each request.
enum
["auto_approve","approval_required"]
invite_linkstring | nulloptional
The chat.whatsapp.com link people tap to join. Null until WhatsApp confirms the group.
statusstringrequired
creating (waiting for WhatsApp), active, suspended (by WhatsApp, for policy), deleted, or failed (WhatsApp refused to create it; see last_error).
enum
["creating","active","suspended","deleted","failed"]
participant_countintegerrequired
Members besides the business.
max_participantsintegerrequired
8, the business counted in.
seats_leftintegeroptional
How many more people can join.
pending_join_requestsintegeroptional
People waiting for approval on an approval_required group.
conversation_idinteger | nulloptional
The inbox thread for the group.
invite_template_idinteger | nulloptional
The approved template used for invites from this group.
last_message_atstring | nulloptional
When the thread last had a message, either way.
format
date-time
last_errorobject | array | nulloptional
WhatsApp's last refusal, when there was one.
last_synced_atstring | nulloptional
When the roster and settings were last read back from WhatsApp.
format
date-time
created_atstring | nulloptional
When the platform created the record.
format
date-time
updated_atstring | nulloptional
When it last changed.
format
date-time
{
    "status": "success",
    "data": {
        "id": 12,
        "meta_group_id": "Y2FwaV9ncm91cDo6MTIzNDU2",
        "request_id": "b5c1\u2026",
        "phone_number_id": "243438852181644",
        "waba_id": "1029384756",
        "subject": "VIP customers \u2014 September",
        "description": "Offers first.",
        "join_approval_mode": "auto_approve",
        "invite_link": "https://chat.whatsapp.com/AbCdEf123",
        "status": "deleted",
        "participant_count": 5,
        "max_participants": 8,
        "seats_left": 2,
        "pending_join_requests": 0,
        "conversation_id": 8812,
        "invite_template_id": 41,
        "last_message_at": "2026-09-07T10:12:00+03:00",
        "last_error": null,
        "last_synced_at": "2026-09-07T09:00:00+03:00",
        "created_at": "2026-09-01T08:00:00+03:00",
        "updated_at": "2026-09-07T10:12:00+03:00"
    }
}
default
{
    "status": "success",
    "data": {
        "id": 12,
        "meta_group_id": "Y2FwaV9ncm91cDo6MTIzNDU2",
        "request_id": "b5c1\u2026",
        "phone_number_id": "243438852181644",
        "waba_id": "1029384756",
        "subject": "VIP customers \u2014 September",
        "description": "Offers first.",
        "join_approval_mode": "auto_approve",
        "invite_link": "https://chat.whatsapp.com/AbCdEf123",
        "status": "deleted",
        "participant_count": 5,
        "max_participants": 8,
        "seats_left": 2,
        "pending_join_requests": 0,
        "conversation_id": 8812,
        "invite_template_id": 41,
        "last_message_at": "2026-09-07T10:12:00+03:00",
        "last_error": null,
        "last_synced_at": "2026-09-07T09:00:00+03:00",
        "created_at": "2026-09-01T08:00:00+03:00",
        "updated_at": "2026-09-07T10:12:00+03:00"
    }
}
404Group not found.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Group not found"
}
default
{
    "status": "error",
    "message": "Group not found"
}
422WhatsApp refused the request, or the group cannot take it right now (not confirmed yet, suspended, deleted). Meta's error code, when there is one, is under errors.meta[0].code.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "WhatsApp suspended this group; nothing can be sent until the suspension clears."
}
default
{
    "status": "error",
    "message": "WhatsApp suspended this group; nothing can be sent until the suspension clears."
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The key's issuer does not hold communications.groups.manage, or the key has no issuer on record.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This API key was issued by a user without the \"communications.groups.manage\" permission."
}
default
{
    "status": "error",
    "message": "This API key was issued by a user without the \"communications.groups.manage\" permission."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / WhatsApp groups

Invite people to a group

POST/api/v3/whatsapp/groups/{id}/invites

Sends each recipient the approved invite-link template as a normal 1:1 template message (billed as such). They join by tapping the link; the roster updates from the webhook.

AuthenticationTenant API token

Required permission: communications.groups.manage

Path parameters

idintegerrequired
The platform id of the group (from the list).

Example: 12

Request body

application/json · required

recipientsarray<string>required
Phone numbers in international format, without the plus sign.
minItems
1
maxItems
7
templatestringoptional
An approved invite template name; the group's own, or the account's first matching one, when omitted.
Complete request schema
{
    "type": "object",
    "properties": {
        "recipients": {
            "type": "array",
            "minItems": 1,
            "maxItems": 7,
            "items": {
                "type": "string"
            },
            "description": "Phone numbers in international format, without the plus sign."
        },
        "template": {
            "type": "string",
            "description": "An approved invite template name; the group's own, or the account's first matching one, when omitted."
        }
    },
    "required": [
        "recipients"
    ]
}
default
{
    "recipients": [
        "255711000003"
    ]
}

Responses

200Who was invited and who was not.
statusstringrequired
Always "success" on a 2xx response.
enum
["success"]
dataobjectrequired
Who was invited and who was not.
Show child properties
sentarray<string>optional
Recipients the invite was queued for.
failedarray<object>optional
Recipients it was not sent to, with the reason.
Show child properties
phonestringoptional
The recipient.
reasonstringoptional
How they got here: invite_link, left, removed_by_business, sync, group_deleted.
groupobjectoptional
The group after the change.
Show child properties
idintegerrequired
Platform id of the group; what every group endpoint takes.
meta_group_idstring | nulloptional
WhatsApp's own group id. Null while the group is still being created.
request_idstring | nulloptional
WhatsApp's create request id; how the confirmation webhook is matched.
phone_number_idstringoptional
The business number the group was created from.
waba_idstring | nulloptional
The WhatsApp Business Account the number belongs to.
subjectstringrequired
The group name, up to 128 characters.
maxLength
128
descriptionstring | nulloptional
What the group is for; members see it before joining. Up to 2048 characters.
maxLength
2048
join_approval_modestringoptional
auto_approve: anyone with the link joins. approval_required: the business approves each request.
enum
["auto_approve","approval_required"]
invite_linkstring | nulloptional
The chat.whatsapp.com link people tap to join. Null until WhatsApp confirms the group.
statusstringrequired
creating (waiting for WhatsApp), active, suspended (by WhatsApp, for policy), deleted, or failed (WhatsApp refused to create it; see last_error).
enum
["creating","active","suspended","deleted","failed"]
participant_countintegerrequired
Members besides the business.
max_participantsintegerrequired
8, the business counted in.
seats_leftintegeroptional
How many more people can join.
pending_join_requestsintegeroptional
People waiting for approval on an approval_required group.
conversation_idinteger | nulloptional
The inbox thread for the group.
invite_template_idinteger | nulloptional
The approved template used for invites from this group.
last_message_atstring | nulloptional
When the thread last had a message, either way.
format
date-time
last_errorobject | array | nulloptional
WhatsApp's last refusal, when there was one.
last_synced_atstring | nulloptional
When the roster and settings were last read back from WhatsApp.
format
date-time
created_atstring | nulloptional
When the platform created the record.
format
date-time
updated_atstring | nulloptional
When it last changed.
format
date-time
{
    "status": "success",
    "data": {
        "sent": [
            "255711000003"
        ],
        "failed": [],
        "group": {
            "id": 12,
            "meta_group_id": "Y2FwaV9ncm91cDo6MTIzNDU2",
            "request_id": "b5c1\u2026",
            "phone_number_id": "243438852181644",
            "waba_id": "1029384756",
            "subject": "VIP customers \u2014 September",
            "description": "Offers first.",
            "join_approval_mode": "auto_approve",
            "invite_link": "https://chat.whatsapp.com/AbCdEf123",
            "status": "active",
            "participant_count": 5,
            "max_participants": 8,
            "seats_left": 2,
            "pending_join_requests": 0,
            "conversation_id": 8812,
            "invite_template_id": 41,
            "last_message_at": "2026-09-07T10:12:00+03:00",
            "last_error": null,
            "last_synced_at": "2026-09-07T09:00:00+03:00",
            "created_at": "2026-09-01T08:00:00+03:00",
            "updated_at": "2026-09-07T10:12:00+03:00"
        }
    }
}
default
{
    "status": "success",
    "data": {
        "sent": [
            "255711000003"
        ],
        "failed": [],
        "group": {
            "id": 12,
            "meta_group_id": "Y2FwaV9ncm91cDo6MTIzNDU2",
            "request_id": "b5c1\u2026",
            "phone_number_id": "243438852181644",
            "waba_id": "1029384756",
            "subject": "VIP customers \u2014 September",
            "description": "Offers first.",
            "join_approval_mode": "auto_approve",
            "invite_link": "https://chat.whatsapp.com/AbCdEf123",
            "status": "active",
            "participant_count": 5,
            "max_participants": 8,
            "seats_left": 2,
            "pending_join_requests": 0,
            "conversation_id": 8812,
            "invite_template_id": 41,
            "last_message_at": "2026-09-07T10:12:00+03:00",
            "last_error": null,
            "last_synced_at": "2026-09-07T09:00:00+03:00",
            "created_at": "2026-09-01T08:00:00+03:00",
            "updated_at": "2026-09-07T10:12:00+03:00"
        }
    }
}
404Group not found.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Group not found"
}
default
{
    "status": "error",
    "message": "Group not found"
}
422WhatsApp refused the request, or the group cannot take it right now (not confirmed yet, suspended, deleted). Meta's error code, when there is one, is under errors.meta[0].code.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "WhatsApp suspended this group; nothing can be sent until the suspension clears."
}
default
{
    "status": "error",
    "message": "WhatsApp suspended this group; nothing can be sent until the suspension clears."
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The key's issuer does not hold communications.groups.manage, or the key has no issuer on record.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This API key was issued by a user without the \"communications.groups.manage\" permission."
}
default
{
    "status": "error",
    "message": "This API key was issued by a user without the \"communications.groups.manage\" permission."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / WhatsApp groups

Remove people from a group

DELETE/api/v3/whatsapp/groups/{id}/participants

Up to 8 per call, by phone number or wa_id.

AuthenticationTenant API token

Required permission: communications.groups.manage

Path parameters

idintegerrequired
The platform id of the group (from the list).

Example: 12

Request body

application/json · required

participantsarray<string>required
Everyone ever invited into or seen in the group, with their current state.
minItems
1
maxItems
8
Complete request schema
{
    "type": "object",
    "properties": {
        "participants": {
            "type": "array",
            "minItems": 1,
            "maxItems": 8,
            "items": {
                "type": "string"
            },
            "description": "Everyone ever invited into or seen in the group, with their current state."
        }
    },
    "required": [
        "participants"
    ]
}
default
{
    "participants": [
        "255711000002"
    ]
}

Responses

200Who was removed.
statusstringrequired
Always "success" on a 2xx response.
enum
["success"]
dataobjectrequired
Who was removed.
Show child properties
removedarray<string>optional
People removed.
failedarray<object>optional
Recipients it was not sent to, with the reason.
groupobjectoptional
The group after the change.
Show child properties
idintegerrequired
Platform id of the group; what every group endpoint takes.
meta_group_idstring | nulloptional
WhatsApp's own group id. Null while the group is still being created.
request_idstring | nulloptional
WhatsApp's create request id; how the confirmation webhook is matched.
phone_number_idstringoptional
The business number the group was created from.
waba_idstring | nulloptional
The WhatsApp Business Account the number belongs to.
subjectstringrequired
The group name, up to 128 characters.
maxLength
128
descriptionstring | nulloptional
What the group is for; members see it before joining. Up to 2048 characters.
maxLength
2048
join_approval_modestringoptional
auto_approve: anyone with the link joins. approval_required: the business approves each request.
enum
["auto_approve","approval_required"]
invite_linkstring | nulloptional
The chat.whatsapp.com link people tap to join. Null until WhatsApp confirms the group.
statusstringrequired
creating (waiting for WhatsApp), active, suspended (by WhatsApp, for policy), deleted, or failed (WhatsApp refused to create it; see last_error).
enum
["creating","active","suspended","deleted","failed"]
participant_countintegerrequired
Members besides the business.
max_participantsintegerrequired
8, the business counted in.
seats_leftintegeroptional
How many more people can join.
pending_join_requestsintegeroptional
People waiting for approval on an approval_required group.
conversation_idinteger | nulloptional
The inbox thread for the group.
invite_template_idinteger | nulloptional
The approved template used for invites from this group.
last_message_atstring | nulloptional
When the thread last had a message, either way.
format
date-time
last_errorobject | array | nulloptional
WhatsApp's last refusal, when there was one.
last_synced_atstring | nulloptional
When the roster and settings were last read back from WhatsApp.
format
date-time
created_atstring | nulloptional
When the platform created the record.
format
date-time
updated_atstring | nulloptional
When it last changed.
format
date-time
participantsarray<object>optional
Everyone ever invited into or seen in the group, with their current state.
Show child properties
idintegeroptional
Platform id of the group; what every group endpoint takes.
wa_idstringoptional
The person, as WhatsApp identifies them (digits, international format).
display_namestring | nulloptional
The name WhatsApp showed with their last message, when known.
statusstringoptional
creating (waiting for WhatsApp), active, suspended (by WhatsApp, for policy), deleted, or failed (WhatsApp refused to create it; see last_error).
enum
["invited","member","left","removed","failed"]
invited_atstring | nulloptional
When the invite template was sent to them.
format
date-time
joined_atstring | nulloptional
When they joined.
format
date-time
left_atstring | nulloptional
When they left or were removed.
format
date-time
reasonstring | nulloptional
How they got here: invite_link, left, removed_by_business, sync, group_deleted.
last_errorobject | array | nulloptional
WhatsApp's last refusal, when there was one.
join_requestsarray<object>optional
Join requests, newest first.
Show child properties
idintegeroptional
Platform id of the group; what every group endpoint takes.
join_request_idstringoptional
WhatsApp's id for the request; what approve and reject take.
wa_idstringoptional
The person, as WhatsApp identifies them (digits, international format).
statusstringoptional
creating (waiting for WhatsApp), active, suspended (by WhatsApp, for policy), deleted, or failed (WhatsApp refused to create it; see last_error).
enum
["pending","approved","rejected","revoked","failed"]
requested_atstring | nulloptional
When they asked to join.
format
date-time
resolved_atstring | nulloptional
When the request was approved, rejected or withdrawn.
format
date-time
invite_templateobject | nulloptional
Name of an approved group-invite template on this account.
Show child properties
idintegeroptional
Platform id of the group; what every group endpoint takes.
namestringoptional
Template name.
languagestring | nulloptional
Template language code.
whatsapp_statusstring | nulloptional
The template's approval state on WhatsApp.
eventsarray<object>optional
Recent activity, newest first.
Show child properties
idintegeroptional
Platform id of the group; what every group endpoint takes.
typestringoptional
What happened, e.g. group.participant_joined.
actorstring | nulloptional
Who did it: business, participant, meta, or a user of this platform.
enum
["business","participant","meta","user",null]
payloadobject | array | nulloptional
Event-specific detail.
occurred_atstring | nulloptional
When it happened.
format
date-time
{
    "status": "success",
    "data": {
        "removed": [
            "255711000002"
        ],
        "failed": [],
        "group": {
            "id": 12,
            "meta_group_id": "Y2FwaV9ncm91cDo6MTIzNDU2",
            "request_id": "b5c1\u2026",
            "phone_number_id": "243438852181644",
            "waba_id": "1029384756",
            "subject": "VIP customers \u2014 September",
            "description": "Offers first.",
            "join_approval_mode": "auto_approve",
            "invite_link": "https://chat.whatsapp.com/AbCdEf123",
            "status": "active",
            "participant_count": 5,
            "max_participants": 8,
            "seats_left": 2,
            "pending_join_requests": 0,
            "conversation_id": 8812,
            "invite_template_id": 41,
            "last_message_at": "2026-09-07T10:12:00+03:00",
            "last_error": null,
            "last_synced_at": "2026-09-07T09:00:00+03:00",
            "created_at": "2026-09-01T08:00:00+03:00",
            "updated_at": "2026-09-07T10:12:00+03:00",
            "participants": [
                {
                    "id": 1,
                    "wa_id": "255711000001",
                    "display_name": "Asha",
                    "status": "member",
                    "invited_at": "2026-09-01T08:05:00+03:00",
                    "joined_at": "2026-09-01T08:09:00+03:00",
                    "left_at": null,
                    "reason": "invite_link",
                    "last_error": null
                }
            ],
            "join_requests": [
                {
                    "id": 3,
                    "join_request_id": "JR-1",
                    "wa_id": "255711000005",
                    "status": "pending",
                    "requested_at": "2026-09-07T10:00:00+03:00",
                    "resolved_at": null
                }
            ],
            "invite_template": {
                "id": 41,
                "name": "group_invite_link",
                "language": "en",
                "whatsapp_status": "approved"
            },
            "events": [
                {
                    "id": 90,
                    "type": "group.participant_joined",
                    "actor": "participant",
                    "payload": {
                        "wa_ids": [
                            "255711000001"
                        ],
                        "reason": "invite_link"
                    },
                    "occurred_at": "2026-09-01T08:09:00+03:00"
                }
            ]
        }
    }
}
default
{
    "status": "success",
    "data": {
        "removed": [
            "255711000002"
        ],
        "failed": [],
        "group": {
            "id": 12,
            "meta_group_id": "Y2FwaV9ncm91cDo6MTIzNDU2",
            "request_id": "b5c1\u2026",
            "phone_number_id": "243438852181644",
            "waba_id": "1029384756",
            "subject": "VIP customers \u2014 September",
            "description": "Offers first.",
            "join_approval_mode": "auto_approve",
            "invite_link": "https://chat.whatsapp.com/AbCdEf123",
            "status": "active",
            "participant_count": 5,
            "max_participants": 8,
            "seats_left": 2,
            "pending_join_requests": 0,
            "conversation_id": 8812,
            "invite_template_id": 41,
            "last_message_at": "2026-09-07T10:12:00+03:00",
            "last_error": null,
            "last_synced_at": "2026-09-07T09:00:00+03:00",
            "created_at": "2026-09-01T08:00:00+03:00",
            "updated_at": "2026-09-07T10:12:00+03:00",
            "participants": [
                {
                    "id": 1,
                    "wa_id": "255711000001",
                    "display_name": "Asha",
                    "status": "member",
                    "invited_at": "2026-09-01T08:05:00+03:00",
                    "joined_at": "2026-09-01T08:09:00+03:00",
                    "left_at": null,
                    "reason": "invite_link",
                    "last_error": null
                }
            ],
            "join_requests": [
                {
                    "id": 3,
                    "join_request_id": "JR-1",
                    "wa_id": "255711000005",
                    "status": "pending",
                    "requested_at": "2026-09-07T10:00:00+03:00",
                    "resolved_at": null
                }
            ],
            "invite_template": {
                "id": 41,
                "name": "group_invite_link",
                "language": "en",
                "whatsapp_status": "approved"
            },
            "events": [
                {
                    "id": 90,
                    "type": "group.participant_joined",
                    "actor": "participant",
                    "payload": {
                        "wa_ids": [
                            "255711000001"
                        ],
                        "reason": "invite_link"
                    },
                    "occurred_at": "2026-09-01T08:09:00+03:00"
                }
            ]
        }
    }
}
404Group not found.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Group not found"
}
default
{
    "status": "error",
    "message": "Group not found"
}
422WhatsApp refused the request, or the group cannot take it right now (not confirmed yet, suspended, deleted). Meta's error code, when there is one, is under errors.meta[0].code.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "WhatsApp suspended this group; nothing can be sent until the suspension clears."
}
default
{
    "status": "error",
    "message": "WhatsApp suspended this group; nothing can be sent until the suspension clears."
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The key's issuer does not hold communications.groups.manage, or the key has no issuer on record.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This API key was issued by a user without the \"communications.groups.manage\" permission."
}
default
{
    "status": "error",
    "message": "This API key was issued by a user without the \"communications.groups.manage\" permission."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / WhatsApp groups

List join requests

GET/api/v3/whatsapp/groups/{id}/join-requests

Everyone who asked to join an approval-required group, newest first.

AuthenticationTenant API token

Required permission: communications.groups.view

Path parameters

idintegerrequired
The platform id of the group (from the list).

Example: 12

Responses

200Join requests.
statusstringrequired
Always "success" on a 2xx response.
enum
["success"]
dataobjectrequired
Join requests.
Show child properties
itemsarray<object>optional
The rows on this page.
Show child properties
idintegeroptional
Platform id of the group; what every group endpoint takes.
join_request_idstringoptional
WhatsApp's id for the request; what approve and reject take.
wa_idstringoptional
The person, as WhatsApp identifies them (digits, international format).
statusstringoptional
creating (waiting for WhatsApp), active, suspended (by WhatsApp, for policy), deleted, or failed (WhatsApp refused to create it; see last_error).
enum
["pending","approved","rejected","revoked","failed"]
requested_atstring | nulloptional
When they asked to join.
format
date-time
resolved_atstring | nulloptional
When the request was approved, rejected or withdrawn.
format
date-time
{
    "status": "success",
    "data": {
        "items": [
            {
                "id": 3,
                "join_request_id": "JR-1",
                "wa_id": "255711000005",
                "status": "pending",
                "requested_at": "2026-09-07T10:00:00+03:00",
                "resolved_at": null
            }
        ]
    }
}
default
{
    "status": "success",
    "data": {
        "items": [
            {
                "id": 3,
                "join_request_id": "JR-1",
                "wa_id": "255711000005",
                "status": "pending",
                "requested_at": "2026-09-07T10:00:00+03:00",
                "resolved_at": null
            }
        ]
    }
}
404Group not found.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Group not found"
}
default
{
    "status": "error",
    "message": "Group not found"
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The key's issuer does not hold communications.groups.view, or the key has no issuer on record.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This API key was issued by a user without the \"communications.groups.view\" permission."
}
default
{
    "status": "error",
    "message": "This API key was issued by a user without the \"communications.groups.view\" permission."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / WhatsApp groups

Approve join requests

POST/api/v3/whatsapp/groups/{id}/join-requests/approve

Lets the people in the request into the group. Their join shows up on the group.participant_joined webhook.

AuthenticationTenant API token

Required permission: communications.groups.manage

Path parameters

idintegerrequired
The platform id of the group (from the list).

Example: 12

Request body

application/json · required

join_requestsarray<string>required
join_request_id values from the list or the group.join_requested webhook.
minItems
1
Complete request schema
{
    "type": "object",
    "properties": {
        "join_requests": {
            "type": "array",
            "minItems": 1,
            "items": {
                "type": "string"
            },
            "description": "join_request_id values from the list or the group.join_requested webhook."
        }
    },
    "required": [
        "join_requests"
    ]
}
default
{
    "join_requests": [
        "JR-1"
    ]
}

Responses

200Result per request.
statusstringrequired
Always "success" on a 2xx response.
enum
["success"]
dataobjectrequired
Result per request.
Show child properties
approvedarray<string>optional
Requests approved.
failedarray<object>optional
Recipients it was not sent to, with the reason.
groupobjectoptional
The group after the change.
Show child properties
idintegerrequired
Platform id of the group; what every group endpoint takes.
meta_group_idstring | nulloptional
WhatsApp's own group id. Null while the group is still being created.
request_idstring | nulloptional
WhatsApp's create request id; how the confirmation webhook is matched.
phone_number_idstringoptional
The business number the group was created from.
waba_idstring | nulloptional
The WhatsApp Business Account the number belongs to.
subjectstringrequired
The group name, up to 128 characters.
maxLength
128
descriptionstring | nulloptional
What the group is for; members see it before joining. Up to 2048 characters.
maxLength
2048
join_approval_modestringoptional
auto_approve: anyone with the link joins. approval_required: the business approves each request.
enum
["auto_approve","approval_required"]
invite_linkstring | nulloptional
The chat.whatsapp.com link people tap to join. Null until WhatsApp confirms the group.
statusstringrequired
creating (waiting for WhatsApp), active, suspended (by WhatsApp, for policy), deleted, or failed (WhatsApp refused to create it; see last_error).
enum
["creating","active","suspended","deleted","failed"]
participant_countintegerrequired
Members besides the business.
max_participantsintegerrequired
8, the business counted in.
seats_leftintegeroptional
How many more people can join.
pending_join_requestsintegeroptional
People waiting for approval on an approval_required group.
conversation_idinteger | nulloptional
The inbox thread for the group.
invite_template_idinteger | nulloptional
The approved template used for invites from this group.
last_message_atstring | nulloptional
When the thread last had a message, either way.
format
date-time
last_errorobject | array | nulloptional
WhatsApp's last refusal, when there was one.
last_synced_atstring | nulloptional
When the roster and settings were last read back from WhatsApp.
format
date-time
created_atstring | nulloptional
When the platform created the record.
format
date-time
updated_atstring | nulloptional
When it last changed.
format
date-time
participantsarray<object>optional
Everyone ever invited into or seen in the group, with their current state.
Show child properties
idintegeroptional
Platform id of the group; what every group endpoint takes.
wa_idstringoptional
The person, as WhatsApp identifies them (digits, international format).
display_namestring | nulloptional
The name WhatsApp showed with their last message, when known.
statusstringoptional
creating (waiting for WhatsApp), active, suspended (by WhatsApp, for policy), deleted, or failed (WhatsApp refused to create it; see last_error).
enum
["invited","member","left","removed","failed"]
invited_atstring | nulloptional
When the invite template was sent to them.
format
date-time
joined_atstring | nulloptional
When they joined.
format
date-time
left_atstring | nulloptional
When they left or were removed.
format
date-time
reasonstring | nulloptional
How they got here: invite_link, left, removed_by_business, sync, group_deleted.
last_errorobject | array | nulloptional
WhatsApp's last refusal, when there was one.
join_requestsarray<object>optional
Join requests, newest first.
Show child properties
idintegeroptional
Platform id of the group; what every group endpoint takes.
join_request_idstringoptional
WhatsApp's id for the request; what approve and reject take.
wa_idstringoptional
The person, as WhatsApp identifies them (digits, international format).
statusstringoptional
creating (waiting for WhatsApp), active, suspended (by WhatsApp, for policy), deleted, or failed (WhatsApp refused to create it; see last_error).
enum
["pending","approved","rejected","revoked","failed"]
requested_atstring | nulloptional
When they asked to join.
format
date-time
resolved_atstring | nulloptional
When the request was approved, rejected or withdrawn.
format
date-time
invite_templateobject | nulloptional
Name of an approved group-invite template on this account.
Show child properties
idintegeroptional
Platform id of the group; what every group endpoint takes.
namestringoptional
Template name.
languagestring | nulloptional
Template language code.
whatsapp_statusstring | nulloptional
The template's approval state on WhatsApp.
eventsarray<object>optional
Recent activity, newest first.
Show child properties
idintegeroptional
Platform id of the group; what every group endpoint takes.
typestringoptional
What happened, e.g. group.participant_joined.
actorstring | nulloptional
Who did it: business, participant, meta, or a user of this platform.
enum
["business","participant","meta","user",null]
payloadobject | array | nulloptional
Event-specific detail.
occurred_atstring | nulloptional
When it happened.
format
date-time
{
    "status": "success",
    "data": {
        "approved": [
            "JR-1"
        ],
        "failed": [],
        "group": {
            "id": 12,
            "meta_group_id": "Y2FwaV9ncm91cDo6MTIzNDU2",
            "request_id": "b5c1\u2026",
            "phone_number_id": "243438852181644",
            "waba_id": "1029384756",
            "subject": "VIP customers \u2014 September",
            "description": "Offers first.",
            "join_approval_mode": "auto_approve",
            "invite_link": "https://chat.whatsapp.com/AbCdEf123",
            "status": "active",
            "participant_count": 5,
            "max_participants": 8,
            "seats_left": 2,
            "pending_join_requests": 0,
            "conversation_id": 8812,
            "invite_template_id": 41,
            "last_message_at": "2026-09-07T10:12:00+03:00",
            "last_error": null,
            "last_synced_at": "2026-09-07T09:00:00+03:00",
            "created_at": "2026-09-01T08:00:00+03:00",
            "updated_at": "2026-09-07T10:12:00+03:00",
            "participants": [
                {
                    "id": 1,
                    "wa_id": "255711000001",
                    "display_name": "Asha",
                    "status": "member",
                    "invited_at": "2026-09-01T08:05:00+03:00",
                    "joined_at": "2026-09-01T08:09:00+03:00",
                    "left_at": null,
                    "reason": "invite_link",
                    "last_error": null
                }
            ],
            "join_requests": [
                {
                    "id": 3,
                    "join_request_id": "JR-1",
                    "wa_id": "255711000005",
                    "status": "pending",
                    "requested_at": "2026-09-07T10:00:00+03:00",
                    "resolved_at": null
                }
            ],
            "invite_template": {
                "id": 41,
                "name": "group_invite_link",
                "language": "en",
                "whatsapp_status": "approved"
            },
            "events": [
                {
                    "id": 90,
                    "type": "group.participant_joined",
                    "actor": "participant",
                    "payload": {
                        "wa_ids": [
                            "255711000001"
                        ],
                        "reason": "invite_link"
                    },
                    "occurred_at": "2026-09-01T08:09:00+03:00"
                }
            ]
        }
    }
}
default
{
    "status": "success",
    "data": {
        "approved": [
            "JR-1"
        ],
        "failed": [],
        "group": {
            "id": 12,
            "meta_group_id": "Y2FwaV9ncm91cDo6MTIzNDU2",
            "request_id": "b5c1\u2026",
            "phone_number_id": "243438852181644",
            "waba_id": "1029384756",
            "subject": "VIP customers \u2014 September",
            "description": "Offers first.",
            "join_approval_mode": "auto_approve",
            "invite_link": "https://chat.whatsapp.com/AbCdEf123",
            "status": "active",
            "participant_count": 5,
            "max_participants": 8,
            "seats_left": 2,
            "pending_join_requests": 0,
            "conversation_id": 8812,
            "invite_template_id": 41,
            "last_message_at": "2026-09-07T10:12:00+03:00",
            "last_error": null,
            "last_synced_at": "2026-09-07T09:00:00+03:00",
            "created_at": "2026-09-01T08:00:00+03:00",
            "updated_at": "2026-09-07T10:12:00+03:00",
            "participants": [
                {
                    "id": 1,
                    "wa_id": "255711000001",
                    "display_name": "Asha",
                    "status": "member",
                    "invited_at": "2026-09-01T08:05:00+03:00",
                    "joined_at": "2026-09-01T08:09:00+03:00",
                    "left_at": null,
                    "reason": "invite_link",
                    "last_error": null
                }
            ],
            "join_requests": [
                {
                    "id": 3,
                    "join_request_id": "JR-1",
                    "wa_id": "255711000005",
                    "status": "pending",
                    "requested_at": "2026-09-07T10:00:00+03:00",
                    "resolved_at": null
                }
            ],
            "invite_template": {
                "id": 41,
                "name": "group_invite_link",
                "language": "en",
                "whatsapp_status": "approved"
            },
            "events": [
                {
                    "id": 90,
                    "type": "group.participant_joined",
                    "actor": "participant",
                    "payload": {
                        "wa_ids": [
                            "255711000001"
                        ],
                        "reason": "invite_link"
                    },
                    "occurred_at": "2026-09-01T08:09:00+03:00"
                }
            ]
        }
    }
}
404Group not found.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Group not found"
}
default
{
    "status": "error",
    "message": "Group not found"
}
422WhatsApp refused the request, or the group cannot take it right now (not confirmed yet, suspended, deleted). Meta's error code, when there is one, is under errors.meta[0].code.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "WhatsApp suspended this group; nothing can be sent until the suspension clears."
}
default
{
    "status": "error",
    "message": "WhatsApp suspended this group; nothing can be sent until the suspension clears."
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The key's issuer does not hold communications.groups.manage, or the key has no issuer on record.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This API key was issued by a user without the \"communications.groups.manage\" permission."
}
default
{
    "status": "error",
    "message": "This API key was issued by a user without the \"communications.groups.manage\" permission."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / WhatsApp groups

Reject join requests

POST/api/v3/whatsapp/groups/{id}/join-requests/reject

Turns the people in the request away. They can ask again with the same link.

AuthenticationTenant API token

Required permission: communications.groups.manage

Path parameters

idintegerrequired
The platform id of the group (from the list).

Example: 12

Request body

application/json · required

join_requestsarray<string>required
join_request_id values from the list or the group.join_requested webhook.
minItems
1
Complete request schema
{
    "type": "object",
    "properties": {
        "join_requests": {
            "type": "array",
            "minItems": 1,
            "items": {
                "type": "string"
            },
            "description": "join_request_id values from the list or the group.join_requested webhook."
        }
    },
    "required": [
        "join_requests"
    ]
}
default
{
    "join_requests": [
        "JR-1"
    ]
}

Responses

200Result per request.
statusstringrequired
Always "success" on a 2xx response.
enum
["success"]
dataobjectrequired
Result per request.
Show child properties
rejectedarray<string>optional
Requests rejected.
failedarray<object>optional
Recipients it was not sent to, with the reason.
groupobjectoptional
The group after the change.
Show child properties
idintegerrequired
Platform id of the group; what every group endpoint takes.
meta_group_idstring | nulloptional
WhatsApp's own group id. Null while the group is still being created.
request_idstring | nulloptional
WhatsApp's create request id; how the confirmation webhook is matched.
phone_number_idstringoptional
The business number the group was created from.
waba_idstring | nulloptional
The WhatsApp Business Account the number belongs to.
subjectstringrequired
The group name, up to 128 characters.
maxLength
128
descriptionstring | nulloptional
What the group is for; members see it before joining. Up to 2048 characters.
maxLength
2048
join_approval_modestringoptional
auto_approve: anyone with the link joins. approval_required: the business approves each request.
enum
["auto_approve","approval_required"]
invite_linkstring | nulloptional
The chat.whatsapp.com link people tap to join. Null until WhatsApp confirms the group.
statusstringrequired
creating (waiting for WhatsApp), active, suspended (by WhatsApp, for policy), deleted, or failed (WhatsApp refused to create it; see last_error).
enum
["creating","active","suspended","deleted","failed"]
participant_countintegerrequired
Members besides the business.
max_participantsintegerrequired
8, the business counted in.
seats_leftintegeroptional
How many more people can join.
pending_join_requestsintegeroptional
People waiting for approval on an approval_required group.
conversation_idinteger | nulloptional
The inbox thread for the group.
invite_template_idinteger | nulloptional
The approved template used for invites from this group.
last_message_atstring | nulloptional
When the thread last had a message, either way.
format
date-time
last_errorobject | array | nulloptional
WhatsApp's last refusal, when there was one.
last_synced_atstring | nulloptional
When the roster and settings were last read back from WhatsApp.
format
date-time
created_atstring | nulloptional
When the platform created the record.
format
date-time
updated_atstring | nulloptional
When it last changed.
format
date-time
participantsarray<object>optional
Everyone ever invited into or seen in the group, with their current state.
Show child properties
idintegeroptional
Platform id of the group; what every group endpoint takes.
wa_idstringoptional
The person, as WhatsApp identifies them (digits, international format).
display_namestring | nulloptional
The name WhatsApp showed with their last message, when known.
statusstringoptional
creating (waiting for WhatsApp), active, suspended (by WhatsApp, for policy), deleted, or failed (WhatsApp refused to create it; see last_error).
enum
["invited","member","left","removed","failed"]
invited_atstring | nulloptional
When the invite template was sent to them.
format
date-time
joined_atstring | nulloptional
When they joined.
format
date-time
left_atstring | nulloptional
When they left or were removed.
format
date-time
reasonstring | nulloptional
How they got here: invite_link, left, removed_by_business, sync, group_deleted.
last_errorobject | array | nulloptional
WhatsApp's last refusal, when there was one.
join_requestsarray<object>optional
Join requests, newest first.
Show child properties
idintegeroptional
Platform id of the group; what every group endpoint takes.
join_request_idstringoptional
WhatsApp's id for the request; what approve and reject take.
wa_idstringoptional
The person, as WhatsApp identifies them (digits, international format).
statusstringoptional
creating (waiting for WhatsApp), active, suspended (by WhatsApp, for policy), deleted, or failed (WhatsApp refused to create it; see last_error).
enum
["pending","approved","rejected","revoked","failed"]
requested_atstring | nulloptional
When they asked to join.
format
date-time
resolved_atstring | nulloptional
When the request was approved, rejected or withdrawn.
format
date-time
invite_templateobject | nulloptional
Name of an approved group-invite template on this account.
Show child properties
idintegeroptional
Platform id of the group; what every group endpoint takes.
namestringoptional
Template name.
languagestring | nulloptional
Template language code.
whatsapp_statusstring | nulloptional
The template's approval state on WhatsApp.
eventsarray<object>optional
Recent activity, newest first.
Show child properties
idintegeroptional
Platform id of the group; what every group endpoint takes.
typestringoptional
What happened, e.g. group.participant_joined.
actorstring | nulloptional
Who did it: business, participant, meta, or a user of this platform.
enum
["business","participant","meta","user",null]
payloadobject | array | nulloptional
Event-specific detail.
occurred_atstring | nulloptional
When it happened.
format
date-time
{
    "status": "success",
    "data": {
        "rejected": [
            "JR-1"
        ],
        "failed": [],
        "group": {
            "id": 12,
            "meta_group_id": "Y2FwaV9ncm91cDo6MTIzNDU2",
            "request_id": "b5c1\u2026",
            "phone_number_id": "243438852181644",
            "waba_id": "1029384756",
            "subject": "VIP customers \u2014 September",
            "description": "Offers first.",
            "join_approval_mode": "auto_approve",
            "invite_link": "https://chat.whatsapp.com/AbCdEf123",
            "status": "active",
            "participant_count": 5,
            "max_participants": 8,
            "seats_left": 2,
            "pending_join_requests": 0,
            "conversation_id": 8812,
            "invite_template_id": 41,
            "last_message_at": "2026-09-07T10:12:00+03:00",
            "last_error": null,
            "last_synced_at": "2026-09-07T09:00:00+03:00",
            "created_at": "2026-09-01T08:00:00+03:00",
            "updated_at": "2026-09-07T10:12:00+03:00",
            "participants": [
                {
                    "id": 1,
                    "wa_id": "255711000001",
                    "display_name": "Asha",
                    "status": "member",
                    "invited_at": "2026-09-01T08:05:00+03:00",
                    "joined_at": "2026-09-01T08:09:00+03:00",
                    "left_at": null,
                    "reason": "invite_link",
                    "last_error": null
                }
            ],
            "join_requests": [
                {
                    "id": 3,
                    "join_request_id": "JR-1",
                    "wa_id": "255711000005",
                    "status": "pending",
                    "requested_at": "2026-09-07T10:00:00+03:00",
                    "resolved_at": null
                }
            ],
            "invite_template": {
                "id": 41,
                "name": "group_invite_link",
                "language": "en",
                "whatsapp_status": "approved"
            },
            "events": [
                {
                    "id": 90,
                    "type": "group.participant_joined",
                    "actor": "participant",
                    "payload": {
                        "wa_ids": [
                            "255711000001"
                        ],
                        "reason": "invite_link"
                    },
                    "occurred_at": "2026-09-01T08:09:00+03:00"
                }
            ]
        }
    }
}
default
{
    "status": "success",
    "data": {
        "rejected": [
            "JR-1"
        ],
        "failed": [],
        "group": {
            "id": 12,
            "meta_group_id": "Y2FwaV9ncm91cDo6MTIzNDU2",
            "request_id": "b5c1\u2026",
            "phone_number_id": "243438852181644",
            "waba_id": "1029384756",
            "subject": "VIP customers \u2014 September",
            "description": "Offers first.",
            "join_approval_mode": "auto_approve",
            "invite_link": "https://chat.whatsapp.com/AbCdEf123",
            "status": "active",
            "participant_count": 5,
            "max_participants": 8,
            "seats_left": 2,
            "pending_join_requests": 0,
            "conversation_id": 8812,
            "invite_template_id": 41,
            "last_message_at": "2026-09-07T10:12:00+03:00",
            "last_error": null,
            "last_synced_at": "2026-09-07T09:00:00+03:00",
            "created_at": "2026-09-01T08:00:00+03:00",
            "updated_at": "2026-09-07T10:12:00+03:00",
            "participants": [
                {
                    "id": 1,
                    "wa_id": "255711000001",
                    "display_name": "Asha",
                    "status": "member",
                    "invited_at": "2026-09-01T08:05:00+03:00",
                    "joined_at": "2026-09-01T08:09:00+03:00",
                    "left_at": null,
                    "reason": "invite_link",
                    "last_error": null
                }
            ],
            "join_requests": [
                {
                    "id": 3,
                    "join_request_id": "JR-1",
                    "wa_id": "255711000005",
                    "status": "pending",
                    "requested_at": "2026-09-07T10:00:00+03:00",
                    "resolved_at": null
                }
            ],
            "invite_template": {
                "id": 41,
                "name": "group_invite_link",
                "language": "en",
                "whatsapp_status": "approved"
            },
            "events": [
                {
                    "id": 90,
                    "type": "group.participant_joined",
                    "actor": "participant",
                    "payload": {
                        "wa_ids": [
                            "255711000001"
                        ],
                        "reason": "invite_link"
                    },
                    "occurred_at": "2026-09-01T08:09:00+03:00"
                }
            ]
        }
    }
}
404Group not found.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Group not found"
}
default
{
    "status": "error",
    "message": "Group not found"
}
422WhatsApp refused the request, or the group cannot take it right now (not confirmed yet, suspended, deleted). Meta's error code, when there is one, is under errors.meta[0].code.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "WhatsApp suspended this group; nothing can be sent until the suspension clears."
}
default
{
    "status": "error",
    "message": "WhatsApp suspended this group; nothing can be sent until the suspension clears."
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The key's issuer does not hold communications.groups.manage, or the key has no issuer on record.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This API key was issued by a user without the \"communications.groups.manage\" permission."
}
default
{
    "status": "error",
    "message": "This API key was issued by a user without the \"communications.groups.manage\" permission."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / WhatsApp groups

Send a message into a group

POST/api/v3/whatsapp/groups/{id}/messages

Text, a media link, or an approved template, to everyone in the room. Text and media need a member to have written in the last 24 hours; a template always sends. WhatsApp bills one message per member it is delivered to. Buttons, lists, products and reactions are not accepted in groups.

AuthenticationTenant API token

Required permission: communications.groups.manage, communications.send

Path parameters

idintegerrequired
The platform id of the group (from the list).

Example: 12

Request body

application/json · required

messagestringoptional
The text, or the caption when media_url is given.
maxLength
4096
media_urlstringoptional
A public URL to an image, video, audio file or document.
format
uri
media_typestringoptional
What the media is; document when omitted.
enum
["image","video","audio","document"]
templateobjectoptional
An approved template.
Show child properties
namestringrequired
Template name.
languagestringoptional
Template language code.
componentsarray<object>optional
Template components, exactly as for /whatsapp/send.
Complete request schema
{
    "type": "object",
    "properties": {
        "message": {
            "type": "string",
            "maxLength": 4096,
            "description": "The text, or the caption when media_url is given."
        },
        "media_url": {
            "type": "string",
            "format": "uri",
            "description": "A public URL to an image, video, audio file or document."
        },
        "media_type": {
            "type": "string",
            "enum": [
                "image",
                "video",
                "audio",
                "document"
            ],
            "description": "What the media is; document when omitted."
        },
        "template": {
            "type": "object",
            "properties": {
                "name": {
                    "type": "string",
                    "description": "Template name."
                },
                "language": {
                    "type": "string",
                    "description": "Template language code."
                },
                "components": {
                    "type": "array",
                    "items": {
                        "type": "object"
                    },
                    "description": "Template components, exactly as for /whatsapp/send."
                }
            },
            "required": [
                "name"
            ],
            "description": "An approved template."
        }
    }
}
default
{
    "message": "Ofa ya leo: 20% off hadi saa 12."
}

Responses

201The queued message.
statusstringrequired
Always "success" on a 2xx response.
enum
["success"]
dataobjectrequired
The queued message.
Show child properties
idintegerrequired
Internal numeric id.
uidstringrequired
Public unique identifier (e.g. msg_01JXYZSMS01).
directionstringrequired
Whether you sent the message (`outbound`) or received it (`inbound`).
enum
["inbound","outbound"]
channel_typestringrequired
Channel: sms or whatsapp.
enum
["sms","whatsapp"]
tenant_channel_idintegeroptional
The account channel selected automatically by the outbound routing policy.
channel_codestring | nulloptional
Resolved channel code returned for observability; it is not caller-selectable.
senderstring | nulloptional
Sender identity; inbound messages can contain the customer phone or provider identity.
recipientstringrequired
Recipient phone number (E.164 or national).
bodystringrequired
Message text content.
statusstringrequired
Delivery status. Outbound messages walk queued → processing → sent → delivered → read, or stop at failed with `error_message` set; `received` is what inbound messages carry.
enum
["queued","processing","sent","checking_delivery","delivered","read","failed","received"]
media_urlstring | nulloptional
The attached file, when the message carries one.
media_typestring | nulloptional
The kind of attached media (image, video, audio, document, sticker).
gateway_message_idstring | nulloptional
Provider message ID, used for replies/reactions. Customer message webhooks identify local records with numeric message_id instead.
error_messagestring | nulloptional
Why the send failed, straight from the gateway. Null unless `status` is `failed`.
metadataobject | nulloptional
Anything extra recorded with the message — the interactive or reaction payload, the id it replies to, the source that created it.
additionalProperties
true
template_paramsobject | nulloptional
The template name, language and variables used, when the message was sent from a template.
additionalProperties
true
created_atstring | nulloptional
ISO 8601 timestamp of when the message record was created.
updated_atstring | nulloptional
ISO 8601 timestamp of the last change to the record.
sent_atstring | nulloptional
When the gateway accepted the message. Null until then.
delivered_atstring | nulloptional
When the gateway confirmed delivery to the recipient's device.
read_atstring | nulloptional
When the recipient opened it. WhatsApp only, and only with read receipts on.
{
    "status": "success",
    "data": {
        "id": 901,
        "public_uid": "msg_01JXYZWG01",
        "conversation_id": 8812,
        "direction": "outbound",
        "body": "Ofa ya leo: 20% off hadi saa 12.",
        "status": "queued"
    }
}
default
{
    "status": "success",
    "data": {
        "id": 901,
        "public_uid": "msg_01JXYZWG01",
        "conversation_id": 8812,
        "direction": "outbound",
        "body": "Ofa ya leo: 20% off hadi saa 12.",
        "status": "queued"
    }
}
404Group not found.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Group not found"
}
default
{
    "status": "error",
    "message": "Group not found"
}
422WhatsApp refused the request, or the group cannot take it right now (not confirmed yet, suspended, deleted). Meta's error code, when there is one, is under errors.meta[0].code.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "WhatsApp suspended this group; nothing can be sent until the suspension clears."
}
default
{
    "status": "error",
    "message": "WhatsApp suspended this group; nothing can be sent until the suspension clears."
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The key's issuer does not hold communications.groups.manage, communications.send, or the key has no issuer on record.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This API key was issued by a user without the \"communications.groups.manage\" permission."
}
default
{
    "status": "error",
    "message": "This API key was issued by a user without the \"communications.groups.manage\" permission."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / WhatsApp groups

Pin or unpin a message

POST/api/v3/whatsapp/groups/{id}/pin

At most three pinned at a time; pinning a fourth unpins the oldest.

AuthenticationTenant API token

Required permission: communications.groups.manage

Path parameters

idintegerrequired
The platform id of the group (from the list).

Example: 12

Request body

application/json · required

message_uidstringrequired
The public uid of a delivered message in this group.
pinbooleanrequired
true to pin, false to unpin.
expiration_daysintegeroptional
How long to keep it pinned; WhatsApp allows 1 to 30 days.
minimum
1
maximum
30
Complete request schema
{
    "type": "object",
    "properties": {
        "message_uid": {
            "type": "string",
            "description": "The public uid of a delivered message in this group."
        },
        "pin": {
            "type": "boolean",
            "description": "true to pin, false to unpin."
        },
        "expiration_days": {
            "type": "integer",
            "minimum": 1,
            "maximum": 30,
            "description": "How long to keep it pinned; WhatsApp allows 1 to 30 days."
        }
    },
    "required": [
        "message_uid",
        "pin"
    ]
}
default
{
    "message_uid": "msg_01JXYZWG01",
    "pin": true,
    "expiration_days": 7
}

Responses

200Done.
statusstringrequired
Always "success" on a 2xx response.
enum
["success"]
dataobjectrequired
Done.
Show child properties
pinnedbooleanoptional
Whether the message is pinned now.
{
    "status": "success",
    "data": {
        "pinned": true
    }
}
default
{
    "status": "success",
    "data": {
        "pinned": true
    }
}
404Group not found.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Group not found"
}
default
{
    "status": "error",
    "message": "Group not found"
}
422WhatsApp refused the request, or the group cannot take it right now (not confirmed yet, suspended, deleted). Meta's error code, when there is one, is under errors.meta[0].code.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "WhatsApp suspended this group; nothing can be sent until the suspension clears."
}
default
{
    "status": "error",
    "message": "WhatsApp suspended this group; nothing can be sent until the suspension clears."
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The key's issuer does not hold communications.groups.manage, or the key has no issuer on record.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This API key was issued by a user without the \"communications.groups.manage\" permission."
}
default
{
    "status": "error",
    "message": "This API key was issued by a user without the \"communications.groups.manage\" permission."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Contacts

Create a contact

POST/api/v3/contacts/{group_id}/store

Stores one contact in a group using legacy fields and custom dynamic attributes. POST /api/v3/contact-groups/{group}/contacts is the same operation on the REST path, and …/contacts/batch takes up to 500 at once.

AuthenticationTenant API token

Required permission: contacts.create

Path parameters

idintegerrequired
The platform id of the group (from the list).

Example: 12

Request body

application/json · required

PHONEstringoptional
The phone number, with or without the country code. Combined with `country_code` and normalised for storage.
maxLength
64
country_codestringoptional
Explicit calling code, e.g. 255. The contact parser does not infer this from a +255 PHONE value when omitted.
maxLength
8
namestringoptional
Display name. When absent, FIRST_NAME and LAST_NAME are joined; when those are absent too, the phone number is used.
maxLength
160
FIRST_NAMEstringoptional
First name. Joined with LAST_NAME when `name` is absent.
LAST_NAMEstringoptional
Last name. Joined with FIRST_NAME when `name` is absent.
is_subscribedbooleanoptional
Stored subscription flag. Defaults true on create and retains its value when omitted on update. Current campaign dispatch does not automatically filter this flag.
phone_numberstringoptional
Alias of PHONE. PHONE wins when both are supplied.
maxLength
64
NAMEstringoptional
Name alias used when name is absent.
Provide at least one of these alternatives

PHONE

phone_number

Complete request schema
{
    "type": "object",
    "properties": {
        "PHONE": {
            "type": "string",
            "description": "The phone number, with or without the country code. Combined with `country_code` and normalised for storage.",
            "maxLength": 64
        },
        "country_code": {
            "type": "string",
            "description": "Explicit calling code, e.g. 255. The contact parser does not infer this from a +255 PHONE value when omitted.",
            "maxLength": 8
        },
        "name": {
            "type": "string",
            "description": "Display name. When absent, FIRST_NAME and LAST_NAME are joined; when those are absent too, the phone number is used.",
            "maxLength": 160
        },
        "FIRST_NAME": {
            "type": "string",
            "description": "First name. Joined with LAST_NAME when `name` is absent."
        },
        "LAST_NAME": {
            "type": "string",
            "description": "Last name. Joined with FIRST_NAME when `name` is absent."
        },
        "is_subscribed": {
            "type": "boolean",
            "description": "Stored subscription flag. Defaults true on create and retains its value when omitted on update. Current campaign dispatch does not automatically filter this flag."
        },
        "phone_number": {
            "type": "string",
            "description": "Alias of PHONE. PHONE wins when both are supplied.",
            "maxLength": 64
        },
        "NAME": {
            "type": "string",
            "description": "Name alias used when name is absent."
        }
    },
    "additionalProperties": true,
    "description": "PHONE or phone_number is required, including on PATCH. Name is recalculated and custom fields are replaced, not merged. Nonreserved top-level fields become custom_field_values. Reserved keys include PHONE, phone_number, country_code, name, NAME, FIRST_NAME, LAST_NAME, is_subscribed and _token.",
    "anyOf": [
        {
            "required": [
                "PHONE"
            ]
        },
        {
            "required": [
                "phone_number"
            ]
        }
    ]
}
Minimal — only the phone number is required
{
    "PHONE": "255700333444"
}
With name + structured first/last name
{
    "PHONE": "255700333444",
    "name": "Asha Mwita",
    "FIRST_NAME": "Asha",
    "LAST_NAME": "Mwita"
}
Local phone format + explicit country code
{
    "PHONE": "0700333444",
    "country_code": "TZ",
    "name": "Asha Mwita"
}
Custom merge fields (any keys you don't recognise become custom fields)
{
    "PHONE": "255700333444",
    "FIRST_NAME": "Asha",
    "LAST_NAME": "Mwita",
    "CITY": "Dar es Salaam",
    "ACCOUNT_NUMBER": "AC-2204",
    "PLAN": "Pro",
    "RENEWAL_DATE": "2026-05-01"
}
Mark contact as opted-out (won't receive campaigns)
{
    "PHONE": "255700333444",
    "name": "Asha Mwita",
    "is_subscribed": false
}

Responses

201Contact created. Phone numbers are normalised to E.164 (international) format and de-duplicated within the group — re-posting the same PHONE returns the existing row.
statusstringrequired
Always "success" on a 2xx response.
enum
["success"]
dataobjectrequired
The contact record.
Show child properties
idintegerrequired
Internal numeric id.
uidstringrequired
Public unique identifier.
group_idintegerrequired
Contact group internal id.
group_uidstringrequired
Contact group public uid.
namestringrequired
Contact display name.
country_codestringrequired
Country code (e.g. 255).
phone_numberstringrequired
National number without country code.
full_phone_numberstringrequired
E.164 or full number for sending.
is_subscribedbooleanrequired
Stored subscription flag; not automatically applied by the current SMS campaign dispatch loop.
custom_field_valuesobjectoptional
Every non-reserved field you sent when creating or updating the contact, echoed back. Always an object — `{}` when there are none.
additionalProperties
true
created_atstring | nulloptional
ISO 8601 timestamp of when the contact was created.
updated_atstring | nulloptional
ISO 8601 timestamp of the last change to it.
{
    "status": "success",
    "data": {
        "id": 66,
        "uid": "ctc_01JXYZ001",
        "group_id": 8,
        "group_uid": "grp_01JXYZABC",
        "name": "John Doe",
        "country_code": "255",
        "phone_number": "700333444",
        "full_phone_number": "255700333444",
        "is_subscribed": true,
        "custom_field_values": {
            "CITY": "Dar es Salaam"
        }
    }
}
default
{
    "status": "success",
    "data": {
        "id": 66,
        "uid": "ctc_01JXYZ001",
        "group_id": 8,
        "group_uid": "grp_01JXYZABC",
        "name": "John Doe",
        "country_code": "255",
        "phone_number": "700333444",
        "full_phone_number": "255700333444",
        "is_subscribed": true,
        "custom_field_values": {
            "CITY": "Dar es Salaam"
        }
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
404Group not found.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Contact group not found."
}
default
{
    "status": "error",
    "message": "Contact group not found."
}
422Validation error.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Validation failed.",
    "errors": {
        "PHONE": [
            "The PHONE field is required."
        ]
    }
}
default
{
    "status": "error",
    "message": "Validation failed.",
    "errors": {
        "PHONE": [
            "The PHONE field is required."
        ]
    }
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}

API REFERENCE / Contacts

Find a contact

POST/api/v3/contacts/{group_id}/search/{uid}

Finds a single contact in a group by public uid. GET /api/v3/contact-groups/{group}/contacts/{contact} is the same operation on the REST path.

AuthenticationTenant API token

Required permission: contacts.view

Path parameters

idintegerrequired
The platform id of the group (from the list).

Example: 12

uidstringrequired
The contact `uid` returned when it was created (or its numeric `id`).

Example: ctc_gz0os4at1itzvvpxvewj

Responses

200Single contact.
statusstringrequired
Always "success" on a 2xx response.
enum
["success"]
dataobjectrequired
The contact record.
Show child properties
idintegerrequired
Internal numeric id.
uidstringrequired
Public unique identifier.
group_idintegerrequired
Contact group internal id.
group_uidstringrequired
Contact group public uid.
namestringrequired
Contact display name.
country_codestringrequired
Country code (e.g. 255).
phone_numberstringrequired
National number without country code.
full_phone_numberstringrequired
E.164 or full number for sending.
is_subscribedbooleanrequired
Stored subscription flag; not automatically applied by the current SMS campaign dispatch loop.
custom_field_valuesobjectoptional
Every non-reserved field you sent when creating or updating the contact, echoed back. Always an object — `{}` when there are none.
additionalProperties
true
created_atstring | nulloptional
ISO 8601 timestamp of when the contact was created.
updated_atstring | nulloptional
ISO 8601 timestamp of the last change to it.
{
    "status": "success",
    "data": {
        "id": 66,
        "uid": "ctc_01JXYZ001",
        "group_id": 8,
        "group_uid": "grp_01JXYZABC",
        "name": "John Doe",
        "country_code": "255",
        "phone_number": "700333444",
        "full_phone_number": "255700333444",
        "is_subscribed": true,
        "custom_field_values": {
            "CITY": "Dar es Salaam"
        }
    }
}
default
{
    "status": "success",
    "data": {
        "id": 66,
        "uid": "ctc_01JXYZ001",
        "group_id": 8,
        "group_uid": "grp_01JXYZABC",
        "name": "John Doe",
        "country_code": "255",
        "phone_number": "700333444",
        "full_phone_number": "255700333444",
        "is_subscribed": true,
        "custom_field_values": {
            "CITY": "Dar es Salaam"
        }
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
404Contact not found.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Contact not found."
}
default
{
    "status": "error",
    "message": "Contact not found."
}
422The payload failed validation. `errors` maps each rejected field to its messages.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Validation failed.",
    "errors": {
        "recipient": [
            "Provide recipient or recipients."
        ]
    }
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}

API REFERENCE / Contacts

Update a contact

PATCH/api/v3/contacts/{group_id}/update/{uid}

Changes exactly what you send. Custom fields MERGE into what is stored (send a key as null to remove it), and the name, number and subscription each keep their current value when the payload is silent about them; PHONE is required only when the number itself is changing.

This changed on 2026-09-14. Until then the endpoint replaced the whole custom-field map and renamed the contact after their phone number whenever name was absent, so a call meant only to flip is_subscribed destroyed data. A caller that followed the old advice — read, merge, send everything — is unaffected. PATCH /api/v3/contact-groups/{group}/contacts/{contact} is the same operation on the REST path.

AuthenticationTenant API token

Required permission: contacts.edit

Path parameters

idintegerrequired
The platform id of the group (from the list).

Example: 12

uidstringrequired
The contact `uid` returned when it was created (or its numeric `id`).

Example: ctc_gz0os4at1itzvvpxvewj

Request body

application/json · required

PHONEstringoptional
The phone number, with or without the country code. Combined with `country_code` and normalised for storage.
maxLength
64
country_codestringoptional
Explicit calling code, e.g. 255. The contact parser does not infer this from a +255 PHONE value when omitted.
maxLength
8
namestringoptional
Display name. When absent, FIRST_NAME and LAST_NAME are joined; when those are absent too, the phone number is used.
maxLength
160
is_subscribedbooleanoptional
Stored subscription flag. Defaults true on create and retains its value when omitted on update. Current campaign dispatch does not automatically filter this flag.
FIRST_NAMEstringoptional
First name. Joined with LAST_NAME when `name` is absent.
LAST_NAMEstringoptional
Last name. Joined with FIRST_NAME when `name` is absent.
phone_numberstringoptional
Alias of PHONE. PHONE wins when both are supplied.
maxLength
64
NAMEstringoptional
Name alias used when name is absent.
Provide at least one of these alternatives

PHONE

phone_number

Complete request schema
{
    "type": "object",
    "properties": {
        "PHONE": {
            "type": "string",
            "description": "The phone number, with or without the country code. Combined with `country_code` and normalised for storage.",
            "maxLength": 64
        },
        "country_code": {
            "type": "string",
            "description": "Explicit calling code, e.g. 255. The contact parser does not infer this from a +255 PHONE value when omitted.",
            "maxLength": 8
        },
        "name": {
            "type": "string",
            "description": "Display name. When absent, FIRST_NAME and LAST_NAME are joined; when those are absent too, the phone number is used.",
            "maxLength": 160
        },
        "is_subscribed": {
            "type": "boolean",
            "description": "Stored subscription flag. Defaults true on create and retains its value when omitted on update. Current campaign dispatch does not automatically filter this flag."
        },
        "FIRST_NAME": {
            "type": "string",
            "description": "First name. Joined with LAST_NAME when `name` is absent."
        },
        "LAST_NAME": {
            "type": "string",
            "description": "Last name. Joined with FIRST_NAME when `name` is absent."
        },
        "phone_number": {
            "type": "string",
            "description": "Alias of PHONE. PHONE wins when both are supplied.",
            "maxLength": 64
        },
        "NAME": {
            "type": "string",
            "description": "Name alias used when name is absent."
        }
    },
    "additionalProperties": true,
    "description": "PHONE or phone_number is required, including on PATCH. Name is recalculated and custom fields are replaced, not merged. Nonreserved top-level fields become custom_field_values. Reserved keys include PHONE, phone_number, country_code, name, NAME, FIRST_NAME, LAST_NAME, is_subscribed and _token.",
    "anyOf": [
        {
            "required": [
                "PHONE"
            ]
        },
        {
            "required": [
                "phone_number"
            ]
        }
    ]
}
default
{
    "PHONE": "255700333444",
    "name": "John Updated"
}

Responses

200Contact updated.
statusstringrequired
Always "success" on a 2xx response.
enum
["success"]
dataobjectrequired
The contact record.
Show child properties
idintegerrequired
Internal numeric id.
uidstringrequired
Public unique identifier.
group_idintegerrequired
Contact group internal id.
group_uidstringrequired
Contact group public uid.
namestringrequired
Contact display name.
country_codestringrequired
Country code (e.g. 255).
phone_numberstringrequired
National number without country code.
full_phone_numberstringrequired
E.164 or full number for sending.
is_subscribedbooleanrequired
Stored subscription flag; not automatically applied by the current SMS campaign dispatch loop.
custom_field_valuesobjectoptional
Every non-reserved field you sent when creating or updating the contact, echoed back. Always an object — `{}` when there are none.
additionalProperties
true
created_atstring | nulloptional
ISO 8601 timestamp of when the contact was created.
updated_atstring | nulloptional
ISO 8601 timestamp of the last change to it.
{
    "status": "success",
    "data": {
        "id": 66,
        "uid": "ctc_01JXYZ001",
        "group_id": 8,
        "group_uid": "grp_01JXYZABC",
        "name": "John Updated",
        "country_code": "255",
        "phone_number": "700333444",
        "full_phone_number": "255700333444",
        "is_subscribed": true,
        "custom_field_values": []
    }
}
default
{
    "status": "success",
    "data": {
        "id": 66,
        "uid": "ctc_01JXYZ001",
        "group_id": 8,
        "group_uid": "grp_01JXYZABC",
        "name": "John Updated",
        "country_code": "255",
        "phone_number": "700333444",
        "full_phone_number": "255700333444",
        "is_subscribed": true,
        "custom_field_values": []
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
404Contact or group not found.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Contact not found."
}
default
{
    "status": "error",
    "message": "Contact not found."
}
422Validation error.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Validation failed.",
    "errors": {
        "PHONE": [
            "The PHONE field is required."
        ]
    }
}
default
{
    "status": "error",
    "message": "Validation failed.",
    "errors": {
        "PHONE": [
            "The PHONE field is required."
        ]
    }
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}

API REFERENCE / Contacts

Delete a contact

DELETE/api/v3/contacts/{group_id}/delete/{uid}

Deletes one contact by uid within a contact group. DELETE /api/v3/contact-groups/{group}/contacts/{contact} is the same operation on the REST path.

AuthenticationTenant API token

Required permission: contacts.delete

Path parameters

idintegerrequired
The platform id of the group (from the list).

Example: 12

uidstringrequired
The contact `uid` returned when it was created (or its numeric `id`).

Example: ctc_gz0os4at1itzvvpxvewj

Responses

200Contact deleted.
statusstringrequired
Always "success" on a 2xx response.
enum
["success"]
dataobjectrequired
What was deleted.
Show child properties
deletedbooleanrequired
Always true — the contact is gone.
uidstringrequired
The uid of the deleted contact.
{
    "status": "success",
    "data": {
        "deleted": true,
        "uid": "ctc_01JXYZ001"
    }
}
default
{
    "status": "success",
    "data": {
        "deleted": true,
        "uid": "ctc_01JXYZ001"
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
404Contact or group not found.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Contact not found."
}
default
{
    "status": "error",
    "message": "Contact not found."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}

API REFERENCE / Contacts

List contacts in a group

POST/api/v3/contacts/{group_id}/all

Lists contacts by group with optional search and pagination controls. GET /api/v3/contact-groups/{group}/contacts is the same operation on the REST path.

AuthenticationTenant API token

Required permission: contacts.view

Path parameters

idintegerrequired
The platform id of the group (from the list).

Example: 12

Query parameters

pageintegeroptional
Page number, 1-based. Read `data.pagination.has_more_pages` to know when to stop.
minimum
1
default
1

Example: 1

Request body

application/json

searchstringoptional
Match contacts whose name or phone number contains this text.
limitintegeroptional
Rows per page, 1–100. Defaults to 20.
per_pageintegeroptional
Alias of `limit`.
Complete request schema
{
    "type": "object",
    "properties": {
        "search": {
            "type": "string",
            "description": "Match contacts whose name or phone number contains this text."
        },
        "limit": {
            "type": "integer",
            "description": "Rows per page, 1\u2013100. Defaults to 20."
        },
        "per_page": {
            "type": "integer",
            "description": "Alias of `limit`."
        }
    }
}
Default — first 25 contacts
{
    "limit": 25
}
Search by name or phone substring
{
    "search": "Asha",
    "limit": 25
}
Pagination — page 2
{
    "limit": 25,
    "page": 2
}
Only subscribed (campaign-eligible) contacts
{
    "is_subscribed": true,
    "limit": 50
}

Responses

200Contact collection. Phone numbers are returned in two parts: `country_code` + `phone_number` (local), and a pre-joined `full_phone_number`.
statusstringrequired
Always "success" on a 2xx response.
enum
["success"]
dataobjectrequired
A page of contacts and its page state.
Show child properties
itemsarray<object>required
The contacts on this page, most recently updated first.
Show child properties
idintegerrequired
Internal numeric id.
uidstringrequired
Public unique identifier.
group_idintegerrequired
Contact group internal id.
group_uidstringrequired
Contact group public uid.
namestringrequired
Contact display name.
country_codestringrequired
Country code (e.g. 255).
phone_numberstringrequired
National number without country code.
full_phone_numberstringrequired
E.164 or full number for sending.
is_subscribedbooleanrequired
Stored subscription flag; not automatically applied by the current SMS campaign dispatch loop.
custom_field_valuesobjectoptional
Every non-reserved field you sent when creating or updating the contact, echoed back. Always an object — `{}` when there are none.
additionalProperties
true
created_atstring | nulloptional
ISO 8601 timestamp of when the contact was created.
updated_atstring | nulloptional
ISO 8601 timestamp of the last change to it.
paginationobjectrequired
Page state for this list: where you are and whether more pages follow.
Show child properties
current_pageintegerrequired
1-based current page index.
per_pageintegerrequired
Number of items per page.
last_pageintegerrequired
1-based index of the last page.
totalintegerrequired
Total number of items across all pages.
has_more_pagesbooleanrequired
True if more pages exist after the current page.
{
    "status": "success",
    "data": {
        "items": [
            {
                "id": 66,
                "uid": "ctc_01JXYZ001",
                "group_id": 8,
                "group_uid": "grp_01JXYZABC",
                "name": "John Doe",
                "country_code": "255",
                "phone_number": "700333444",
                "full_phone_number": "255700333444",
                "is_subscribed": true,
                "custom_field_values": {
                    "CITY": "Dar es Salaam"
                }
            }
        ],
        "pagination": {
            "current_page": 1,
            "per_page": 25,
            "last_page": 1,
            "total": 1,
            "has_more_pages": false
        }
    }
}
default
{
    "status": "success",
    "data": {
        "items": [
            {
                "id": 66,
                "uid": "ctc_01JXYZ001",
                "group_id": 8,
                "group_uid": "grp_01JXYZABC",
                "name": "John Doe",
                "country_code": "255",
                "phone_number": "700333444",
                "full_phone_number": "255700333444",
                "is_subscribed": true,
                "custom_field_values": {
                    "CITY": "Dar es Salaam"
                }
            }
        ],
        "pagination": {
            "current_page": 1,
            "per_page": 25,
            "last_page": 1,
            "total": 1,
            "has_more_pages": false
        }
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
404Group not found.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Contact group not found."
}
default
{
    "status": "error",
    "message": "Contact group not found."
}
422The payload failed validation. `errors` maps each rejected field to its messages.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Validation failed.",
    "errors": {
        "recipient": [
            "Provide recipient or recipients."
        ]
    }
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}

API REFERENCE / Profile & Balance

Get current account

GET/api/v3/me

Returns the tenant profile represented by the bearer token.

AuthenticationTenant API token

Responses

200Tenant profile.
statusstringrequired
Always "success" on a 2xx response.
enum
["success"]
dataobjectrequired
The tenant this token belongs to.
Show child properties
idintegerrequired
Tenant id.
namestringrequired
Account name as it appears in the dashboard.
slugstringrequired
URL-safe form of the account name.
external_client_idstring | nulloptional
Your own reference for this account, when one was set. Null otherwise.
created_atstring | nulloptional
ISO 8601 timestamp of when the account was created.
updated_atstring | nulloptional
ISO 8601 timestamp of the last change to it.
{
    "status": "success",
    "data": {
        "id": 12,
        "name": "Workspace Alpha",
        "slug": "workspace-alpha",
        "external_client_id": null
    }
}
default
{
    "status": "success",
    "data": {
        "id": 12,
        "name": "Workspace Alpha",
        "slug": "workspace-alpha",
        "external_client_id": null
    }
}
401Unauthorized.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
default
{
    "status": "error",
    "message": "Invalid API token."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Profile & Balance

Get balance

GET/api/v3/balance

Returns wallet balance, currency, billing mode, and spend metadata.

AuthenticationTenant API token

Responses

200Tenant wallet balance.
statusstringrequired
Always "success" on a 2xx response.
enum
["success"]
dataobjectrequired
The wallet behind this account.
Show child properties
wallet_balancenumberrequired
Spendable balance in `wallet_currency`, in major units (973093.57 is TZS 973,093.57).
wallet_currencystringrequired
ISO 4217 currency the wallet is held in.
billing_modestringrequired
prepaid (sends draw down this balance) or postpaid (sends are invoiced).
cumulative_spend_centsintegeroptional
Lifetime spend in cents, which is what moves the account between pricing tiers. Null before the first charge.
tier_overridebooleanoptional
True when an operator pinned this account to a tier instead of letting spend decide it.
last_updated_atstring | nulloptional
ISO 8601 timestamp of the last wallet movement.
{
    "status": "success",
    "data": {
        "wallet_balance": 12000.5,
        "wallet_currency": "TZS",
        "billing_mode": "prepaid",
        "cumulative_spend_cents": 0,
        "tier_override": false
    }
}
default
{
    "status": "success",
    "data": {
        "wallet_balance": 12000.5,
        "wallet_currency": "TZS",
        "billing_mode": "prepaid",
        "cumulative_spend_cents": 0,
        "tier_override": false
    }
}
401Unauthorized.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Missing bearer token."
}
default
{
    "status": "error",
    "message": "Missing bearer token."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Catalogue

List catalogues

GET/api/v3/catalogues

Every shop belonging to the token's tenant, newest first, each with its product and order counts.

AuthenticationTenant API token

Query parameters

limitintegeroptional
Rows per page, 1–100. Defaults to 20 (25 for catalogue endpoints). Values above 100 are clamped.
minimum
1
maximum
100
default
20

Example: 20

per_pageintegeroptional
Alias of `limit`, for clients that already speak Laravel pagination. `limit` wins if both are sent.
minimum
1
maximum
100

Example: 25

pageintegeroptional
Page number, 1-based. Read `data.pagination.has_more_pages` to know when to stop.
minimum
1
default
1

Example: 1

Responses

200A page of catalogues.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
A page of rows and its page state.
Show child properties
itemsarray<object>required
The catalogues on this page.
Show child properties
idintegerrequired
Catalogue id. Use it in every /catalogues/{catalogue} path.
namestringrequired
Shop name as customers see it.
descriptionstring | nulloptional
Optional shop description.
verticalstring | nulloptional
Meta commerce vertical, e.g. "commerce".
default_currencystring | nulloptional
ISO 4217 currency new products default to.
sku_prefixstring | nulloptional
The prefix on codes this shop issues itself, such as `AMY` in `AMY-00042`. Fixed once the shop exists.
stock_policystringoptional
`external`: your system owns the stock count, we mirror it and tell you what sold. `momo`: we keep the count, and orders reserve and commit against it. Shops created through this API default to `external`.
enum
["external","momo"]
source_of_truthstringoptional
Who wins on product fields when a selling platform has drifted from us.
enum
["api","momo","platform"]
allow_backorderbooleanoptional
Sell past zero. When false, a count of zero sets availability to `out of stock`.
reservation_ttl_hoursintegeroptional
How long a pending order holds stock before it goes back on the shelf.
low_stock_thresholdinteger | nulloptional
Raise `stock.low` at or below this count. Null means never.
meta_catalogue_idstring | nulloptional
Meta catalogue id when the shop is connected; null keeps every product local.
is_connected_to_wababooleanoptional
True once the shop is bound to a WhatsApp Business Account.
is_catalogue_visiblebooleanoptional
Whether customers can browse the catalogue in the chat.
is_cart_enabledbooleanoptional
Whether customers can build a cart and submit an order.
channelsarray<object>optional
Where this shop is published. Empty is normal for a shop that sells only through the assistant or the phone menus.
Show child properties
platformstringrequired
Which platform this presence is on.
enum
["whatsapp","storefront","facebook","instagram","tiktok"]
labelstringrequired
What the merchant named this connection.
external_catalogue_idstring | nulloptional
The platform's own id for the catalogue.
external_account_idstring | nulloptional
The account it is bound to — a WABA id for WhatsApp.
public_slugstring | nulloptional
Storefront address, when this is a hosted storefront.
is_connectedbooleanrequired
Whether customers can currently see it.
last_synced_atstring | nulloptional
When products were last pushed to this platform.
format
date-time
products_countinteger | nulloptional
Number of products in the shop.
orders_countinteger | nulloptional
Number of orders received by the shop.
last_synced_atstring | nulloptional
When the shop last synced to Meta.
format
date-time
created_atstring | nulloptional
ISO 8601 creation timestamp.
format
date-time
updated_atstring | nulloptional
ISO 8601 update timestamp.
format
date-time
paginationobjectrequired
Page state for this list: where you are and whether more pages follow.
Show child properties
current_pageintegerrequired
1-based current page index.
per_pageintegerrequired
Number of items per page.
last_pageintegerrequired
1-based index of the last page.
totalintegerrequired
Total number of items across all pages.
has_more_pagesbooleanrequired
True if more pages exist after the current page.
{
    "status": "success",
    "data": {
        "items": [
            {
                "id": 1,
                "name": "Acme Duka",
                "description": null,
                "vertical": "commerce",
                "default_currency": "TZS",
                "meta_catalogue_id": null,
                "is_connected_to_waba": false,
                "is_catalogue_visible": false,
                "is_cart_enabled": true,
                "products_count": 1,
                "orders_count": 1,
                "last_synced_at": null,
                "created_at": "2026-09-04T19:19:55+00:00",
                "updated_at": "2026-09-04T19:19:55+00:00"
            }
        ],
        "pagination": {
            "current_page": 1,
            "per_page": 25,
            "last_page": 1,
            "total": 1,
            "has_more_pages": false
        }
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Catalogue

Create a catalogue

POST/api/v3/catalogues

Make a shop from your own system, so an integration can finish its setup without anyone opening the dashboard. Connecting the shop to WhatsApp stays a dashboard step: it needs a consent a bearer token cannot give on a person's behalf.

AuthenticationTenant API token

Request body

application/json · required

namestringrequired
Shop name.
maxLength
120
descriptionstringoptional
What this shop sells.
maxLength
2000
verticalstringoptional
Commerce vertical, defaults to `commerce`.
maxLength
40
default_currencystringoptional
Currency for products that do not name one.
minLength
3
maxLength
3
sku_prefixstringoptional
Prefix for codes the shop issues itself. Accepted on create only.
maxLength
8
stock_policystringoptional
Who owns the stock count. Defaults to `external` here.
enum
["external","momo"]
source_of_truthstringoptional
Who wins on product fields. Defaults to `api` here.
enum
["api","momo","platform"]
allow_backorderbooleanoptional
Sell past zero.
reservation_ttl_hoursintegeroptional
How long a pending order holds stock.
minimum
1
maximum
720
low_stock_thresholdinteger | nulloptional
Raise `stock.low` at or below this count.
minimum
0
Complete request schema
{
    "type": "object",
    "properties": {
        "name": {
            "type": "string",
            "maxLength": 120,
            "description": "Shop name."
        },
        "description": {
            "type": "string",
            "maxLength": 2000,
            "description": "What this shop sells."
        },
        "vertical": {
            "type": "string",
            "maxLength": 40,
            "description": "Commerce vertical, defaults to `commerce`."
        },
        "default_currency": {
            "type": "string",
            "minLength": 3,
            "maxLength": 3,
            "description": "Currency for products that do not name one."
        },
        "sku_prefix": {
            "type": "string",
            "maxLength": 8,
            "description": "Prefix for codes the shop issues itself. Accepted on create only."
        },
        "stock_policy": {
            "type": "string",
            "enum": [
                "external",
                "momo"
            ],
            "description": "Who owns the stock count. Defaults to `external` here."
        },
        "source_of_truth": {
            "type": "string",
            "enum": [
                "api",
                "momo",
                "platform"
            ],
            "description": "Who wins on product fields. Defaults to `api` here."
        },
        "allow_backorder": {
            "type": "boolean",
            "description": "Sell past zero."
        },
        "reservation_ttl_hours": {
            "type": "integer",
            "minimum": 1,
            "maximum": 720,
            "description": "How long a pending order holds stock."
        },
        "low_stock_threshold": {
            "type": [
                "integer",
                "null"
            ],
            "minimum": 0,
            "description": "Raise `stock.low` at or below this count."
        }
    },
    "required": [
        "name"
    ]
}

Responses

201The created catalogue.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
The catalogue record.
Show child properties
idintegerrequired
Catalogue id. Use it in every /catalogues/{catalogue} path.
namestringrequired
Shop name as customers see it.
descriptionstring | nulloptional
Optional shop description.
verticalstring | nulloptional
Meta commerce vertical, e.g. "commerce".
default_currencystring | nulloptional
ISO 4217 currency new products default to.
sku_prefixstring | nulloptional
The prefix on codes this shop issues itself, such as `AMY` in `AMY-00042`. Fixed once the shop exists.
stock_policystringoptional
`external`: your system owns the stock count, we mirror it and tell you what sold. `momo`: we keep the count, and orders reserve and commit against it. Shops created through this API default to `external`.
enum
["external","momo"]
source_of_truthstringoptional
Who wins on product fields when a selling platform has drifted from us.
enum
["api","momo","platform"]
allow_backorderbooleanoptional
Sell past zero. When false, a count of zero sets availability to `out of stock`.
reservation_ttl_hoursintegeroptional
How long a pending order holds stock before it goes back on the shelf.
low_stock_thresholdinteger | nulloptional
Raise `stock.low` at or below this count. Null means never.
meta_catalogue_idstring | nulloptional
Meta catalogue id when the shop is connected; null keeps every product local.
is_connected_to_wababooleanoptional
True once the shop is bound to a WhatsApp Business Account.
is_catalogue_visiblebooleanoptional
Whether customers can browse the catalogue in the chat.
is_cart_enabledbooleanoptional
Whether customers can build a cart and submit an order.
channelsarray<object>optional
Where this shop is published. Empty is normal for a shop that sells only through the assistant or the phone menus.
Show child properties
platformstringrequired
Which platform this presence is on.
enum
["whatsapp","storefront","facebook","instagram","tiktok"]
labelstringrequired
What the merchant named this connection.
external_catalogue_idstring | nulloptional
The platform's own id for the catalogue.
external_account_idstring | nulloptional
The account it is bound to — a WABA id for WhatsApp.
public_slugstring | nulloptional
Storefront address, when this is a hosted storefront.
is_connectedbooleanrequired
Whether customers can currently see it.
last_synced_atstring | nulloptional
When products were last pushed to this platform.
format
date-time
products_countinteger | nulloptional
Number of products in the shop.
orders_countinteger | nulloptional
Number of orders received by the shop.
last_synced_atstring | nulloptional
When the shop last synced to Meta.
format
date-time
created_atstring | nulloptional
ISO 8601 creation timestamp.
format
date-time
updated_atstring | nulloptional
ISO 8601 update timestamp.
format
date-time
{
    "status": "success",
    "data": {
        "id": 42,
        "name": "Mango Electronics",
        "description": null,
        "vertical": "commerce",
        "default_currency": "TZS",
        "sku_prefix": "MNG",
        "stock_policy": "external",
        "source_of_truth": "api",
        "allow_backorder": false,
        "reservation_ttl_hours": 48,
        "low_stock_threshold": null,
        "meta_catalogue_id": null,
        "is_connected_to_waba": false,
        "is_catalogue_visible": false,
        "is_cart_enabled": true,
        "channels": [
            {
                "platform": "whatsapp",
                "label": "Mango Electronics",
                "external_catalogue_id": "1122334455",
                "external_account_id": "998877",
                "public_slug": null,
                "is_connected": true,
                "last_synced_at": "2026-09-11T02:00:41+00:00"
            }
        ],
        "products_count": 1994,
        "orders_count": 12,
        "last_synced_at": "2026-09-11T02:00:41+00:00",
        "created_at": "2026-09-01T08:15:00+00:00",
        "updated_at": "2026-09-11T02:00:41+00:00"
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}
422The payload failed validation. `errors` maps each rejected field to its messages.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Validation failed.",
    "errors": {
        "recipient": [
            "Provide recipient or recipients."
        ]
    }
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Catalogue

Read one catalogue

GET/api/v3/catalogues/{catalogue}

One shop with its product and order counts.

AuthenticationTenant API token

Path parameters

catalogueintegerrequired
Catalogue (shop) id, as returned by `GET /api/v3/catalogues`.

Example: 1

Responses

200The catalogue.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
The catalogue record.
Show child properties
idintegerrequired
Catalogue id. Use it in every /catalogues/{catalogue} path.
namestringrequired
Shop name as customers see it.
descriptionstring | nulloptional
Optional shop description.
verticalstring | nulloptional
Meta commerce vertical, e.g. "commerce".
default_currencystring | nulloptional
ISO 4217 currency new products default to.
sku_prefixstring | nulloptional
The prefix on codes this shop issues itself, such as `AMY` in `AMY-00042`. Fixed once the shop exists.
stock_policystringoptional
`external`: your system owns the stock count, we mirror it and tell you what sold. `momo`: we keep the count, and orders reserve and commit against it. Shops created through this API default to `external`.
enum
["external","momo"]
source_of_truthstringoptional
Who wins on product fields when a selling platform has drifted from us.
enum
["api","momo","platform"]
allow_backorderbooleanoptional
Sell past zero. When false, a count of zero sets availability to `out of stock`.
reservation_ttl_hoursintegeroptional
How long a pending order holds stock before it goes back on the shelf.
low_stock_thresholdinteger | nulloptional
Raise `stock.low` at or below this count. Null means never.
meta_catalogue_idstring | nulloptional
Meta catalogue id when the shop is connected; null keeps every product local.
is_connected_to_wababooleanoptional
True once the shop is bound to a WhatsApp Business Account.
is_catalogue_visiblebooleanoptional
Whether customers can browse the catalogue in the chat.
is_cart_enabledbooleanoptional
Whether customers can build a cart and submit an order.
channelsarray<object>optional
Where this shop is published. Empty is normal for a shop that sells only through the assistant or the phone menus.
Show child properties
platformstringrequired
Which platform this presence is on.
enum
["whatsapp","storefront","facebook","instagram","tiktok"]
labelstringrequired
What the merchant named this connection.
external_catalogue_idstring | nulloptional
The platform's own id for the catalogue.
external_account_idstring | nulloptional
The account it is bound to — a WABA id for WhatsApp.
public_slugstring | nulloptional
Storefront address, when this is a hosted storefront.
is_connectedbooleanrequired
Whether customers can currently see it.
last_synced_atstring | nulloptional
When products were last pushed to this platform.
format
date-time
products_countinteger | nulloptional
Number of products in the shop.
orders_countinteger | nulloptional
Number of orders received by the shop.
last_synced_atstring | nulloptional
When the shop last synced to Meta.
format
date-time
created_atstring | nulloptional
ISO 8601 creation timestamp.
format
date-time
updated_atstring | nulloptional
ISO 8601 update timestamp.
format
date-time
{
    "status": "success",
    "data": {
        "id": 1,
        "name": "Acme Duka",
        "description": null,
        "vertical": "commerce",
        "default_currency": "TZS",
        "meta_catalogue_id": null,
        "is_connected_to_waba": false,
        "is_catalogue_visible": false,
        "is_cart_enabled": true,
        "products_count": 1,
        "orders_count": 1,
        "last_synced_at": null,
        "created_at": "2026-09-04T19:19:55+00:00",
        "updated_at": "2026-09-04T19:19:55+00:00"
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}
404No such record for this tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Catalogue

Change a catalogue

PATCH/api/v3/catalogues/{catalogue}

Change a shop's name, currency or stock policy. sku_prefix is ignored: it is stamped into every code the shop has already issued.

AuthenticationTenant API token

Path parameters

catalogueintegerrequired
Catalogue (shop) id, as returned by `GET /api/v3/catalogues`.

Example: 1

Request body

application/json · required

namestringoptional
Shop name.
maxLength
120
descriptionstringoptional
What this shop sells.
maxLength
2000
verticalstringoptional
Commerce vertical, defaults to `commerce`.
maxLength
40
default_currencystringoptional
Currency for products that do not name one.
minLength
3
maxLength
3
sku_prefixstringoptional
Prefix for codes the shop issues itself. Accepted on create only.
maxLength
8
stock_policystringoptional
Who owns the stock count. Defaults to `external` here.
enum
["external","momo"]
source_of_truthstringoptional
Who wins on product fields. Defaults to `api` here.
enum
["api","momo","platform"]
allow_backorderbooleanoptional
Sell past zero.
reservation_ttl_hoursintegeroptional
How long a pending order holds stock.
minimum
1
maximum
720
low_stock_thresholdinteger | nulloptional
Raise `stock.low` at or below this count.
minimum
0
Complete request schema
{
    "type": "object",
    "properties": {
        "name": {
            "type": "string",
            "maxLength": 120,
            "description": "Shop name."
        },
        "description": {
            "type": "string",
            "maxLength": 2000,
            "description": "What this shop sells."
        },
        "vertical": {
            "type": "string",
            "maxLength": 40,
            "description": "Commerce vertical, defaults to `commerce`."
        },
        "default_currency": {
            "type": "string",
            "minLength": 3,
            "maxLength": 3,
            "description": "Currency for products that do not name one."
        },
        "sku_prefix": {
            "type": "string",
            "maxLength": 8,
            "description": "Prefix for codes the shop issues itself. Accepted on create only."
        },
        "stock_policy": {
            "type": "string",
            "enum": [
                "external",
                "momo"
            ],
            "description": "Who owns the stock count. Defaults to `external` here."
        },
        "source_of_truth": {
            "type": "string",
            "enum": [
                "api",
                "momo",
                "platform"
            ],
            "description": "Who wins on product fields. Defaults to `api` here."
        },
        "allow_backorder": {
            "type": "boolean",
            "description": "Sell past zero."
        },
        "reservation_ttl_hours": {
            "type": "integer",
            "minimum": 1,
            "maximum": 720,
            "description": "How long a pending order holds stock."
        },
        "low_stock_threshold": {
            "type": [
                "integer",
                "null"
            ],
            "minimum": 0,
            "description": "Raise `stock.low` at or below this count."
        }
    }
}

Responses

200The updated catalogue.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
The catalogue record.
Show child properties
idintegerrequired
Catalogue id. Use it in every /catalogues/{catalogue} path.
namestringrequired
Shop name as customers see it.
descriptionstring | nulloptional
Optional shop description.
verticalstring | nulloptional
Meta commerce vertical, e.g. "commerce".
default_currencystring | nulloptional
ISO 4217 currency new products default to.
sku_prefixstring | nulloptional
The prefix on codes this shop issues itself, such as `AMY` in `AMY-00042`. Fixed once the shop exists.
stock_policystringoptional
`external`: your system owns the stock count, we mirror it and tell you what sold. `momo`: we keep the count, and orders reserve and commit against it. Shops created through this API default to `external`.
enum
["external","momo"]
source_of_truthstringoptional
Who wins on product fields when a selling platform has drifted from us.
enum
["api","momo","platform"]
allow_backorderbooleanoptional
Sell past zero. When false, a count of zero sets availability to `out of stock`.
reservation_ttl_hoursintegeroptional
How long a pending order holds stock before it goes back on the shelf.
low_stock_thresholdinteger | nulloptional
Raise `stock.low` at or below this count. Null means never.
meta_catalogue_idstring | nulloptional
Meta catalogue id when the shop is connected; null keeps every product local.
is_connected_to_wababooleanoptional
True once the shop is bound to a WhatsApp Business Account.
is_catalogue_visiblebooleanoptional
Whether customers can browse the catalogue in the chat.
is_cart_enabledbooleanoptional
Whether customers can build a cart and submit an order.
channelsarray<object>optional
Where this shop is published. Empty is normal for a shop that sells only through the assistant or the phone menus.
Show child properties
platformstringrequired
Which platform this presence is on.
enum
["whatsapp","storefront","facebook","instagram","tiktok"]
labelstringrequired
What the merchant named this connection.
external_catalogue_idstring | nulloptional
The platform's own id for the catalogue.
external_account_idstring | nulloptional
The account it is bound to — a WABA id for WhatsApp.
public_slugstring | nulloptional
Storefront address, when this is a hosted storefront.
is_connectedbooleanrequired
Whether customers can currently see it.
last_synced_atstring | nulloptional
When products were last pushed to this platform.
format
date-time
products_countinteger | nulloptional
Number of products in the shop.
orders_countinteger | nulloptional
Number of orders received by the shop.
last_synced_atstring | nulloptional
When the shop last synced to Meta.
format
date-time
created_atstring | nulloptional
ISO 8601 creation timestamp.
format
date-time
updated_atstring | nulloptional
ISO 8601 update timestamp.
format
date-time
{
    "status": "success",
    "data": {
        "id": 42,
        "name": "Mango Electronics",
        "description": null,
        "vertical": "commerce",
        "default_currency": "TZS",
        "sku_prefix": "MNG",
        "stock_policy": "external",
        "source_of_truth": "api",
        "allow_backorder": false,
        "reservation_ttl_hours": 48,
        "low_stock_threshold": null,
        "meta_catalogue_id": null,
        "is_connected_to_waba": false,
        "is_catalogue_visible": false,
        "is_cart_enabled": true,
        "channels": [
            {
                "platform": "whatsapp",
                "label": "Mango Electronics",
                "external_catalogue_id": "1122334455",
                "external_account_id": "998877",
                "public_slug": null,
                "is_connected": true,
                "last_synced_at": "2026-09-11T02:00:41+00:00"
            }
        ],
        "products_count": 1994,
        "orders_count": 12,
        "last_synced_at": "2026-09-11T02:00:41+00:00",
        "created_at": "2026-09-01T08:15:00+00:00",
        "updated_at": "2026-09-11T02:00:41+00:00"
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}
422The payload failed validation. `errors` maps each rejected field to its messages.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Validation failed.",
    "errors": {
        "recipient": [
            "Provide recipient or recipients."
        ]
    }
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}
404No such record for this tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}

API REFERENCE / Catalogue

List products in a catalogue

GET/api/v3/catalogues/{catalogue}/products

Products in the shop, ordered by name. Filter with search (name or SKU) and availability.

AuthenticationTenant API token

Path parameters

catalogueintegerrequired
Catalogue (shop) id, as returned by `GET /api/v3/catalogues`.

Example: 1

Query parameters

searchstringoptional
Match products whose name or `retailer_id` contains this text.

Example: kanga

availabilitystringoptional
Only products in this stock state.
enum
["in stock","out of stock","preorder","available for order","discontinued"]

Example: in stock

limitintegeroptional
Rows per page, 1–100. Defaults to 20 (25 for catalogue endpoints). Values above 100 are clamped.
minimum
1
maximum
100
default
20

Example: 20

per_pageintegeroptional
Alias of `limit`, for clients that already speak Laravel pagination. `limit` wins if both are sent.
minimum
1
maximum
100

Example: 25

pageintegeroptional
Page number, 1-based. Read `data.pagination.has_more_pages` to know when to stop.
minimum
1
default
1

Example: 1

Responses

200A page of products.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
A page of rows and its page state.
Show child properties
itemsarray<object>required
The products on this page.
Show child properties
idintegerrequired
Product id.
catalogue_idintegeroptional
Catalogue this product belongs to.
skustringoptional
Your product code, and the identity this API addresses a product by. The shop issues one (`AMY-00042`) when you send none, and it is frozen once the product is live on any platform.
retailer_idstringrequired
The older name for `sku`, kept in step with it. Prefer `sku` in new code.
meta_product_idstring | nulloptional
Meta product id once mirrored; null for a local-only product.
namestringrequired
Product name (max 100 characters).
descriptionstring | nulloptional
Long description (max 5000 characters).
urlstring | nulloptional
Link to the product page on your own site.
priceintegerrequired
Price in the minor unit of `currency`.
currencystringrequired
ISO 4217 currency code.
sale_priceinteger | nulloptional
Optional sale price in the minor unit.
image_urlstring | nulloptional
Publicly reachable product image. Meta fetches it directly.
availabilitystringoptional
Stock state.
enum
["in stock","out of stock","preorder","available for order","discontinued"]
conditionstringoptional
Product condition.
enum
["new","refurbished","used"]
brandstring | nulloptional
Brand name.
categorystring | nulloptional
Category label.
product_typestring | nulloptional
Your own product taxonomy string.
inventoryinteger | nulloptional
Stock count.
visibilitystringoptional
Whether customers can see it.
enum
["staging","published"]
review_statusstring | nulloptional
Meta review outcome: pending, approved or rejected.
sync_statusstring | nulloptional
A roll-up of `listings`. A product on a shop with no platforms is `synced`, because there is nothing to sync.
enum
["pending","syncing","synced","failed","blocked","drifted",null]
listingsarray<object>optional
One row per platform this shop sells on.
Show child properties
platformstringrequired
Which platform this listing is on.
statestringrequired
`blocked` is not a failure: nothing was attempted because the product is missing something this platform requires. `failed` means the platform refused a push and it will be retried. `drifted` means the platform's copy no longer matches ours — someone edited it there — and the shop's `source_of_truth` decides which copy wins.
enum
["pending","syncing","synced","failed","blocked","drifted"]
external_idstring | nulloptional
The platform's own product id once it is live.
problemstring | nulloptional
Why this platform will not show the product yet.
review_statusstring | nulloptional
The platform's review verdict, where it has one.
last_synced_atstring | nulloptional
When this listing last reached the platform.
format
date-time
driftobject | nulloptional
Field by field, what differs, while the listing is `drifted`. Cleared once resolved.
additionalProperties
{"type":"object","properties":{"ours":{"description":"The value we hold."},"theirs":{"description":"The value the platform holds."}}}
drift_detected_atstring | nulloptional
When the difference was last seen.
format
date-time
last_synced_atstring | nulloptional
When the product last synced to Meta.
format
date-time
created_atstring | nulloptional
ISO 8601 creation timestamp.
format
date-time
updated_atstring | nulloptional
ISO 8601 update timestamp.
format
date-time
paginationobjectrequired
Page state for this list: where you are and whether more pages follow.
Show child properties
current_pageintegerrequired
1-based current page index.
per_pageintegerrequired
Number of items per page.
last_pageintegerrequired
1-based index of the last page.
totalintegerrequired
Total number of items across all pages.
has_more_pagesbooleanrequired
True if more pages exist after the current page.
{
    "status": "success",
    "data": {
        "items": [
            {
                "id": 1,
                "catalogue_id": 1,
                "retailer_id": "ACME-001",
                "meta_product_id": null,
                "name": "Kanga Print",
                "description": null,
                "url": null,
                "price": 25000,
                "currency": "TZS",
                "sale_price": null,
                "image_url": "https://cdn.acme.co.tz/kanga.jpg",
                "availability": "in stock",
                "condition": "new",
                "brand": null,
                "category": null,
                "product_type": null,
                "inventory": null,
                "visibility": "published",
                "review_status": null,
                "last_synced_at": null,
                "created_at": "2026-09-04T19:19:55+00:00",
                "updated_at": "2026-09-04T19:19:55+00:00"
            }
        ],
        "pagination": {
            "current_page": 1,
            "per_page": 25,
            "last_page": 1,
            "total": 1,
            "has_more_pages": false
        }
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}
404No such record for this tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Catalogue

Add a product to a catalogue

POST/api/v3/catalogues/{catalogue}/products

Add one product. sku is optional — the shop issues AMY-00042 style codes when you send none. The product is stored first and pushed to every platform the shop is on afterwards, so a 201 means it is saved, and listings says where it is live.

AuthenticationTenant API token

Path parameters

catalogueintegerrequired
Catalogue (shop) id, as returned by `GET /api/v3/catalogues`.

Example: 1

Request body

application/json · required

skustringoptional
Your product code. Left out, the shop issues one.
maxLength
100
retailer_idstringoptional
The older name for `sku`.
maxLength
100
namestringrequired
Product name as customers see it.
maxLength
200
priceintegerrequired
Price in the minor unit of `currency` — 25000 is TZS 250.00 for a 2-decimal currency.
minimum
0
currencystringoptional
ISO 4217 code. Left out, the shop's own currency is used.
minLength
3
maxLength
3
descriptionstringoptional
Long description.
maxLength
9000
sale_priceintegeroptional
Optional sale price in the minor unit. Ignored when it is higher than `price`.
minimum
0
image_urlstringoptional
Publicly reachable image. Platforms fetch it themselves, so it cannot sit behind a login.
format
uri
maxLength
2048
additional_image_urlsarray<string>optional
Up to ten more images.
maxItems
10
items.format
uri
urlstringoptional
The product page on your own site.
format
uri
maxLength
2048
availabilitystringoptional
Left out, it is derived from `inventory`: a count of zero means `out of stock` unless the shop allows backorders. Sent explicitly, what you send wins.
enum
["in stock","out of stock","preorder","available for order","discontinued"]
conditionstringoptional
Item condition.
enum
["new","refurbished","used"]
brandstringoptional
Brand name. Becomes a brand record on first use.
maxLength
255
categorystringoptional
Category name. Becomes a category record on first use.
maxLength
255
product_typestringoptional
Your own taxonomy path.
maxLength
750
inventoryintegeroptional
Units on hand.
minimum
0
visibilitystringoptional
Whether customers may see it.
enum
["staging","published"]
custom_labelsarray<string>optional
Up to five free labels for your own segmentation.
maxItems
5
Complete request schema
{
    "type": "object",
    "required": [
        "name",
        "price"
    ],
    "description": "A product. Only a name and a price are required here; an image, a non-zero price and the rest are what individual PLATFORMS require, and a product missing them is stored and reported as `blocked` on that platform rather than refused.",
    "properties": {
        "sku": {
            "type": "string",
            "maxLength": 100,
            "description": "Your product code. Left out, the shop issues one."
        },
        "retailer_id": {
            "type": "string",
            "maxLength": 100,
            "description": "The older name for `sku`."
        },
        "name": {
            "type": "string",
            "maxLength": 200,
            "description": "Product name as customers see it."
        },
        "price": {
            "type": "integer",
            "minimum": 0,
            "description": "Price in the minor unit of `currency` \u2014 25000 is TZS 250.00 for a 2-decimal currency."
        },
        "currency": {
            "type": "string",
            "minLength": 3,
            "maxLength": 3,
            "description": "ISO 4217 code. Left out, the shop's own currency is used."
        },
        "description": {
            "type": "string",
            "maxLength": 9000,
            "description": "Long description."
        },
        "sale_price": {
            "type": "integer",
            "minimum": 0,
            "description": "Optional sale price in the minor unit. Ignored when it is higher than `price`."
        },
        "image_url": {
            "type": "string",
            "format": "uri",
            "maxLength": 2048,
            "description": "Publicly reachable image. Platforms fetch it themselves, so it cannot sit behind a login."
        },
        "additional_image_urls": {
            "type": "array",
            "maxItems": 10,
            "items": {
                "type": "string",
                "format": "uri"
            },
            "description": "Up to ten more images."
        },
        "url": {
            "type": "string",
            "format": "uri",
            "maxLength": 2048,
            "description": "The product page on your own site."
        },
        "availability": {
            "type": "string",
            "enum": [
                "in stock",
                "out of stock",
                "preorder",
                "available for order",
                "discontinued"
            ],
            "description": "Left out, it is derived from `inventory`: a count of zero means `out of stock` unless the shop allows backorders. Sent explicitly, what you send wins."
        },
        "condition": {
            "type": "string",
            "enum": [
                "new",
                "refurbished",
                "used"
            ],
            "description": "Item condition."
        },
        "brand": {
            "type": "string",
            "maxLength": 255,
            "description": "Brand name. Becomes a brand record on first use."
        },
        "category": {
            "type": "string",
            "maxLength": 255,
            "description": "Category name. Becomes a category record on first use."
        },
        "product_type": {
            "type": "string",
            "maxLength": 750,
            "description": "Your own taxonomy path."
        },
        "inventory": {
            "type": "integer",
            "minimum": 0,
            "description": "Units on hand."
        },
        "visibility": {
            "type": "string",
            "enum": [
                "staging",
                "published"
            ],
            "description": "Whether customers may see it."
        },
        "custom_labels": {
            "type": "array",
            "maxItems": 5,
            "items": {
                "type": "string"
            },
            "description": "Up to five free labels for your own segmentation."
        }
    }
}

Responses

201The created product.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
The product record.
Show child properties
idintegerrequired
Product id.
catalogue_idintegeroptional
Catalogue this product belongs to.
skustringoptional
Your product code, and the identity this API addresses a product by. The shop issues one (`AMY-00042`) when you send none, and it is frozen once the product is live on any platform.
retailer_idstringrequired
The older name for `sku`, kept in step with it. Prefer `sku` in new code.
meta_product_idstring | nulloptional
Meta product id once mirrored; null for a local-only product.
namestringrequired
Product name (max 100 characters).
descriptionstring | nulloptional
Long description (max 5000 characters).
urlstring | nulloptional
Link to the product page on your own site.
priceintegerrequired
Price in the minor unit of `currency`.
currencystringrequired
ISO 4217 currency code.
sale_priceinteger | nulloptional
Optional sale price in the minor unit.
image_urlstring | nulloptional
Publicly reachable product image. Meta fetches it directly.
availabilitystringoptional
Stock state.
enum
["in stock","out of stock","preorder","available for order","discontinued"]
conditionstringoptional
Product condition.
enum
["new","refurbished","used"]
brandstring | nulloptional
Brand name.
categorystring | nulloptional
Category label.
product_typestring | nulloptional
Your own product taxonomy string.
inventoryinteger | nulloptional
Stock count.
visibilitystringoptional
Whether customers can see it.
enum
["staging","published"]
review_statusstring | nulloptional
Meta review outcome: pending, approved or rejected.
sync_statusstring | nulloptional
A roll-up of `listings`. A product on a shop with no platforms is `synced`, because there is nothing to sync.
enum
["pending","syncing","synced","failed","blocked","drifted",null]
listingsarray<object>optional
One row per platform this shop sells on.
Show child properties
platformstringrequired
Which platform this listing is on.
statestringrequired
`blocked` is not a failure: nothing was attempted because the product is missing something this platform requires. `failed` means the platform refused a push and it will be retried. `drifted` means the platform's copy no longer matches ours — someone edited it there — and the shop's `source_of_truth` decides which copy wins.
enum
["pending","syncing","synced","failed","blocked","drifted"]
external_idstring | nulloptional
The platform's own product id once it is live.
problemstring | nulloptional
Why this platform will not show the product yet.
review_statusstring | nulloptional
The platform's review verdict, where it has one.
last_synced_atstring | nulloptional
When this listing last reached the platform.
format
date-time
driftobject | nulloptional
Field by field, what differs, while the listing is `drifted`. Cleared once resolved.
additionalProperties
{"type":"object","properties":{"ours":{"description":"The value we hold."},"theirs":{"description":"The value the platform holds."}}}
drift_detected_atstring | nulloptional
When the difference was last seen.
format
date-time
last_synced_atstring | nulloptional
When the product last synced to Meta.
format
date-time
created_atstring | nulloptional
ISO 8601 creation timestamp.
format
date-time
updated_atstring | nulloptional
ISO 8601 update timestamp.
format
date-time
{
    "status": "success",
    "data": {
        "id": 2,
        "catalogue_id": 1,
        "retailer_id": "ACME-002",
        "meta_product_id": null,
        "name": "Kitenge 6 yards",
        "description": "Wax print, 6 yards.",
        "url": null,
        "price": 45000,
        "currency": "TZS",
        "sale_price": null,
        "image_url": "https://cdn.acme.co.tz/kitenge.jpg",
        "availability": "in stock",
        "condition": "new",
        "brand": null,
        "category": null,
        "product_type": null,
        "inventory": null,
        "visibility": "published",
        "review_status": null,
        "last_synced_at": null,
        "created_at": "2026-09-04T19:19:55+00:00",
        "updated_at": "2026-09-04T19:19:55+00:00"
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}
404No such record for this tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}
422Validation failed, or the `retailer_id` is already used in this catalogue.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Validation failed.",
    "errors": {
        "retailer_id": [
            "The retailer id field is required."
        ],
        "price": [
            "The price field is required."
        ],
        "currency": [
            "The currency field is required."
        ],
        "image_url": [
            "The image url field is required."
        ]
    }
}
Required fields missing
{
    "status": "error",
    "message": "Validation failed.",
    "errors": {
        "retailer_id": [
            "The retailer id field is required."
        ],
        "price": [
            "The price field is required."
        ],
        "currency": [
            "The currency field is required."
        ],
        "image_url": [
            "The image url field is required."
        ]
    }
}
Duplicate retailer_id
{
    "status": "error",
    "message": "Retailer ID already exists in this catalogue.",
    "errors": {
        "retailer_id": [
            "This retailer_id is already used by another product in this catalogue."
        ]
    }
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}
502WhatsApp/Meta refused the call. The message repeats what they said; the record was not changed.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "(#100) Invalid parameter: product image_url is not reachable."
}

API REFERENCE / Catalogue

Sync products in bulk

POST/api/v3/catalogues/{catalogue}/products/batch

The endpoint a store integration lives on. Send up to 5,000 products; every field in the product schema is stored, not just the identity ones. The call returns 202 with a sync id and the work happens in the background, because pushing a catalogue to a platform takes longer than an HTTP request should.

Read the outcome from GET /catalogues/{catalogue}/syncs/{sync}: it names every row that was refused and every product a platform will not show, with the reason for each.

Send an Idempotency-Key header on scheduled runs. A repeat of the same key returns the run that already owns it with replayed: true and imports nothing, so a cron that times out is safe to retry.

AuthenticationTenant API token

Path parameters

catalogueintegerrequired
Catalogue (shop) id, as returned by `GET /api/v3/catalogues`.

Example: 1

Header parameters

Idempotency-Keystringoptional
Your own name for this run, for example `nightly-2026-09-11`. Repeating it never imports twice.
maxLength
190

Example: nightly-2026-09-11

Request body

application/json · required

modestringoptional
`upsert` leaves products the payload does not mention alone. `replace` says this payload IS the catalogue and retires everything missing from it — use it only when you are sending your whole product list.
enum
["upsert","replace"]
default
upsert
productsarray<object>required
The products. A row that cannot be stored is reported in the sync report as one rejected row; it does not refuse the rest of the file.
minItems
1
maxItems
5000
Show child properties
skustringoptional
Your product code. Left out, the shop issues one.
maxLength
100
retailer_idstringoptional
The older name for `sku`.
maxLength
100
namestringrequired
Product name as customers see it.
maxLength
200
priceintegerrequired
Price in the minor unit of `currency` — 25000 is TZS 250.00 for a 2-decimal currency.
minimum
0
currencystringoptional
ISO 4217 code. Left out, the shop's own currency is used.
minLength
3
maxLength
3
descriptionstringoptional
Long description.
maxLength
9000
sale_priceintegeroptional
Optional sale price in the minor unit. Ignored when it is higher than `price`.
minimum
0
image_urlstringoptional
Publicly reachable image. Platforms fetch it themselves, so it cannot sit behind a login.
format
uri
maxLength
2048
additional_image_urlsarray<string>optional
Up to ten more images.
maxItems
10
items.format
uri
urlstringoptional
The product page on your own site.
format
uri
maxLength
2048
availabilitystringoptional
Left out, it is derived from `inventory`: a count of zero means `out of stock` unless the shop allows backorders. Sent explicitly, what you send wins.
enum
["in stock","out of stock","preorder","available for order","discontinued"]
conditionstringoptional
Item condition.
enum
["new","refurbished","used"]
brandstringoptional
Brand name. Becomes a brand record on first use.
maxLength
255
categorystringoptional
Category name. Becomes a category record on first use.
maxLength
255
product_typestringoptional
Your own taxonomy path.
maxLength
750
inventoryintegeroptional
Units on hand.
minimum
0
visibilitystringoptional
Whether customers may see it.
enum
["staging","published"]
custom_labelsarray<string>optional
Up to five free labels for your own segmentation.
maxItems
5
Complete request schema
{
    "type": "object",
    "required": [
        "products"
    ],
    "properties": {
        "mode": {
            "type": "string",
            "enum": [
                "upsert",
                "replace"
            ],
            "default": "upsert",
            "description": "`upsert` leaves products the payload does not mention alone. `replace` says this payload IS the catalogue and retires everything missing from it \u2014 use it only when you are sending your whole product list."
        },
        "products": {
            "type": "array",
            "minItems": 1,
            "maxItems": 5000,
            "items": {
                "type": "object",
                "required": [
                    "name",
                    "price"
                ],
                "description": "A product. Only a name and a price are required here; an image, a non-zero price and the rest are what individual PLATFORMS require, and a product missing them is stored and reported as `blocked` on that platform rather than refused.",
                "properties": {
                    "sku": {
                        "type": "string",
                        "maxLength": 100,
                        "description": "Your product code. Left out, the shop issues one."
                    },
                    "retailer_id": {
                        "type": "string",
                        "maxLength": 100,
                        "description": "The older name for `sku`."
                    },
                    "name": {
                        "type": "string",
                        "maxLength": 200,
                        "description": "Product name as customers see it."
                    },
                    "price": {
                        "type": "integer",
                        "minimum": 0,
                        "description": "Price in the minor unit of `currency` \u2014 25000 is TZS 250.00 for a 2-decimal currency."
                    },
                    "currency": {
                        "type": "string",
                        "minLength": 3,
                        "maxLength": 3,
                        "description": "ISO 4217 code. Left out, the shop's own currency is used."
                    },
                    "description": {
                        "type": "string",
                        "maxLength": 9000,
                        "description": "Long description."
                    },
                    "sale_price": {
                        "type": "integer",
                        "minimum": 0,
                        "description": "Optional sale price in the minor unit. Ignored when it is higher than `price`."
                    },
                    "image_url": {
                        "type": "string",
                        "format": "uri",
                        "maxLength": 2048,
                        "description": "Publicly reachable image. Platforms fetch it themselves, so it cannot sit behind a login."
                    },
                    "additional_image_urls": {
                        "type": "array",
                        "maxItems": 10,
                        "items": {
                            "type": "string",
                            "format": "uri"
                        },
                        "description": "Up to ten more images."
                    },
                    "url": {
                        "type": "string",
                        "format": "uri",
                        "maxLength": 2048,
                        "description": "The product page on your own site."
                    },
                    "availability": {
                        "type": "string",
                        "enum": [
                            "in stock",
                            "out of stock",
                            "preorder",
                            "available for order",
                            "discontinued"
                        ],
                        "description": "Left out, it is derived from `inventory`: a count of zero means `out of stock` unless the shop allows backorders. Sent explicitly, what you send wins."
                    },
                    "condition": {
                        "type": "string",
                        "enum": [
                            "new",
                            "refurbished",
                            "used"
                        ],
                        "description": "Item condition."
                    },
                    "brand": {
                        "type": "string",
                        "maxLength": 255,
                        "description": "Brand name. Becomes a brand record on first use."
                    },
                    "category": {
                        "type": "string",
                        "maxLength": 255,
                        "description": "Category name. Becomes a category record on first use."
                    },
                    "product_type": {
                        "type": "string",
                        "maxLength": 750,
                        "description": "Your own taxonomy path."
                    },
                    "inventory": {
                        "type": "integer",
                        "minimum": 0,
                        "description": "Units on hand."
                    },
                    "visibility": {
                        "type": "string",
                        "enum": [
                            "staging",
                            "published"
                        ],
                        "description": "Whether customers may see it."
                    },
                    "custom_labels": {
                        "type": "array",
                        "maxItems": 5,
                        "items": {
                            "type": "string"
                        },
                        "description": "Up to five free labels for your own segmentation."
                    }
                }
            },
            "description": "The products. A row that cannot be stored is reported in the sync report as one rejected row; it does not refuse the rest of the file."
        }
    }
}

Responses

202The sync was accepted. Poll it for the outcome.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
The sync that was accepted.
Show child properties
idintegerrequired
Sync id. Use it to read this report back.
catalogue_idintegerrequired
The catalogue that was written to.
sourcestringoptional
Where the rows came from.
enum
["api","feed","import","ui"]
modestringoptional
`upsert` leaves products the payload did not mention alone; `replace` retires them.
enum
["upsert","replace"]
statusstringrequired
Where the run has got to.
enum
["queued","running","completed","failed"]
idempotency_keystring | nulloptional
The key the caller sent, if any.
receivedintegerrequired
How many rows arrived.
createdintegeroptional
New products.
updatedintegeroptional
Products that changed.
unchangedintegeroptional
Products that were already identical — nothing was re-published for these.
rejectedintegeroptional
Rows that could not be stored. Each one is in `problems`.
retiredintegeroptional
Products taken off sale, in `replace` mode only.
platformsobjectoptional
Per platform, how many listings ended in each state — for example `{"whatsapp": {"synced": 1960, "blocked": 34}}`.
additionalProperties
{"type":"object","additionalProperties":{"type":"integer"}}
problemsarray<object>optional
Up to 200 problems, ingest refusals first.
Show child properties
skustringrequired
The product code, or the row number when the row had no code.
stagestringrequired
`ingest`, or a platform name such as `whatsapp`.
reasonstringrequired
What is wrong, in words a merchant can act on.
problems_truncatedbooleanoptional
True when there were more than 200 problems and the list was cut.
errorstring | nulloptional
Set only when the run itself failed.
started_atstring | nulloptional
When the run began.
format
date-time
finished_atstring | nulloptional
When it finished.
format
date-time
created_atstring | nulloptional
When it was accepted.
format
date-time
{
    "status": "success",
    "data": {
        "id": 812,
        "catalogue_id": 42,
        "source": "api",
        "mode": "upsert",
        "status": "queued",
        "idempotency_key": "nightly-2026-09-11",
        "received": 2000,
        "created": 0,
        "updated": 0,
        "unchanged": 0,
        "rejected": 0,
        "retired": 0,
        "platforms": {
            "whatsapp": {
                "pending": 0
            }
        },
        "problems": [],
        "problems_truncated": false,
        "error": null,
        "started_at": null,
        "finished_at": null,
        "created_at": "2026-09-11T02:00:04+00:00"
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}
422The payload failed validation. `errors` maps each rejected field to its messages.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Validation failed.",
    "errors": {
        "recipient": [
            "Provide recipient or recipients."
        ]
    }
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}
404No such record for this tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}

API REFERENCE / Catalogue

Read one product

GET/api/v3/catalogues/{catalogue}/products/{product}

One product from a catalogue. A product that exists but sits in a different catalogue answers 404.

AuthenticationTenant API token

Path parameters

catalogueintegerrequired
Catalogue (shop) id, as returned by `GET /api/v3/catalogues`.

Example: 1

productintegerrequired
Product id. Not the `retailer_id` — that is your own SKU.

Example: 1

Responses

200The product.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
The product record.
Show child properties
idintegerrequired
Product id.
catalogue_idintegeroptional
Catalogue this product belongs to.
skustringoptional
Your product code, and the identity this API addresses a product by. The shop issues one (`AMY-00042`) when you send none, and it is frozen once the product is live on any platform.
retailer_idstringrequired
The older name for `sku`, kept in step with it. Prefer `sku` in new code.
meta_product_idstring | nulloptional
Meta product id once mirrored; null for a local-only product.
namestringrequired
Product name (max 100 characters).
descriptionstring | nulloptional
Long description (max 5000 characters).
urlstring | nulloptional
Link to the product page on your own site.
priceintegerrequired
Price in the minor unit of `currency`.
currencystringrequired
ISO 4217 currency code.
sale_priceinteger | nulloptional
Optional sale price in the minor unit.
image_urlstring | nulloptional
Publicly reachable product image. Meta fetches it directly.
availabilitystringoptional
Stock state.
enum
["in stock","out of stock","preorder","available for order","discontinued"]
conditionstringoptional
Product condition.
enum
["new","refurbished","used"]
brandstring | nulloptional
Brand name.
categorystring | nulloptional
Category label.
product_typestring | nulloptional
Your own product taxonomy string.
inventoryinteger | nulloptional
Stock count.
visibilitystringoptional
Whether customers can see it.
enum
["staging","published"]
review_statusstring | nulloptional
Meta review outcome: pending, approved or rejected.
sync_statusstring | nulloptional
A roll-up of `listings`. A product on a shop with no platforms is `synced`, because there is nothing to sync.
enum
["pending","syncing","synced","failed","blocked","drifted",null]
listingsarray<object>optional
One row per platform this shop sells on.
Show child properties
platformstringrequired
Which platform this listing is on.
statestringrequired
`blocked` is not a failure: nothing was attempted because the product is missing something this platform requires. `failed` means the platform refused a push and it will be retried. `drifted` means the platform's copy no longer matches ours — someone edited it there — and the shop's `source_of_truth` decides which copy wins.
enum
["pending","syncing","synced","failed","blocked","drifted"]
external_idstring | nulloptional
The platform's own product id once it is live.
problemstring | nulloptional
Why this platform will not show the product yet.
review_statusstring | nulloptional
The platform's review verdict, where it has one.
last_synced_atstring | nulloptional
When this listing last reached the platform.
format
date-time
driftobject | nulloptional
Field by field, what differs, while the listing is `drifted`. Cleared once resolved.
additionalProperties
{"type":"object","properties":{"ours":{"description":"The value we hold."},"theirs":{"description":"The value the platform holds."}}}
drift_detected_atstring | nulloptional
When the difference was last seen.
format
date-time
last_synced_atstring | nulloptional
When the product last synced to Meta.
format
date-time
created_atstring | nulloptional
ISO 8601 creation timestamp.
format
date-time
updated_atstring | nulloptional
ISO 8601 update timestamp.
format
date-time
{
    "status": "success",
    "data": {
        "id": 1,
        "catalogue_id": 1,
        "retailer_id": "ACME-001",
        "meta_product_id": null,
        "name": "Kanga Print",
        "description": null,
        "url": null,
        "price": 25000,
        "currency": "TZS",
        "sale_price": null,
        "image_url": "https://cdn.acme.co.tz/kanga.jpg",
        "availability": "in stock",
        "condition": "new",
        "brand": null,
        "category": null,
        "product_type": null,
        "inventory": null,
        "visibility": "published",
        "review_status": null,
        "last_synced_at": null,
        "created_at": "2026-09-04T19:19:55+00:00",
        "updated_at": "2026-09-04T19:19:55+00:00"
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}
404No such record for this tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Catalogue

Update a product

PUT/api/v3/catalogues/{catalogue}/products/{product}

Change one product. Only the fields you send are touched, so sending {"inventory": 0} is a stock update and nothing else. The product code cannot be changed here — it is the identity platforms know the item by.

AuthenticationTenant API token

Path parameters

catalogueintegerrequired
Catalogue (shop) id, as returned by `GET /api/v3/catalogues`.

Example: 1

productintegerrequired
Product id. Not the `retailer_id` — that is your own SKU.

Example: 1

Request body

application/json · required

skustringoptional
Your product code. Left out, the shop issues one.
maxLength
100
retailer_idstringoptional
The older name for `sku`.
maxLength
100
namestringoptional
Product name as customers see it.
maxLength
200
priceintegeroptional
Price in the minor unit of `currency` — 25000 is TZS 250.00 for a 2-decimal currency.
minimum
0
currencystringoptional
ISO 4217 code. Left out, the shop's own currency is used.
minLength
3
maxLength
3
descriptionstringoptional
Long description.
maxLength
9000
sale_priceintegeroptional
Optional sale price in the minor unit. Ignored when it is higher than `price`.
minimum
0
image_urlstringoptional
Publicly reachable image. Platforms fetch it themselves, so it cannot sit behind a login.
format
uri
maxLength
2048
additional_image_urlsarray<string>optional
Up to ten more images.
maxItems
10
items.format
uri
urlstringoptional
The product page on your own site.
format
uri
maxLength
2048
availabilitystringoptional
Left out, it is derived from `inventory`: a count of zero means `out of stock` unless the shop allows backorders. Sent explicitly, what you send wins.
enum
["in stock","out of stock","preorder","available for order","discontinued"]
conditionstringoptional
Item condition.
enum
["new","refurbished","used"]
brandstringoptional
Brand name. Becomes a brand record on first use.
maxLength
255
categorystringoptional
Category name. Becomes a category record on first use.
maxLength
255
product_typestringoptional
Your own taxonomy path.
maxLength
750
inventoryintegeroptional
Units on hand.
minimum
0
visibilitystringoptional
Whether customers may see it.
enum
["staging","published"]
custom_labelsarray<string>optional
Up to five free labels for your own segmentation.
maxItems
5
Complete request schema
{
    "type": "object",
    "required": [],
    "description": "A product. Only a name and a price are required here; an image, a non-zero price and the rest are what individual PLATFORMS require, and a product missing them is stored and reported as `blocked` on that platform rather than refused.",
    "properties": {
        "sku": {
            "type": "string",
            "maxLength": 100,
            "description": "Your product code. Left out, the shop issues one."
        },
        "retailer_id": {
            "type": "string",
            "maxLength": 100,
            "description": "The older name for `sku`."
        },
        "name": {
            "type": "string",
            "maxLength": 200,
            "description": "Product name as customers see it."
        },
        "price": {
            "type": "integer",
            "minimum": 0,
            "description": "Price in the minor unit of `currency` \u2014 25000 is TZS 250.00 for a 2-decimal currency."
        },
        "currency": {
            "type": "string",
            "minLength": 3,
            "maxLength": 3,
            "description": "ISO 4217 code. Left out, the shop's own currency is used."
        },
        "description": {
            "type": "string",
            "maxLength": 9000,
            "description": "Long description."
        },
        "sale_price": {
            "type": "integer",
            "minimum": 0,
            "description": "Optional sale price in the minor unit. Ignored when it is higher than `price`."
        },
        "image_url": {
            "type": "string",
            "format": "uri",
            "maxLength": 2048,
            "description": "Publicly reachable image. Platforms fetch it themselves, so it cannot sit behind a login."
        },
        "additional_image_urls": {
            "type": "array",
            "maxItems": 10,
            "items": {
                "type": "string",
                "format": "uri"
            },
            "description": "Up to ten more images."
        },
        "url": {
            "type": "string",
            "format": "uri",
            "maxLength": 2048,
            "description": "The product page on your own site."
        },
        "availability": {
            "type": "string",
            "enum": [
                "in stock",
                "out of stock",
                "preorder",
                "available for order",
                "discontinued"
            ],
            "description": "Left out, it is derived from `inventory`: a count of zero means `out of stock` unless the shop allows backorders. Sent explicitly, what you send wins."
        },
        "condition": {
            "type": "string",
            "enum": [
                "new",
                "refurbished",
                "used"
            ],
            "description": "Item condition."
        },
        "brand": {
            "type": "string",
            "maxLength": 255,
            "description": "Brand name. Becomes a brand record on first use."
        },
        "category": {
            "type": "string",
            "maxLength": 255,
            "description": "Category name. Becomes a category record on first use."
        },
        "product_type": {
            "type": "string",
            "maxLength": 750,
            "description": "Your own taxonomy path."
        },
        "inventory": {
            "type": "integer",
            "minimum": 0,
            "description": "Units on hand."
        },
        "visibility": {
            "type": "string",
            "enum": [
                "staging",
                "published"
            ],
            "description": "Whether customers may see it."
        },
        "custom_labels": {
            "type": "array",
            "maxItems": 5,
            "items": {
                "type": "string"
            },
            "description": "Up to five free labels for your own segmentation."
        }
    }
}

Responses

200The updated product.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
The product record.
Show child properties
idintegerrequired
Product id.
catalogue_idintegeroptional
Catalogue this product belongs to.
skustringoptional
Your product code, and the identity this API addresses a product by. The shop issues one (`AMY-00042`) when you send none, and it is frozen once the product is live on any platform.
retailer_idstringrequired
The older name for `sku`, kept in step with it. Prefer `sku` in new code.
meta_product_idstring | nulloptional
Meta product id once mirrored; null for a local-only product.
namestringrequired
Product name (max 100 characters).
descriptionstring | nulloptional
Long description (max 5000 characters).
urlstring | nulloptional
Link to the product page on your own site.
priceintegerrequired
Price in the minor unit of `currency`.
currencystringrequired
ISO 4217 currency code.
sale_priceinteger | nulloptional
Optional sale price in the minor unit.
image_urlstring | nulloptional
Publicly reachable product image. Meta fetches it directly.
availabilitystringoptional
Stock state.
enum
["in stock","out of stock","preorder","available for order","discontinued"]
conditionstringoptional
Product condition.
enum
["new","refurbished","used"]
brandstring | nulloptional
Brand name.
categorystring | nulloptional
Category label.
product_typestring | nulloptional
Your own product taxonomy string.
inventoryinteger | nulloptional
Stock count.
visibilitystringoptional
Whether customers can see it.
enum
["staging","published"]
review_statusstring | nulloptional
Meta review outcome: pending, approved or rejected.
sync_statusstring | nulloptional
A roll-up of `listings`. A product on a shop with no platforms is `synced`, because there is nothing to sync.
enum
["pending","syncing","synced","failed","blocked","drifted",null]
listingsarray<object>optional
One row per platform this shop sells on.
Show child properties
platformstringrequired
Which platform this listing is on.
statestringrequired
`blocked` is not a failure: nothing was attempted because the product is missing something this platform requires. `failed` means the platform refused a push and it will be retried. `drifted` means the platform's copy no longer matches ours — someone edited it there — and the shop's `source_of_truth` decides which copy wins.
enum
["pending","syncing","synced","failed","blocked","drifted"]
external_idstring | nulloptional
The platform's own product id once it is live.
problemstring | nulloptional
Why this platform will not show the product yet.
review_statusstring | nulloptional
The platform's review verdict, where it has one.
last_synced_atstring | nulloptional
When this listing last reached the platform.
format
date-time
driftobject | nulloptional
Field by field, what differs, while the listing is `drifted`. Cleared once resolved.
additionalProperties
{"type":"object","properties":{"ours":{"description":"The value we hold."},"theirs":{"description":"The value the platform holds."}}}
drift_detected_atstring | nulloptional
When the difference was last seen.
format
date-time
last_synced_atstring | nulloptional
When the product last synced to Meta.
format
date-time
created_atstring | nulloptional
ISO 8601 creation timestamp.
format
date-time
updated_atstring | nulloptional
ISO 8601 update timestamp.
format
date-time
{
    "status": "success",
    "data": {
        "id": 1,
        "catalogue_id": 1,
        "retailer_id": "ACME-001",
        "meta_product_id": null,
        "name": "Kanga Print",
        "description": null,
        "url": null,
        "price": 27000,
        "currency": "TZS",
        "sale_price": null,
        "image_url": "https://cdn.acme.co.tz/kanga.jpg",
        "availability": "in stock",
        "condition": "new",
        "brand": null,
        "category": null,
        "product_type": null,
        "inventory": null,
        "visibility": "published",
        "review_status": null,
        "last_synced_at": null,
        "created_at": "2026-09-04T19:19:55+00:00",
        "updated_at": "2026-09-04T19:19:55+00:00"
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}
404No such record for this tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}
422The payload failed validation. `errors` maps each rejected field to its messages.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Validation failed.",
    "errors": {
        "recipient": [
            "Provide recipient or recipients."
        ]
    }
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}
502WhatsApp/Meta refused the call. The message repeats what they said; the record was not changed.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "(#100) Invalid parameter: product image_url is not reachable."
}

API REFERENCE / Catalogue

Delete a product

DELETE/api/v3/catalogues/{catalogue}/products/{product}

Removes the product from the catalogue, and from Meta first when it was mirrored there.

AuthenticationTenant API token

Path parameters

catalogueintegerrequired
Catalogue (shop) id, as returned by `GET /api/v3/catalogues`.

Example: 1

productintegerrequired
Product id. Not the `retailer_id` — that is your own SKU.

Example: 1

Responses

200The product was deleted.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
What was deleted.
Show child properties
deletedbooleanrequired
Always true.
idintegerrequired
Id of the deleted product.
retailer_idstringoptional
SKU of the deleted product, free to reuse now.
{
    "status": "success",
    "data": {
        "deleted": true,
        "id": 1,
        "retailer_id": "ACME-001"
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}
404No such record for this tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Catalogue

List orders

GET/api/v3/catalogues/orders

Orders customers submitted from a WhatsApp cart, newest first. Filter by status.

AuthenticationTenant API token

Query parameters

statusstringoptional
Only orders in this fulfilment state.
enum
["pending","confirmed","processing","shipped","delivered","cancelled","refunded"]

Example: pending

limitintegeroptional
Rows per page, 1–100. Defaults to 20 (25 for catalogue endpoints). Values above 100 are clamped.
minimum
1
maximum
100
default
20

Example: 20

per_pageintegeroptional
Alias of `limit`, for clients that already speak Laravel pagination. `limit` wins if both are sent.
minimum
1
maximum
100

Example: 25

pageintegeroptional
Page number, 1-based. Read `data.pagination.has_more_pages` to know when to stop.
minimum
1
default
1

Example: 1

updated_sincestringoptional
Only orders changed at or after this moment, newest change first. This is how you recover from a webhook you missed.
format
date-time

Example: 2026-09-11T02:00:00+00:00

platformstringoptional
Only orders that arrived through this platform.

Example: whatsapp

catalogue_idintegeroptional
Only orders against this shop.

Example: 42

Responses

200A page of orders.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
A page of rows and its page state.
Show child properties
itemsarray<object>required
The orders on this page.
Show child properties
idintegerrequired
Order id.
catalogue_idinteger | nulloptional
Catalogue the cart was built from.
catalogueobject | nulloptional
Compact catalogue reference.
Show child properties
idintegeroptional
Catalogue id.
namestringoptional
Catalogue name.
platformstringoptional
Where the order came from: `whatsapp`, `storefront`, `instagram`, `ivr`, `manual` and so on.
customer_handlestring | nulloptional
Whatever identifies the customer on their own platform — a WhatsApp id, a handle, an email, a typed phone number.
customer_wa_idstringrequired
The older name for `customer_handle`. It has not held only WhatsApp ids since orders became platform-neutral; prefer `customer_handle`.
customer_namestring | nulloptional
WhatsApp profile name, when shared.
customer_phonestring | nulloptional
Phone number when it differs from the WhatsApp id.
customer_notestring | nulloptional
Free text the customer attached to the order.
product_itemsarray<object>optional
The cart lines.
Show child properties
product_retailer_idstringoptional
The SKU the customer added to the cart.
quantityintegeroptional
How many.
item_priceintegeroptional
Unit price in the minor unit of `currency`.
currencystringoptional
ISO 4217 currency code.
unit_price_minorinteger | nulloptional
Unit price in the minor unit. Prefer this over `item_price`, which is Meta's own field and is in MAJOR units.
line_total_minorinteger | nulloptional
Unit price times quantity, in the minor unit.
reservedinteger | nulloptional
How many units of this line are actually being held for the order.
stock_shortboolean | nulloptional
True when we could not hold the whole quantity. The order was still recorded.
unresolvedboolean | nulloptional
True when this product code is not in the catalogue. The code is kept verbatim so a person can work out what the customer meant.
total_amountintegeroptional
Order total in the minor unit of `total_currency`.
total_currencystringoptional
ISO 4217 currency code.
statusstringrequired
Fulfilment state.
enum
["pending","confirmed","processing","shipped","delivered","cancelled","refunded"]
needs_attentionbooleanoptional
Somebody has to look at this before it can be fulfilled: a line we could not hold stock for, or a product code that is not in the catalogue. The order still exists — a customer asked for it — and the offending line says which of the two it is.
gateway_message_idstring | nulloptional
WhatsApp message id the order arrived on.
created_atstring | nulloptional
ISO 8601 creation timestamp.
format
date-time
updated_atstring | nulloptional
ISO 8601 update timestamp.
format
date-time
paginationobjectrequired
Page state for this list: where you are and whether more pages follow.
Show child properties
current_pageintegerrequired
1-based current page index.
per_pageintegerrequired
Number of items per page.
last_pageintegerrequired
1-based index of the last page.
totalintegerrequired
Total number of items across all pages.
has_more_pagesbooleanrequired
True if more pages exist after the current page.
{
    "status": "success",
    "data": {
        "items": [
            {
                "id": 1,
                "catalogue_id": 1,
                "catalogue": {
                    "id": 1,
                    "name": "Acme Duka"
                },
                "customer_wa_id": "255700111222",
                "customer_name": "Asha Mrisho",
                "customer_phone": null,
                "customer_note": null,
                "product_items": [
                    {
                        "product_retailer_id": "ACME-001",
                        "quantity": 2,
                        "item_price": 25000,
                        "currency": "TZS"
                    }
                ],
                "total_amount": 50000,
                "total_currency": "TZS",
                "status": "pending",
                "gateway_message_id": null,
                "created_at": "2026-09-04T19:19:55+00:00",
                "updated_at": "2026-09-04T19:19:55+00:00"
            }
        ],
        "pagination": {
            "current_page": 1,
            "per_page": 25,
            "last_page": 1,
            "total": 1,
            "has_more_pages": false
        }
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Catalogue

Read one order

GET/api/v3/catalogues/orders/{order}

One order with its cart lines and the catalogue it came from.

AuthenticationTenant API token

Path parameters

orderintegerrequired
Order id, as returned by `GET /api/v3/catalogues/orders`.

Example: 1

Responses

200The order.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
The order record.
Show child properties
idintegerrequired
Order id.
catalogue_idinteger | nulloptional
Catalogue the cart was built from.
catalogueobject | nulloptional
Compact catalogue reference.
Show child properties
idintegeroptional
Catalogue id.
namestringoptional
Catalogue name.
platformstringoptional
Where the order came from: `whatsapp`, `storefront`, `instagram`, `ivr`, `manual` and so on.
customer_handlestring | nulloptional
Whatever identifies the customer on their own platform — a WhatsApp id, a handle, an email, a typed phone number.
customer_wa_idstringrequired
The older name for `customer_handle`. It has not held only WhatsApp ids since orders became platform-neutral; prefer `customer_handle`.
customer_namestring | nulloptional
WhatsApp profile name, when shared.
customer_phonestring | nulloptional
Phone number when it differs from the WhatsApp id.
customer_notestring | nulloptional
Free text the customer attached to the order.
product_itemsarray<object>optional
The cart lines.
Show child properties
product_retailer_idstringoptional
The SKU the customer added to the cart.
quantityintegeroptional
How many.
item_priceintegeroptional
Unit price in the minor unit of `currency`.
currencystringoptional
ISO 4217 currency code.
unit_price_minorinteger | nulloptional
Unit price in the minor unit. Prefer this over `item_price`, which is Meta's own field and is in MAJOR units.
line_total_minorinteger | nulloptional
Unit price times quantity, in the minor unit.
reservedinteger | nulloptional
How many units of this line are actually being held for the order.
stock_shortboolean | nulloptional
True when we could not hold the whole quantity. The order was still recorded.
unresolvedboolean | nulloptional
True when this product code is not in the catalogue. The code is kept verbatim so a person can work out what the customer meant.
total_amountintegeroptional
Order total in the minor unit of `total_currency`.
total_currencystringoptional
ISO 4217 currency code.
statusstringrequired
Fulfilment state.
enum
["pending","confirmed","processing","shipped","delivered","cancelled","refunded"]
needs_attentionbooleanoptional
Somebody has to look at this before it can be fulfilled: a line we could not hold stock for, or a product code that is not in the catalogue. The order still exists — a customer asked for it — and the offending line says which of the two it is.
gateway_message_idstring | nulloptional
WhatsApp message id the order arrived on.
created_atstring | nulloptional
ISO 8601 creation timestamp.
format
date-time
updated_atstring | nulloptional
ISO 8601 update timestamp.
format
date-time
{
    "status": "success",
    "data": {
        "id": 1,
        "catalogue_id": 1,
        "catalogue": {
            "id": 1,
            "name": "Acme Duka"
        },
        "customer_wa_id": "255700111222",
        "customer_name": "Asha Mrisho",
        "customer_phone": null,
        "customer_note": null,
        "product_items": [
            {
                "product_retailer_id": "ACME-001",
                "quantity": 2,
                "item_price": 25000,
                "currency": "TZS"
            }
        ],
        "total_amount": 50000,
        "total_currency": "TZS",
        "status": "pending",
        "gateway_message_id": null,
        "created_at": "2026-09-04T19:19:55+00:00",
        "updated_at": "2026-09-04T19:19:55+00:00"
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}
404No such record for this tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Catalogue

Move an order to another status

PUT/api/v3/catalogues/orders/{order}/status

Records a fulfilment transition. Each move is appended to the order history, and when the shop has status templates configured the customer is notified on WhatsApp.

AuthenticationTenant API token

Path parameters

orderintegerrequired
Order id, as returned by `GET /api/v3/catalogues/orders`.

Example: 1

Request body

application/json · required

statusstringrequired
The status to move to.
enum
["pending","confirmed","processing","shipped","delivered","cancelled","refunded"]
Complete request schema
{
    "type": "object",
    "required": [
        "status"
    ],
    "properties": {
        "status": {
            "type": "string",
            "enum": [
                "pending",
                "confirmed",
                "processing",
                "shipped",
                "delivered",
                "cancelled",
                "refunded"
            ],
            "description": "The status to move to."
        }
    }
}
Confirm a new order
{
    "status": "confirmed"
}
Mark shipped
{
    "status": "shipped"
}
Cancel
{
    "status": "cancelled"
}

Responses

200The order in its new status.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
The order record.
Show child properties
idintegerrequired
Order id.
catalogue_idinteger | nulloptional
Catalogue the cart was built from.
catalogueobject | nulloptional
Compact catalogue reference.
Show child properties
idintegeroptional
Catalogue id.
namestringoptional
Catalogue name.
platformstringoptional
Where the order came from: `whatsapp`, `storefront`, `instagram`, `ivr`, `manual` and so on.
customer_handlestring | nulloptional
Whatever identifies the customer on their own platform — a WhatsApp id, a handle, an email, a typed phone number.
customer_wa_idstringrequired
The older name for `customer_handle`. It has not held only WhatsApp ids since orders became platform-neutral; prefer `customer_handle`.
customer_namestring | nulloptional
WhatsApp profile name, when shared.
customer_phonestring | nulloptional
Phone number when it differs from the WhatsApp id.
customer_notestring | nulloptional
Free text the customer attached to the order.
product_itemsarray<object>optional
The cart lines.
Show child properties
product_retailer_idstringoptional
The SKU the customer added to the cart.
quantityintegeroptional
How many.
item_priceintegeroptional
Unit price in the minor unit of `currency`.
currencystringoptional
ISO 4217 currency code.
unit_price_minorinteger | nulloptional
Unit price in the minor unit. Prefer this over `item_price`, which is Meta's own field and is in MAJOR units.
line_total_minorinteger | nulloptional
Unit price times quantity, in the minor unit.
reservedinteger | nulloptional
How many units of this line are actually being held for the order.
stock_shortboolean | nulloptional
True when we could not hold the whole quantity. The order was still recorded.
unresolvedboolean | nulloptional
True when this product code is not in the catalogue. The code is kept verbatim so a person can work out what the customer meant.
total_amountintegeroptional
Order total in the minor unit of `total_currency`.
total_currencystringoptional
ISO 4217 currency code.
statusstringrequired
Fulfilment state.
enum
["pending","confirmed","processing","shipped","delivered","cancelled","refunded"]
needs_attentionbooleanoptional
Somebody has to look at this before it can be fulfilled: a line we could not hold stock for, or a product code that is not in the catalogue. The order still exists — a customer asked for it — and the offending line says which of the two it is.
gateway_message_idstring | nulloptional
WhatsApp message id the order arrived on.
created_atstring | nulloptional
ISO 8601 creation timestamp.
format
date-time
updated_atstring | nulloptional
ISO 8601 update timestamp.
format
date-time
{
    "status": "success",
    "data": {
        "id": 1,
        "catalogue_id": 1,
        "catalogue": {
            "id": 1,
            "name": "Acme Duka"
        },
        "customer_wa_id": "255700111222",
        "customer_name": "Asha Mrisho",
        "customer_phone": null,
        "customer_note": null,
        "product_items": [
            {
                "product_retailer_id": "ACME-001",
                "quantity": 2,
                "item_price": 25000,
                "currency": "TZS"
            }
        ],
        "total_amount": 50000,
        "total_currency": "TZS",
        "status": "confirmed",
        "gateway_message_id": null,
        "created_at": "2026-09-04T19:19:55+00:00",
        "updated_at": "2026-09-04T19:19:55+00:00"
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}
404No such record for this tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}
422Unknown status value.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Validation failed.",
    "errors": {
        "status": [
            "The selected status is invalid."
        ]
    }
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Catalogue

Send one product into a chat, on any platform

POST/api/v3/catalogues/send-product

With platform: whatsapp (the default) this is Meta's interactive product message: the customer sees a card and can add to cart. It needs to, the provider catalogue_id and product_retailer_id.

With any other platform the honest answer is a link. The response carries the product's address on that platform and the endpoint to send it with — one send path for every platform, rather than one per platform. A product that is not published there, or a platform with nowhere to point at, is refused with a 422 that says so.

AuthenticationTenant API token

Request body

application/json · required

platformstringoptional
Where the product should be shown. Defaults to WhatsApp.
enum
["whatsapp","storefront","facebook","instagram","tiktok"]
default
whatsapp
tostringoptional
Customer's WhatsApp number in E.164 without +. Required on WhatsApp only.
catalogue_idstringoptional
Meta catalogue id of the connected shop. Required on WhatsApp only.
product_retailer_idstringrequired
SKU of the product to show.
maxLength
100
bodystringoptional
Message text above the product card.
maxLength
1024
footerstringoptional
Small footer text.
maxLength
60
fromstringoptional
Send from this WhatsApp number when the tenant has several. Defaults to the account default. `GET /api/v3/whatsapp/accounts` lists the accounts and their numbers (`phone_numbers[].id`, or the number itself).
Complete request schema
{
    "type": "object",
    "required": [
        "product_retailer_id"
    ],
    "properties": {
        "platform": {
            "type": "string",
            "enum": [
                "whatsapp",
                "storefront",
                "facebook",
                "instagram",
                "tiktok"
            ],
            "default": "whatsapp",
            "description": "Where the product should be shown. Defaults to WhatsApp."
        },
        "to": {
            "type": "string",
            "description": "Customer's WhatsApp number in E.164 without +. Required on WhatsApp only."
        },
        "catalogue_id": {
            "type": "string",
            "description": "Meta catalogue id of the connected shop. Required on WhatsApp only."
        },
        "product_retailer_id": {
            "type": "string",
            "maxLength": 100,
            "description": "SKU of the product to show."
        },
        "body": {
            "type": "string",
            "maxLength": 1024,
            "description": "Message text above the product card."
        },
        "footer": {
            "type": "string",
            "maxLength": 60,
            "description": "Small footer text."
        },
        "from": {
            "type": "string",
            "description": "Send from this WhatsApp number when the tenant has several. Defaults to the account default. `GET /api/v3/whatsapp/accounts` lists the accounts and their numbers (`phone_numbers[].id`, or the number itself)."
        }
    }
}

Responses

200On WhatsApp, the gateway message id. On any other platform, the link to send.
statusstringrequired
Always "success".
enum
["success"]
dataanyrequired
A message id on WhatsApp; a link everywhere else.
Show child properties
Alternative 1oneOfoptional
Show child properties
message_idstringoptional
The gateway message id, on WhatsApp.
Alternative 2oneOfoptional
Show child properties
platformstringoptional
The platform asked for.
skustringoptional
The product code.
linkstringoptional
Where the product lives on that platform.
format
uri
statestringoptional
The listing state there.
send_withstringoptional
The endpoint to send the link with.
oneOf 1 object
message_idstringoptional
The gateway message id, on WhatsApp.
oneOf 2 object
platformstringoptional
The platform asked for.
skustringoptional
The product code.
linkstringoptional
Where the product lives on that platform.
format
uri
statestringoptional
The listing state there.
send_withstringoptional
The endpoint to send the link with.
{
    "status": "success",
    "data": {
        "message_id": "wamid.HBgL\u2026"
    }
}
Sent as an interactive message
{
    "status": "success",
    "data": {
        "message_id": "wamid.HBgL\u2026"
    }
}
A link to send
{
    "status": "success",
    "data": {
        "platform": "storefront",
        "sku": "MNG-45W",
        "link": "https://business.momo.tz/shop/mango/p/MNG-45W",
        "state": "synced",
        "send_with": "/api/v3/whatsapp/send"
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
422Validation failed, or the tenant has no active WhatsApp channel.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "No WhatsApp channel configured."
}
No WhatsApp channel connected
{
    "status": "error",
    "message": "No WhatsApp channel configured."
}
Missing fields
{
    "status": "error",
    "message": "Validation failed.",
    "errors": {
        "to": [
            "The to field is required."
        ]
    }
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}
502WhatsApp/Meta refused the call. The message repeats what they said; the record was not changed.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "(#100) Invalid parameter: product image_url is not reachable."
}

API REFERENCE / Catalogue

Send a multi-product list to a customer

POST/api/v3/catalogues/send-product-list

Sends a multi-product message: up to 10 sections of products the customer can browse and add to a cart.

AuthenticationTenant API token

Request body

application/json · required

tostringrequired
Customer's WhatsApp number in E.164 without +.
catalogue_idstringrequired
Meta catalogue id of the connected shop.
header_textstringrequired
Bold header above the list.
maxLength
60
bodystringrequired
Message text.
maxLength
1024
footerstringoptional
Small footer text.
maxLength
60
sectionsarray<object>required
Product groups, in display order.
minItems
1
maxItems
10
Show child properties
titlestringrequired
Section heading.
maxLength
24
product_itemsarray<object>required
Products in the section.
minItems
1
Show child properties
product_retailer_idstringrequired
SKU to include.
maxLength
100
fromstringoptional
Send from this WhatsApp number when the tenant has several. `GET /api/v3/whatsapp/accounts` lists the accounts and their numbers (`phone_numbers[].id`, or the number itself).
Complete request schema
{
    "type": "object",
    "required": [
        "to",
        "catalogue_id",
        "header_text",
        "body",
        "sections"
    ],
    "properties": {
        "to": {
            "type": "string",
            "description": "Customer's WhatsApp number in E.164 without +."
        },
        "catalogue_id": {
            "type": "string",
            "description": "Meta catalogue id of the connected shop."
        },
        "header_text": {
            "type": "string",
            "maxLength": 60,
            "description": "Bold header above the list."
        },
        "body": {
            "type": "string",
            "maxLength": 1024,
            "description": "Message text."
        },
        "footer": {
            "type": "string",
            "maxLength": 60,
            "description": "Small footer text."
        },
        "sections": {
            "type": "array",
            "minItems": 1,
            "maxItems": 10,
            "description": "Product groups, in display order.",
            "items": {
                "type": "object",
                "required": [
                    "title",
                    "product_items"
                ],
                "properties": {
                    "title": {
                        "type": "string",
                        "maxLength": 24,
                        "description": "Section heading."
                    },
                    "product_items": {
                        "type": "array",
                        "minItems": 1,
                        "description": "Products in the section.",
                        "items": {
                            "type": "object",
                            "required": [
                                "product_retailer_id"
                            ],
                            "properties": {
                                "product_retailer_id": {
                                    "type": "string",
                                    "maxLength": 100,
                                    "description": "SKU to include."
                                }
                            }
                        }
                    }
                }
            }
        },
        "from": {
            "type": "string",
            "description": "Send from this WhatsApp number when the tenant has several. `GET /api/v3/whatsapp/accounts` lists the accounts and their numbers (`phone_numbers[].id`, or the number itself)."
        }
    }
}

Responses

200WhatsApp accepted the message.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
The message WhatsApp accepted.
Show child properties
message_idstringrequired
The WhatsApp message id (`wamid.…`) to match against later message.* webhooks.
{
    "status": "success",
    "data": {
        "message_id": "wamid.HBgLMjU1NzAwMTExMjIyFQIAERgSN0YzNzhBQTQ5MzBBM0YwQzE2AA=="
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
422Validation failed, or the tenant has no active WhatsApp channel.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "No WhatsApp channel configured."
}
No WhatsApp channel connected
{
    "status": "error",
    "message": "No WhatsApp channel configured."
}
Missing fields
{
    "status": "error",
    "message": "Validation failed.",
    "errors": {
        "to": [
            "The to field is required."
        ]
    }
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}
502WhatsApp/Meta refused the call. The message repeats what they said; the record was not changed.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "(#100) Invalid parameter: product image_url is not reachable."
}

API REFERENCE / Catalogue

Send the whole catalogue to a customer

POST/api/v3/catalogues/send-catalogue

Sends a catalogue message: an invitation to browse the full shop, optionally showing one product as the thumbnail.

AuthenticationTenant API token

Request body

application/json · required

tostringrequired
Customer's WhatsApp number in E.164 without +.
bodystringrequired
Message text.
maxLength
1024
thumbnail_product_retailer_idstringoptional
SKU to use as the cover image. Defaults to the first product.
maxLength
100
footerstringoptional
Small footer text.
maxLength
60
fromstringoptional
Send from this WhatsApp number when the tenant has several. `GET /api/v3/whatsapp/accounts` lists the accounts and their numbers (`phone_numbers[].id`, or the number itself).
Complete request schema
{
    "type": "object",
    "required": [
        "to",
        "body"
    ],
    "properties": {
        "to": {
            "type": "string",
            "description": "Customer's WhatsApp number in E.164 without +."
        },
        "body": {
            "type": "string",
            "maxLength": 1024,
            "description": "Message text."
        },
        "thumbnail_product_retailer_id": {
            "type": "string",
            "maxLength": 100,
            "description": "SKU to use as the cover image. Defaults to the first product."
        },
        "footer": {
            "type": "string",
            "maxLength": 60,
            "description": "Small footer text."
        },
        "from": {
            "type": "string",
            "description": "Send from this WhatsApp number when the tenant has several. `GET /api/v3/whatsapp/accounts` lists the accounts and their numbers (`phone_numbers[].id`, or the number itself)."
        }
    }
}

Responses

200WhatsApp accepted the message.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
The message WhatsApp accepted.
Show child properties
message_idstringrequired
The WhatsApp message id (`wamid.…`) to match against later message.* webhooks.
{
    "status": "success",
    "data": {
        "message_id": "wamid.HBgLMjU1NzAwMTExMjIyFQIAERgSN0YzNzhBQTQ5MzBBM0YwQzE2AA=="
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
422Validation failed, or the tenant has no active WhatsApp channel.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "No WhatsApp channel configured."
}
No WhatsApp channel connected
{
    "status": "error",
    "message": "No WhatsApp channel configured."
}
Missing fields
{
    "status": "error",
    "message": "Validation failed.",
    "errors": {
        "to": [
            "The to field is required."
        ]
    }
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}
502WhatsApp/Meta refused the call. The message repeats what they said; the record was not changed.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "(#100) Invalid parameter: product image_url is not reachable."
}

API REFERENCE / Data tables

List data tables

GET/api/v3/data/tables

Every table this tenant has defined, by name. Take the id into the other endpoints; the schema endpoint tells you what each table holds.

AuthenticationTenant API token

Required permission: data.view

Responses

200The tables.
tablesarray<object>required
The tables, ordered by name.
Show child properties
idstringrequired
The table id; the `{table}` path parameter everywhere else.
format
uuid
namestringrequired
Display name.
slugstringrequired
URL-safe name, unique within the tenant.
descriptionstring | nulloptional
What the table holds, as written by whoever created it.
iconstring | nulloptional
Icon name chosen in the dashboard, or null.
records_countintegerrequired
Live (not deleted) records in the table.
columns_countintegerrequired
Columns defined on the table.
updated_atstring | nulloptional
When the table or its columns last changed (ISO-8601).
format
date-time
{
    "tables": [
        {
            "id": "9d2f4c1e-7b8a-4e0f-9a3b-5c6d7e8f9a0b",
            "name": "Customers",
            "slug": "customers",
            "description": "Everyone who has bought from us.",
            "icon": "users",
            "records_count": 1286,
            "columns_count": 6,
            "updated_at": "2026-09-07T14:02:31+00:00"
        },
        {
            "id": "4e8d9c0b-1a2f-4b3c-8d4e-5f6a7b8c9d0e",
            "name": "Deliveries",
            "slug": "deliveries",
            "description": null,
            "icon": null,
            "records_count": 52014,
            "columns_count": 9,
            "updated_at": "2026-09-08T06:15:00+00:00"
        }
    ]
}
default
{
    "tables": [
        {
            "id": "9d2f4c1e-7b8a-4e0f-9a3b-5c6d7e8f9a0b",
            "name": "Customers",
            "slug": "customers",
            "description": "Everyone who has bought from us.",
            "icon": "users",
            "records_count": 1286,
            "columns_count": 6,
            "updated_at": "2026-09-07T14:02:31+00:00"
        },
        {
            "id": "4e8d9c0b-1a2f-4b3c-8d4e-5f6a7b8c9d0e",
            "name": "Deliveries",
            "slug": "deliveries",
            "description": null,
            "icon": null,
            "records_count": 52014,
            "columns_count": 9,
            "updated_at": "2026-09-08T06:15:00+00:00"
        }
    ]
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The key's issuer does not hold data.view, or the key has no issuer on record.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This API key was issued by a user without the \"data.view\" permission."
}
default
{
    "status": "error",
    "message": "This API key was issued by a user without the \"data.view\" permission."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Data tables

Read a table's schema

GET/api/v3/data/tables/{table}/schema

The columns of a table — key, type, whether required or unique, the validation rules a write runs and the operators a filter may use — plus what every type can do, the system columns, quota usage and what this key is allowed to do. Read it once before writing records, and again after a column changes in the dashboard.

AuthenticationTenant API token

Required permission: data.view

Path parameters

tablestringrequired
The table id (from `GET /api/v3/data/tables`). Anything that is not a UUID, or a table belonging to another tenant, answers 404.
format
uuid

Example: 9d2f4c1e-7b8a-4e0f-9a3b-5c6d7e8f9a0b

Responses

200The schema.
tableobjectrequired
The table.
Show child properties
idstringrequired
The table id.
format
uuid
namestringrequired
Display name.
slugstringrequired
URL-safe name, unique within the tenant.
descriptionstring | nulloptional
What the table holds, or null.
iconstring | nulloptional
Icon name chosen in the dashboard, or null.
records_countintegerrequired
Live (not deleted) records in the table.
storage_bytesintegerrequired
Bytes the records occupy, counted against the storage quota.
title_columnstring | nulloptional
Key of the column that names a record (the `title` on every record row), or null when the first text column is used.
created_atstring | nulloptional
When the table was created (ISO-8601).
format
date-time
updated_atstring | nulloptional
When the table or its columns last changed (ISO-8601).
format
date-time
retentionobjectoptional
Retention policy description.
additionalProperties
true
legal_holdbooleanoptional
Whether retention deletion is held for this table.
columnsarray<object>required
The columns, in position order.
Show child properties
idstringrequired
The column id.
format
uuid
keystringrequired
The key this column has inside a record's `data`, and the `column` to name in a filter or a `sort`.
labelstringrequired
Display label.
typestringrequired
The field type. Its rules, operators and display hints are in `types` on the schema payload. `auto_number` is written by the platform: its `ui.readonly` is true and a value sent for it is refused.
enum
["text","long_text","number","currency","boolean","date","datetime","phone","email","select","multi_select","relation","file","auto_number","unknown"]
stored_typestringoptional
Only when `type` is `unknown`: the type name actually stored, which this version cannot render.
positionintegerrequired
Zero-based column order; record `data` keys come back in this order.
requiredbooleanrequired
A create must supply a value; an update may not clear it.
uniquebooleanrequired
No two live records may share a value. A duplicate answers 422 with `errors`.
indexedbooleanrequired
Whether the column has an index. Sorting a large table on a column needs one — see `sort_index_threshold`.
index_statusstring | nulloptional
State of the latest index job on this column, or null when none was ever requested. Only `ready` makes the column sortable at scale.
enum
["pending","building","ready","failed","dropping",null]
index_errorstring | nulloptional
Why the index build failed, when `index_status` is `failed`.
configobjectoptional
Type-specific settings: `options` for select/multi_select, `table_id` for relation, `default`, `ui` hints, and so on.
additionalProperties
true
rulesarray<string>required
The validation rules a write runs, Laravel-style (`required`, `phone:TZ`, `max:255`, …).
operatorsarray<string>required
The filter operators this column accepts. Any other operator answers 422.
items.enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
uiobjectrequired
Display hints for a grid or form: `cell` and `input` renderer names, `filter` widget, `width` in pixels, plus any of `hidden_in_grid`, `hidden_in_form`, `help_text`, `placeholder`, `is_title_field` set in the dashboard.
additionalProperties
true
Show child properties
cellstringoptional
Renderer for the value in a grid cell.
inputstringoptional
Renderer for the value in a form.
filterstring | nulloptional
Filter widget, or null when the column cannot be filtered.
widthintegeroptional
Suggested column width in pixels.
warningstringoptional
Only when `type` is `unknown`: why the column is read-only.
typesobjectrequired
Every field type this version knows, keyed by name (`text`, `number`, `phone`, …).
additionalProperties
{"$ref":"#/components/schemas/DataFieldType"}
system_columnsarray<object>required
The `$id`, `$created_at`, `$updated_at` and `$source` columns.
Show child properties
keystringrequired
The key to use in a filter `column` or in `sort`.
enum
["$id","$created_at","$updated_at","$source"]
labelstringrequired
Display label.
typestringrequired
The field type its values behave as.
operatorsarray<string>required
The operators this system column accepts.
items.enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
limitsobjectrequired
Current table allowances and usage. Record/storage quota refusals use HTTP 402 with error.code quota_exceeded and quota/limit/used in error.details.
Show child properties
columnsobjectoptional
How many columns the table uses against its allowance.
Show child properties
usedintegeroptional
columns in use.
maxintegeroptional
The most columns this table may have.
indexesobjectoptional
How many indexes the table uses against its allowance.
Show child properties
usedintegeroptional
indexes in use.
maxintegeroptional
The most indexes this table may have.
recordsobjectoptional
How many records the table uses against its allowance.
Show child properties
usedintegeroptional
records in use.
maxintegeroptional
The most records this table may have.
storageobjectoptional
Bytes the records occupy against the table's storage allowance.
Show child properties
used_bytesintegeroptional
Bytes in use.
max_bytesintegeroptional
The storage allowance in bytes.
sort_index_thresholdintegerrequired
At or above this record count, sorting on an unindexed user column returns 501 not_supported with reason sort_needs_index. System timestamp sorts remain supported.
canobjectrequired
What the user who issued this key may do.
Show child properties
managebooleanoptional
May change tables and columns (in the dashboard; not over this API).
edit_recordsbooleanoptional
May create, change and delete records — the gate on the write endpoints here.
manage_reportsbooleanoptional
May save reports on this table.
accessobjectrequired
Per-table access for this caller, including governed state and granted capabilities.
additionalProperties
true
actionsarray<object>required
Available record-action summaries, without private action secrets.
items.additionalProperties
true
unique_setsarray<object>required
Unique field combinations and their index state.
items.additionalProperties
true
{
    "table": {
        "id": "9d2f4c1e-7b8a-4e0f-9a3b-5c6d7e8f9a0b",
        "name": "Customers",
        "slug": "customers",
        "description": "Everyone who has bought from us.",
        "icon": "users",
        "records_count": 1286,
        "storage_bytes": 418304,
        "title_column": "name",
        "created_at": "2026-08-30T09:00:00+00:00",
        "updated_at": "2026-09-07T14:02:31+00:00"
    },
    "columns": [
        {
            "id": "6a1b2c3d-0000-4000-8000-000000000001",
            "key": "name",
            "label": "Name",
            "type": "text",
            "position": 0,
            "required": true,
            "unique": false,
            "indexed": false,
            "index_status": null,
            "index_error": null,
            "config": {
                "ui": {
                    "is_title_field": true
                }
            },
            "rules": [
                "required",
                "string",
                "max:255"
            ],
            "operators": [
                "equals",
                "not_equals",
                "contains",
                "starts_with",
                "is_empty",
                "is_not_empty",
                "in"
            ],
            "ui": {
                "is_title_field": true,
                "cell": "text",
                "input": "text",
                "filter": "text",
                "width": 200
            }
        },
        {
            "id": "6a1b2c3d-0000-4000-8000-000000000002",
            "key": "phone",
            "label": "Simu",
            "type": "phone",
            "position": 1,
            "required": true,
            "unique": true,
            "indexed": true,
            "index_status": "ready",
            "index_error": null,
            "config": {
                "region": "TZ"
            },
            "rules": [
                "required",
                "phone:TZ"
            ],
            "operators": [
                "equals",
                "not_equals",
                "starts_with",
                "contains",
                "is_empty",
                "is_not_empty",
                "in"
            ],
            "ui": {
                "cell": "phone",
                "input": "phone",
                "filter": "text",
                "width": 160
            }
        },
        {
            "id": "6a1b2c3d-0000-4000-8000-000000000003",
            "key": "region",
            "label": "Region",
            "type": "select",
            "position": 2,
            "required": false,
            "unique": false,
            "indexed": false,
            "index_status": null,
            "index_error": null,
            "config": {
                "options": [
                    {
                        "key": "dar",
                        "label": "Dar es Salaam"
                    },
                    {
                        "key": "arusha",
                        "label": "Arusha"
                    }
                ]
            },
            "rules": [
                "in:dar,arusha"
            ],
            "operators": [
                "equals",
                "not_equals",
                "is_empty",
                "is_not_empty",
                "in"
            ],
            "ui": {
                "cell": "select",
                "input": "select",
                "filter": "select",
                "width": 140
            }
        },
        {
            "id": "6a1b2c3d-0000-4000-8000-000000000004",
            "key": "opt_in",
            "label": "Opted in",
            "type": "boolean",
            "position": 3,
            "required": false,
            "unique": false,
            "indexed": false,
            "index_status": null,
            "index_error": null,
            "config": [],
            "rules": [
                "boolean"
            ],
            "operators": [
                "equals",
                "is_empty",
                "is_not_empty"
            ],
            "ui": {
                "cell": "boolean",
                "input": "checkbox",
                "filter": "boolean",
                "width": 100
            }
        },
        {
            "id": "6a1b2c3d-0000-4000-8000-000000000005",
            "key": "balance",
            "label": "Balance",
            "type": "currency",
            "position": 4,
            "required": false,
            "unique": false,
            "indexed": false,
            "index_status": null,
            "index_error": null,
            "config": {
                "currency": "TZS"
            },
            "rules": [
                "numeric"
            ],
            "operators": [
                "equals",
                "not_equals",
                "greater_than",
                "less_than",
                "between",
                "is_empty",
                "is_not_empty"
            ],
            "ui": {
                "cell": "currency",
                "input": "number",
                "filter": "number",
                "width": 140
            }
        }
    ],
    "types": {
        "text": {
            "label": "Text",
            "operators": [
                "equals",
                "not_equals",
                "contains",
                "starts_with",
                "is_empty",
                "is_not_empty",
                "in"
            ],
            "ui": {
                "cell": "text",
                "input": "text",
                "filter": "text",
                "width": 200
            },
            "numeric": false,
            "temporal": false
        },
        "phone": {
            "label": "Phone",
            "operators": [
                "equals",
                "not_equals",
                "starts_with",
                "contains",
                "is_empty",
                "is_not_empty",
                "in"
            ],
            "ui": {
                "cell": "phone",
                "input": "phone",
                "filter": "text",
                "width": 160
            },
            "numeric": false,
            "temporal": false
        },
        "datetime": {
            "label": "Date & time",
            "operators": [
                "equals",
                "not_equals",
                "greater_than",
                "less_than",
                "between",
                "is_empty",
                "is_not_empty"
            ],
            "ui": {
                "cell": "datetime",
                "input": "datetime",
                "filter": "date",
                "width": 180
            },
            "numeric": false,
            "temporal": true
        }
    },
    "system_columns": [
        {
            "key": "$id",
            "label": "ID",
            "type": "relation",
            "operators": [
                "equals",
                "in"
            ]
        },
        {
            "key": "$created_at",
            "label": "Created",
            "type": "datetime",
            "operators": [
                "equals",
                "not_equals",
                "greater_than",
                "less_than",
                "between"
            ]
        },
        {
            "key": "$updated_at",
            "label": "Updated",
            "type": "datetime",
            "operators": [
                "equals",
                "not_equals",
                "greater_than",
                "less_than",
                "between"
            ]
        },
        {
            "key": "$source",
            "label": "Source",
            "type": "text",
            "operators": [
                "equals",
                "in",
                "starts_with"
            ]
        }
    ],
    "limits": {
        "columns": {
            "used": 5,
            "max": 40
        },
        "indexes": {
            "used": 1,
            "max": 5
        },
        "records": {
            "used": 1286,
            "max": 500000
        },
        "storage": {
            "used_bytes": 418304,
            "max_bytes": 2147483648
        }
    },
    "sort_index_threshold": 20000,
    "can": {
        "manage": true,
        "edit_records": true,
        "manage_reports": true
    }
}
default
{
    "table": {
        "id": "9d2f4c1e-7b8a-4e0f-9a3b-5c6d7e8f9a0b",
        "name": "Customers",
        "slug": "customers",
        "description": "Everyone who has bought from us.",
        "icon": "users",
        "records_count": 1286,
        "storage_bytes": 418304,
        "title_column": "name",
        "created_at": "2026-08-30T09:00:00+00:00",
        "updated_at": "2026-09-07T14:02:31+00:00"
    },
    "columns": [
        {
            "id": "6a1b2c3d-0000-4000-8000-000000000001",
            "key": "name",
            "label": "Name",
            "type": "text",
            "position": 0,
            "required": true,
            "unique": false,
            "indexed": false,
            "index_status": null,
            "index_error": null,
            "config": {
                "ui": {
                    "is_title_field": true
                }
            },
            "rules": [
                "required",
                "string",
                "max:255"
            ],
            "operators": [
                "equals",
                "not_equals",
                "contains",
                "starts_with",
                "is_empty",
                "is_not_empty",
                "in"
            ],
            "ui": {
                "is_title_field": true,
                "cell": "text",
                "input": "text",
                "filter": "text",
                "width": 200
            }
        },
        {
            "id": "6a1b2c3d-0000-4000-8000-000000000002",
            "key": "phone",
            "label": "Simu",
            "type": "phone",
            "position": 1,
            "required": true,
            "unique": true,
            "indexed": true,
            "index_status": "ready",
            "index_error": null,
            "config": {
                "region": "TZ"
            },
            "rules": [
                "required",
                "phone:TZ"
            ],
            "operators": [
                "equals",
                "not_equals",
                "starts_with",
                "contains",
                "is_empty",
                "is_not_empty",
                "in"
            ],
            "ui": {
                "cell": "phone",
                "input": "phone",
                "filter": "text",
                "width": 160
            }
        },
        {
            "id": "6a1b2c3d-0000-4000-8000-000000000003",
            "key": "region",
            "label": "Region",
            "type": "select",
            "position": 2,
            "required": false,
            "unique": false,
            "indexed": false,
            "index_status": null,
            "index_error": null,
            "config": {
                "options": [
                    {
                        "key": "dar",
                        "label": "Dar es Salaam"
                    },
                    {
                        "key": "arusha",
                        "label": "Arusha"
                    }
                ]
            },
            "rules": [
                "in:dar,arusha"
            ],
            "operators": [
                "equals",
                "not_equals",
                "is_empty",
                "is_not_empty",
                "in"
            ],
            "ui": {
                "cell": "select",
                "input": "select",
                "filter": "select",
                "width": 140
            }
        },
        {
            "id": "6a1b2c3d-0000-4000-8000-000000000004",
            "key": "opt_in",
            "label": "Opted in",
            "type": "boolean",
            "position": 3,
            "required": false,
            "unique": false,
            "indexed": false,
            "index_status": null,
            "index_error": null,
            "config": [],
            "rules": [
                "boolean"
            ],
            "operators": [
                "equals",
                "is_empty",
                "is_not_empty"
            ],
            "ui": {
                "cell": "boolean",
                "input": "checkbox",
                "filter": "boolean",
                "width": 100
            }
        },
        {
            "id": "6a1b2c3d-0000-4000-8000-000000000005",
            "key": "balance",
            "label": "Balance",
            "type": "currency",
            "position": 4,
            "required": false,
            "unique": false,
            "indexed": false,
            "index_status": null,
            "index_error": null,
            "config": {
                "currency": "TZS"
            },
            "rules": [
                "numeric"
            ],
            "operators": [
                "equals",
                "not_equals",
                "greater_than",
                "less_than",
                "between",
                "is_empty",
                "is_not_empty"
            ],
            "ui": {
                "cell": "currency",
                "input": "number",
                "filter": "number",
                "width": 140
            }
        }
    ],
    "types": {
        "text": {
            "label": "Text",
            "operators": [
                "equals",
                "not_equals",
                "contains",
                "starts_with",
                "is_empty",
                "is_not_empty",
                "in"
            ],
            "ui": {
                "cell": "text",
                "input": "text",
                "filter": "text",
                "width": 200
            },
            "numeric": false,
            "temporal": false
        },
        "phone": {
            "label": "Phone",
            "operators": [
                "equals",
                "not_equals",
                "starts_with",
                "contains",
                "is_empty",
                "is_not_empty",
                "in"
            ],
            "ui": {
                "cell": "phone",
                "input": "phone",
                "filter": "text",
                "width": 160
            },
            "numeric": false,
            "temporal": false
        },
        "datetime": {
            "label": "Date & time",
            "operators": [
                "equals",
                "not_equals",
                "greater_than",
                "less_than",
                "between",
                "is_empty",
                "is_not_empty"
            ],
            "ui": {
                "cell": "datetime",
                "input": "datetime",
                "filter": "date",
                "width": 180
            },
            "numeric": false,
            "temporal": true
        }
    },
    "system_columns": [
        {
            "key": "$id",
            "label": "ID",
            "type": "relation",
            "operators": [
                "equals",
                "in"
            ]
        },
        {
            "key": "$created_at",
            "label": "Created",
            "type": "datetime",
            "operators": [
                "equals",
                "not_equals",
                "greater_than",
                "less_than",
                "between"
            ]
        },
        {
            "key": "$updated_at",
            "label": "Updated",
            "type": "datetime",
            "operators": [
                "equals",
                "not_equals",
                "greater_than",
                "less_than",
                "between"
            ]
        },
        {
            "key": "$source",
            "label": "Source",
            "type": "text",
            "operators": [
                "equals",
                "in",
                "starts_with"
            ]
        }
    ],
    "limits": {
        "columns": {
            "used": 5,
            "max": 40
        },
        "indexes": {
            "used": 1,
            "max": 5
        },
        "records": {
            "used": 1286,
            "max": 500000
        },
        "storage": {
            "used_bytes": 418304,
            "max_bytes": 2147483648
        }
    },
    "sort_index_threshold": 20000,
    "can": {
        "manage": true,
        "edit_records": true,
        "manage_reports": true
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The key's issuer does not hold data.view, or the key has no issuer on record.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This API key was issued by a user without the \"data.view\" permission."
}
default
{
    "status": "error",
    "message": "This API key was issued by a user without the \"data.view\" permission."
}
404No table with that id for this tenant (or the id is not a UUID).
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}
default
{
    "status": "error",
    "message": "Resource not found."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Data tables

List records

GET/api/v3/data/tables/{table}/records

Returns a keyset page, default limit 50 and maximum 200, with optional JSON condition-tree filter and q text search. Preserve filter/q/sort/dir when sending next_cursor back as cursor. Invalid cursor text restarts at page one. Count is null unless with_count is enabled. At or above sort_index_threshold, user-column sorts need an index; unsupported sorts/operators return 501 not_supported.

AuthenticationTenant API token

Required permission: data.view

Path parameters

tablestringrequired
The table id (from `GET /api/v3/data/tables`). Anything that is not a UUID, or a table belonging to another tenant, answers 404.
format
uuid

Example: 9d2f4c1e-7b8a-4e0f-9a3b-5c6d7e8f9a0b

Query parameters

filterstringoptional
URL-encoded JSON condition tree, with all/any groups or column/op/value leaves. Malformed filters/unknown columns return 422; unsupported field operators return 501.

Example: {"all":[{"column":"opt_in","op":"equals","value":true},{"any":[{"column":"region","op":"in","value":["dar","arusha"]},{"column":"$created_at","op":"greater_than","value":{"relative":"last_7_days"}}]}]}

qstringoptional
Free-text search, case-insensitive, over up to six text-like columns (text, long_text, phone, email). Ignored on a table with none.

Example: asha

sortstringoptional
Column key to sort on, or `$created_at` / `$updated_at`. Nulls sort last. Omit for newest first.
default
$created_at

Example: balance

dirstringoptional
Sort direction. Anything else answers 422.
enum
["asc","desc"]
default
desc

Example: desc

cursorstringoptional
The `next_cursor` of the previous page. Send the same `filter`, `q`, `sort` and `dir` with it. Opaque: a cursor that does not decode starts again from the first page rather than failing.

Example: eyJjIjoiMjAyNi0wOS0wOFQwNzo0MToxMi40MTgyMDZaIiwiaSI6IjJjN2UxYTliLTNkNGYtNGE1Yi04YzZkLTdlOGY5YTBiMWMyZCJ9

limitintegeroptional
Records per page, 1–200. Values above 200 are clamped, not refused.
minimum
1
maximum
200
default
50

Example: 50

with_countbooleanoptional
Also count every record matching `filter` and `q`, into `count`. Costs a second query — ask on the first page only.
default
false

Example: 1

Responses

200A page of records.
recordsarray<object>required
The records on this page, in the requested sort order.
Show child properties
idstringrequired
The record id.
format
uuid
dataobjectrequired
The values, keyed by column key, in column position order. A column with no value is absent or null.
additionalProperties
true
sourcestringrequired
Who created the record, fixed at create time: `ui` for a person in the dashboard, `api` for this API, a flow identifier for a flow. Filter on it with the `$source` system column.
created_atstringrequired
When the record was created — UTC, with microseconds, so a cursor built from it resumes at exactly this row.
format
date-time
updated_atstringrequired
When the record last changed (UTC, microseconds).
format
date-time
titlestringrequired
What names this record: the table's title column, else its first text column, else the id.
titlesobjectoptional
Only on tables with relation columns: the related record's title keyed by the relation column key, resolved once per page so a client never fetches per cell.
additionalProperties
{"type":"string"}
next_cursorstring | nullrequired
Opaque position of the last row served. Pass it back as `cursor` — with the same `filter`, `q`, `sort` and `dir` — for the next page. Null on the last page.
has_morebooleanrequired
Whether another page follows.
countinteger | nullrequired
Total records matching the filter and search — only when `with_count=1` was sent, otherwise null.
served_atstringrequired
When this page was read (UTC). Records created after it are not on any later page of the same cursor chain when sorting `$created_at desc`.
format
date-time
{
    "records": [
        {
            "id": "2c7e1a9b-3d4f-4a5b-8c6d-7e8f9a0b1c2d",
            "data": {
                "name": "Asha Mwinyi",
                "phone": "+255712345678",
                "region": "dar",
                "opt_in": true,
                "balance": 15000
            },
            "source": "api",
            "created_at": "2026-09-08T07:41:12.418206Z",
            "updated_at": "2026-09-08T07:41:12.418206Z",
            "title": "Asha Mwinyi"
        },
        {
            "id": "7f3a2b1c-9d8e-4f7a-b6c5-d4e3f2a1b0c9",
            "data": {
                "name": "Juma Hassan",
                "phone": "+255754000111",
                "region": "arusha",
                "opt_in": true,
                "balance": 2500
            },
            "source": "ui",
            "created_at": "2026-09-06T11:03:44.902113Z",
            "updated_at": "2026-09-07T08:20:01.117650Z",
            "title": "Juma Hassan"
        }
    ],
    "next_cursor": "eyJjIjoiMjAyNi0wOS0wNlQxMTowMzo0NC45MDIxMTNaIiwiaSI6IjdmM2EyYjFjLTlkOGUtNGY3YS1iNmM1LWQ0ZTNmMmExYjBjOSJ9",
    "has_more": true,
    "count": 1286,
    "served_at": "2026-09-08T07:45:00Z"
}
default
{
    "records": [
        {
            "id": "2c7e1a9b-3d4f-4a5b-8c6d-7e8f9a0b1c2d",
            "data": {
                "name": "Asha Mwinyi",
                "phone": "+255712345678",
                "region": "dar",
                "opt_in": true,
                "balance": 15000
            },
            "source": "api",
            "created_at": "2026-09-08T07:41:12.418206Z",
            "updated_at": "2026-09-08T07:41:12.418206Z",
            "title": "Asha Mwinyi"
        },
        {
            "id": "7f3a2b1c-9d8e-4f7a-b6c5-d4e3f2a1b0c9",
            "data": {
                "name": "Juma Hassan",
                "phone": "+255754000111",
                "region": "arusha",
                "opt_in": true,
                "balance": 2500
            },
            "source": "ui",
            "created_at": "2026-09-06T11:03:44.902113Z",
            "updated_at": "2026-09-07T08:20:01.117650Z",
            "title": "Juma Hassan"
        }
    ],
    "next_cursor": "eyJjIjoiMjAyNi0wOS0wNlQxMTowMzo0NC45MDIxMTNaIiwiaSI6IjdmM2EyYjFjLTlkOGUtNGY3YS1iNmM1LWQ0ZTNmMmExYjBjOSJ9",
    "has_more": true,
    "count": 1286,
    "served_at": "2026-09-08T07:45:00Z"
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The key's issuer does not hold data.view, or the key has no issuer on record.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This API key was issued by a user without the \"data.view\" permission."
}
default
{
    "status": "error",
    "message": "This API key was issued by a user without the \"data.view\" permission."
}
404No table with that id for this tenant (or the id is not a UUID).
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}
default
{
    "status": "error",
    "message": "Resource not found."
}
422Malformed query or invalid record value. Missing request data may use the standard v3 envelope; expected data refusals use DataError.
Alternative 1oneOfoptional
Expected refusal from the data store. Authentication/framework failures can instead use LegacyErrorEnvelope. Quotas are 402; conflicts 409; unsupported operators and sorts needing an index 501.
Show child properties
errorobjectrequired
Structured domain refusal with a stable machine code and retry guidance.
Show child properties
codestringrequired
Stable refusal category.
enum
["validation_error","conflict","not_found","rate_limited","quota_exceeded","temporary_failure","permission_denied","not_supported","provider_failure"]
messagestringrequired
Human-readable explanation.
retryablebooleanrequired
Whether repeating unchanged could succeed; still apply operation idempotency rules.
fieldstringoptional
Field key associated with this refusal.
expected_typestringoptional
Expected field type when available.
receivedstringoptional
JSON type/shape description, not the submitted sensitive value.
retry_after_secondsintegeroptional
Suggested wait before retry.
minimum
0
detailsobjectoptional
Additional field errors, quota details or reason information.
additionalProperties
true
messagestringrequired
Compatibility human message from the originating refusal.
codestringrequired
Same category as error.code.
enum
["validation_error","conflict","not_found","rate_limited","quota_exceeded","temporary_failure","permission_denied","not_supported","provider_failure"]
errorsobjectoptional
Optional field messages in Laravel validation shape.
additionalProperties
{"type":"array","items":{"type":"string"}}
Alternative 2oneOfoptional
Error response envelope for validation and server errors.
Show child properties
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "error": {
        "code": "validation_error",
        "message": "The supplied data or query is invalid.",
        "retryable": false
    },
    "message": "The supplied data or query is invalid.",
    "code": "validation_error"
}
default
{
    "error": {
        "code": "validation_error",
        "message": "The supplied data or query is invalid.",
        "retryable": false
    },
    "message": "The supplied data or query is invalid.",
    "code": "validation_error"
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}
501The field type does not support the operator, or this sort needs an index.
errorobjectrequired
Structured domain refusal with a stable machine code and retry guidance.
Show child properties
codestringrequired
Stable refusal category.
enum
["validation_error","conflict","not_found","rate_limited","quota_exceeded","temporary_failure","permission_denied","not_supported","provider_failure"]
messagestringrequired
Human-readable explanation.
retryablebooleanrequired
Whether repeating unchanged could succeed; still apply operation idempotency rules.
fieldstringoptional
Field key associated with this refusal.
expected_typestringoptional
Expected field type when available.
receivedstringoptional
JSON type/shape description, not the submitted sensitive value.
retry_after_secondsintegeroptional
Suggested wait before retry.
minimum
0
detailsobjectoptional
Additional field errors, quota details or reason information.
additionalProperties
true
messagestringrequired
Compatibility human message from the originating refusal.
codestringrequired
Same category as error.code.
enum
["validation_error","conflict","not_found","rate_limited","quota_exceeded","temporary_failure","permission_denied","not_supported","provider_failure"]
errorsobjectoptional
Optional field messages in Laravel validation shape.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "error": {
        "code": "not_supported",
        "message": "Refused (sort_needs_index): Index the column 'region' to sort on it.",
        "retryable": false,
        "field": "region",
        "details": {
            "reason": "sort_needs_index"
        }
    },
    "message": "Refused (sort_needs_index): Index the column 'region' to sort on it.",
    "code": "not_supported"
}
default
{
    "error": {
        "code": "not_supported",
        "message": "Refused (sort_needs_index): Index the column 'region' to sort on it.",
        "retryable": false,
        "field": "region",
        "details": {
            "reason": "sort_needs_index"
        }
    },
    "message": "Refused (sort_needs_index): Index the column 'region' to sort on it.",
    "code": "not_supported"
}

API REFERENCE / Data tables

Create a record

POST/api/v3/data/tables/{table}/records

Adds one record. Every key in data is validated and coerced through its column's type (a phone becomes E.164, a number becomes a number), required columns must be present, unique columns must not collide, and the whole record must fit in 8 KB. The record is stamped source: "api".

Keys that are not columns of the table are refused, so read the schema first.

AuthenticationTenant API token

Required permission: data.records.edit

Path parameters

tablestringrequired
The table id (from `GET /api/v3/data/tables`). Anything that is not a UUID, or a table belonging to another tenant, answers 404.
format
uuid

Example: 9d2f4c1e-7b8a-4e0f-9a3b-5c6d7e8f9a0b

Request body

application/json · required

dataobjectrequired
The values, keyed by column key. Every required column must be present; other columns may be omitted.
additionalProperties
true
Complete request schema
{
    "type": "object",
    "required": [
        "data"
    ],
    "properties": {
        "data": {
            "type": "object",
            "additionalProperties": true,
            "description": "The values, keyed by column key. Every required column must be present; other columns may be omitted."
        }
    }
}
default
{
    "data": {
        "name": "Asha Mwinyi",
        "phone": "0712345678",
        "region": "dar",
        "opt_in": true,
        "balance": 15000
    }
}

Responses

201Created. The record as stored, values coerced.
recordobjectrequired
The record.
Show child properties
idstringrequired
The record id.
format
uuid
dataobjectrequired
The values, keyed by column key, in column position order. A column with no value is absent or null.
additionalProperties
true
sourcestringrequired
Who created the record, fixed at create time: `ui` for a person in the dashboard, `api` for this API, a flow identifier for a flow. Filter on it with the `$source` system column.
created_atstringrequired
When the record was created — UTC, with microseconds, so a cursor built from it resumes at exactly this row.
format
date-time
updated_atstringrequired
When the record last changed (UTC, microseconds).
format
date-time
titlestringrequired
What names this record: the table's title column, else its first text column, else the id.
titlesobjectoptional
Only on tables with relation columns: the related record's title keyed by the relation column key, resolved once per page so a client never fetches per cell.
additionalProperties
{"type":"string"}
{
    "record": {
        "id": "2c7e1a9b-3d4f-4a5b-8c6d-7e8f9a0b1c2d",
        "data": {
            "name": "Asha Mwinyi",
            "phone": "+255712345678",
            "region": "dar",
            "opt_in": true,
            "balance": 15000
        },
        "source": "api",
        "created_at": "2026-09-08T07:41:12.418206Z",
        "updated_at": "2026-09-08T07:41:12.418206Z",
        "title": "Asha Mwinyi"
    }
}
default
{
    "record": {
        "id": "2c7e1a9b-3d4f-4a5b-8c6d-7e8f9a0b1c2d",
        "data": {
            "name": "Asha Mwinyi",
            "phone": "+255712345678",
            "region": "dar",
            "opt_in": true,
            "balance": 15000
        },
        "source": "api",
        "created_at": "2026-09-08T07:41:12.418206Z",
        "updated_at": "2026-09-08T07:41:12.418206Z",
        "title": "Asha Mwinyi"
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The key issuer or table grant forbids the write, or a state transition requires a permission the caller lacks.
Alternative 1oneOfoptional
Error response envelope for validation and server errors.
Show child properties
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
Alternative 2oneOfoptional
Expected refusal from the data store. Authentication/framework failures can instead use LegacyErrorEnvelope. Quotas are 402; conflicts 409; unsupported operators and sorts needing an index 501.
Show child properties
errorobjectrequired
Structured domain refusal with a stable machine code and retry guidance.
Show child properties
codestringrequired
Stable refusal category.
enum
["validation_error","conflict","not_found","rate_limited","quota_exceeded","temporary_failure","permission_denied","not_supported","provider_failure"]
messagestringrequired
Human-readable explanation.
retryablebooleanrequired
Whether repeating unchanged could succeed; still apply operation idempotency rules.
fieldstringoptional
Field key associated with this refusal.
expected_typestringoptional
Expected field type when available.
receivedstringoptional
JSON type/shape description, not the submitted sensitive value.
retry_after_secondsintegeroptional
Suggested wait before retry.
minimum
0
detailsobjectoptional
Additional field errors, quota details or reason information.
additionalProperties
true
messagestringrequired
Compatibility human message from the originating refusal.
codestringrequired
Same category as error.code.
enum
["validation_error","conflict","not_found","rate_limited","quota_exceeded","temporary_failure","permission_denied","not_supported","provider_failure"]
errorsobjectoptional
Optional field messages in Laravel validation shape.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "You do not have permission to perform this action."
}
404No table with that id for this tenant (or the id is not a UUID).
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}
default
{
    "status": "error",
    "message": "Resource not found."
}
422Malformed query or invalid record value. Missing request data may use the standard v3 envelope; expected data refusals use DataError.
Alternative 1oneOfoptional
Expected refusal from the data store. Authentication/framework failures can instead use LegacyErrorEnvelope. Quotas are 402; conflicts 409; unsupported operators and sorts needing an index 501.
Show child properties
errorobjectrequired
Structured domain refusal with a stable machine code and retry guidance.
Show child properties
codestringrequired
Stable refusal category.
enum
["validation_error","conflict","not_found","rate_limited","quota_exceeded","temporary_failure","permission_denied","not_supported","provider_failure"]
messagestringrequired
Human-readable explanation.
retryablebooleanrequired
Whether repeating unchanged could succeed; still apply operation idempotency rules.
fieldstringoptional
Field key associated with this refusal.
expected_typestringoptional
Expected field type when available.
receivedstringoptional
JSON type/shape description, not the submitted sensitive value.
retry_after_secondsintegeroptional
Suggested wait before retry.
minimum
0
detailsobjectoptional
Additional field errors, quota details or reason information.
additionalProperties
true
messagestringrequired
Compatibility human message from the originating refusal.
codestringrequired
Same category as error.code.
enum
["validation_error","conflict","not_found","rate_limited","quota_exceeded","temporary_failure","permission_denied","not_supported","provider_failure"]
errorsobjectoptional
Optional field messages in Laravel validation shape.
additionalProperties
{"type":"array","items":{"type":"string"}}
Alternative 2oneOfoptional
Error response envelope for validation and server errors.
Show child properties
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "error": {
        "code": "validation_error",
        "message": "The supplied data or query is invalid.",
        "retryable": false
    },
    "message": "The supplied data or query is invalid.",
    "code": "validation_error"
}
default
{
    "error": {
        "code": "validation_error",
        "message": "The supplied data or query is invalid.",
        "retryable": false
    },
    "message": "The supplied data or query is invalid.",
    "code": "validation_error"
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}
402Table/account quota prevents storage; error.details names the quota and allowance.
errorobjectrequired
Structured domain refusal with a stable machine code and retry guidance.
Show child properties
codestringrequired
Stable refusal category.
enum
["validation_error","conflict","not_found","rate_limited","quota_exceeded","temporary_failure","permission_denied","not_supported","provider_failure"]
messagestringrequired
Human-readable explanation.
retryablebooleanrequired
Whether repeating unchanged could succeed; still apply operation idempotency rules.
fieldstringoptional
Field key associated with this refusal.
expected_typestringoptional
Expected field type when available.
receivedstringoptional
JSON type/shape description, not the submitted sensitive value.
retry_after_secondsintegeroptional
Suggested wait before retry.
minimum
0
detailsobjectoptional
Additional field errors, quota details or reason information.
additionalProperties
true
messagestringrequired
Compatibility human message from the originating refusal.
codestringrequired
Same category as error.code.
enum
["validation_error","conflict","not_found","rate_limited","quota_exceeded","temporary_failure","permission_denied","not_supported","provider_failure"]
errorsobjectoptional
Optional field messages in Laravel validation shape.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "error": {
        "code": "quota_exceeded",
        "message": "The table has reached its record allowance.",
        "retryable": false,
        "details": {
            "quota": "quota_records",
            "limit": 500000,
            "used": 500000
        }
    },
    "message": "The table has reached its record allowance.",
    "code": "quota_exceeded"
}
default
{
    "error": {
        "code": "quota_exceeded",
        "message": "The table has reached its record allowance.",
        "retryable": false,
        "details": {
            "quota": "quota_records",
            "limit": 500000,
            "used": 500000
        }
    },
    "message": "The table has reached its record allowance.",
    "code": "quota_exceeded"
}
409A unique-value or status-transition conflict prevented the write.
errorobjectrequired
Structured domain refusal with a stable machine code and retry guidance.
Show child properties
codestringrequired
Stable refusal category.
enum
["validation_error","conflict","not_found","rate_limited","quota_exceeded","temporary_failure","permission_denied","not_supported","provider_failure"]
messagestringrequired
Human-readable explanation.
retryablebooleanrequired
Whether repeating unchanged could succeed; still apply operation idempotency rules.
fieldstringoptional
Field key associated with this refusal.
expected_typestringoptional
Expected field type when available.
receivedstringoptional
JSON type/shape description, not the submitted sensitive value.
retry_after_secondsintegeroptional
Suggested wait before retry.
minimum
0
detailsobjectoptional
Additional field errors, quota details or reason information.
additionalProperties
true
messagestringrequired
Compatibility human message from the originating refusal.
codestringrequired
Same category as error.code.
enum
["validation_error","conflict","not_found","rate_limited","quota_exceeded","temporary_failure","permission_denied","not_supported","provider_failure"]
errorsobjectoptional
Optional field messages in Laravel validation shape.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "error": {
        "code": "conflict",
        "message": "The record was refused. A unique value or state transition conflicts with current data.",
        "retryable": false
    },
    "message": "The record was refused. A unique value or state transition conflicts with current data.",
    "code": "conflict"
}
default
{
    "error": {
        "code": "conflict",
        "message": "The record was refused. A unique value or state transition conflicts with current data.",
        "retryable": false
    },
    "message": "The record was refused. A unique value or state transition conflicts with current data.",
    "code": "conflict"
}

API REFERENCE / Data tables

Read a record

GET/api/v3/data/tables/{table}/records/{record}

One record by id. A deleted record is a 404.

AuthenticationTenant API token

Required permission: data.view

Path parameters

tablestringrequired
The table id (from `GET /api/v3/data/tables`). Anything that is not a UUID, or a table belonging to another tenant, answers 404.
format
uuid

Example: 9d2f4c1e-7b8a-4e0f-9a3b-5c6d7e8f9a0b

recordstringrequired
The record id (from a records page, or the `record.id` returned when it was created).
format
uuid

Example: 2c7e1a9b-3d4f-4a5b-8c6d-7e8f9a0b1c2d

Responses

200The record.
recordobjectrequired
The record.
Show child properties
idstringrequired
The record id.
format
uuid
dataobjectrequired
The values, keyed by column key, in column position order. A column with no value is absent or null.
additionalProperties
true
sourcestringrequired
Who created the record, fixed at create time: `ui` for a person in the dashboard, `api` for this API, a flow identifier for a flow. Filter on it with the `$source` system column.
created_atstringrequired
When the record was created — UTC, with microseconds, so a cursor built from it resumes at exactly this row.
format
date-time
updated_atstringrequired
When the record last changed (UTC, microseconds).
format
date-time
titlestringrequired
What names this record: the table's title column, else its first text column, else the id.
titlesobjectoptional
Only on tables with relation columns: the related record's title keyed by the relation column key, resolved once per page so a client never fetches per cell.
additionalProperties
{"type":"string"}
{
    "record": {
        "id": "2c7e1a9b-3d4f-4a5b-8c6d-7e8f9a0b1c2d",
        "data": {
            "name": "Asha Mwinyi",
            "phone": "+255712345678",
            "region": "dar",
            "opt_in": true,
            "balance": 15000
        },
        "source": "api",
        "created_at": "2026-09-08T07:41:12.418206Z",
        "updated_at": "2026-09-08T07:41:12.418206Z",
        "title": "Asha Mwinyi"
    }
}
default
{
    "record": {
        "id": "2c7e1a9b-3d4f-4a5b-8c6d-7e8f9a0b1c2d",
        "data": {
            "name": "Asha Mwinyi",
            "phone": "+255712345678",
            "region": "dar",
            "opt_in": true,
            "balance": 15000
        },
        "source": "api",
        "created_at": "2026-09-08T07:41:12.418206Z",
        "updated_at": "2026-09-08T07:41:12.418206Z",
        "title": "Asha Mwinyi"
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The key's issuer does not hold data.view, or the key has no issuer on record.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This API key was issued by a user without the \"data.view\" permission."
}
default
{
    "status": "error",
    "message": "This API key was issued by a user without the \"data.view\" permission."
}
404Unknown/hidden table uses the standard v3 envelope; unknown or deleted record uses the data error envelope.
Alternative 1oneOfoptional
Error response envelope for validation and server errors.
Show child properties
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
Alternative 2oneOfoptional
Expected refusal from the data store. Authentication/framework failures can instead use LegacyErrorEnvelope. Quotas are 402; conflicts 409; unsupported operators and sorts needing an index 501.
Show child properties
errorobjectrequired
Structured domain refusal with a stable machine code and retry guidance.
Show child properties
codestringrequired
Stable refusal category.
enum
["validation_error","conflict","not_found","rate_limited","quota_exceeded","temporary_failure","permission_denied","not_supported","provider_failure"]
messagestringrequired
Human-readable explanation.
retryablebooleanrequired
Whether repeating unchanged could succeed; still apply operation idempotency rules.
fieldstringoptional
Field key associated with this refusal.
expected_typestringoptional
Expected field type when available.
receivedstringoptional
JSON type/shape description, not the submitted sensitive value.
retry_after_secondsintegeroptional
Suggested wait before retry.
minimum
0
detailsobjectoptional
Additional field errors, quota details or reason information.
additionalProperties
true
messagestringrequired
Compatibility human message from the originating refusal.
codestringrequired
Same category as error.code.
enum
["validation_error","conflict","not_found","rate_limited","quota_exceeded","temporary_failure","permission_denied","not_supported","provider_failure"]
errorsobjectoptional
Optional field messages in Laravel validation shape.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "error": {
        "code": "not_found",
        "message": "No such record in this table.",
        "retryable": false,
        "field": "$id"
    },
    "message": "No such record in this table.",
    "code": "not_found"
}
default
{
    "error": {
        "code": "not_found",
        "message": "No such record in this table.",
        "retryable": false,
        "field": "$id"
    },
    "message": "No such record in this table.",
    "code": "not_found"
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Data tables

Update a record

PATCH/api/v3/data/tables/{table}/records/{record}

Changes only the keys you send; everything else keeps its value. A key set to null is cleared — unless the column is required, which answers 422. Values go through the same validation and coercion as a create. source is fixed at create and does not change here.

AuthenticationTenant API token

Required permission: data.records.edit

Path parameters

tablestringrequired
The table id (from `GET /api/v3/data/tables`). Anything that is not a UUID, or a table belonging to another tenant, answers 404.
format
uuid

Example: 9d2f4c1e-7b8a-4e0f-9a3b-5c6d7e8f9a0b

recordstringrequired
The record id (from a records page, or the `record.id` returned when it was created).
format
uuid

Example: 2c7e1a9b-3d4f-4a5b-8c6d-7e8f9a0b1c2d

Request body

application/json · required

dataobjectrequired
Only the keys to change. `null` clears a key.
additionalProperties
true
Complete request schema
{
    "type": "object",
    "required": [
        "data"
    ],
    "properties": {
        "data": {
            "type": "object",
            "additionalProperties": true,
            "description": "Only the keys to change. `null` clears a key."
        }
    }
}
default
{
    "data": {
        "region": "arusha",
        "balance": 12500
    }
}

Responses

200Updated. The whole record as it now stands.
recordobjectrequired
The record.
Show child properties
idstringrequired
The record id.
format
uuid
dataobjectrequired
The values, keyed by column key, in column position order. A column with no value is absent or null.
additionalProperties
true
sourcestringrequired
Who created the record, fixed at create time: `ui` for a person in the dashboard, `api` for this API, a flow identifier for a flow. Filter on it with the `$source` system column.
created_atstringrequired
When the record was created — UTC, with microseconds, so a cursor built from it resumes at exactly this row.
format
date-time
updated_atstringrequired
When the record last changed (UTC, microseconds).
format
date-time
titlestringrequired
What names this record: the table's title column, else its first text column, else the id.
titlesobjectoptional
Only on tables with relation columns: the related record's title keyed by the relation column key, resolved once per page so a client never fetches per cell.
additionalProperties
{"type":"string"}
{
    "record": {
        "id": "2c7e1a9b-3d4f-4a5b-8c6d-7e8f9a0b1c2d",
        "data": {
            "name": "Asha Mwinyi",
            "phone": "+255712345678",
            "region": "arusha",
            "opt_in": true,
            "balance": 12500
        },
        "source": "api",
        "created_at": "2026-09-08T07:41:12.418206Z",
        "updated_at": "2026-09-08T09:12:40.006511Z",
        "title": "Asha Mwinyi"
    }
}
default
{
    "record": {
        "id": "2c7e1a9b-3d4f-4a5b-8c6d-7e8f9a0b1c2d",
        "data": {
            "name": "Asha Mwinyi",
            "phone": "+255712345678",
            "region": "arusha",
            "opt_in": true,
            "balance": 12500
        },
        "source": "api",
        "created_at": "2026-09-08T07:41:12.418206Z",
        "updated_at": "2026-09-08T09:12:40.006511Z",
        "title": "Asha Mwinyi"
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The key issuer or table grant forbids the write, or a state transition requires a permission the caller lacks.
Alternative 1oneOfoptional
Error response envelope for validation and server errors.
Show child properties
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
Alternative 2oneOfoptional
Expected refusal from the data store. Authentication/framework failures can instead use LegacyErrorEnvelope. Quotas are 402; conflicts 409; unsupported operators and sorts needing an index 501.
Show child properties
errorobjectrequired
Structured domain refusal with a stable machine code and retry guidance.
Show child properties
codestringrequired
Stable refusal category.
enum
["validation_error","conflict","not_found","rate_limited","quota_exceeded","temporary_failure","permission_denied","not_supported","provider_failure"]
messagestringrequired
Human-readable explanation.
retryablebooleanrequired
Whether repeating unchanged could succeed; still apply operation idempotency rules.
fieldstringoptional
Field key associated with this refusal.
expected_typestringoptional
Expected field type when available.
receivedstringoptional
JSON type/shape description, not the submitted sensitive value.
retry_after_secondsintegeroptional
Suggested wait before retry.
minimum
0
detailsobjectoptional
Additional field errors, quota details or reason information.
additionalProperties
true
messagestringrequired
Compatibility human message from the originating refusal.
codestringrequired
Same category as error.code.
enum
["validation_error","conflict","not_found","rate_limited","quota_exceeded","temporary_failure","permission_denied","not_supported","provider_failure"]
errorsobjectoptional
Optional field messages in Laravel validation shape.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "You do not have permission to perform this action."
}
404Unknown/hidden table uses the standard v3 envelope; unknown or deleted record uses the data error envelope.
Alternative 1oneOfoptional
Error response envelope for validation and server errors.
Show child properties
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
Alternative 2oneOfoptional
Expected refusal from the data store. Authentication/framework failures can instead use LegacyErrorEnvelope. Quotas are 402; conflicts 409; unsupported operators and sorts needing an index 501.
Show child properties
errorobjectrequired
Structured domain refusal with a stable machine code and retry guidance.
Show child properties
codestringrequired
Stable refusal category.
enum
["validation_error","conflict","not_found","rate_limited","quota_exceeded","temporary_failure","permission_denied","not_supported","provider_failure"]
messagestringrequired
Human-readable explanation.
retryablebooleanrequired
Whether repeating unchanged could succeed; still apply operation idempotency rules.
fieldstringoptional
Field key associated with this refusal.
expected_typestringoptional
Expected field type when available.
receivedstringoptional
JSON type/shape description, not the submitted sensitive value.
retry_after_secondsintegeroptional
Suggested wait before retry.
minimum
0
detailsobjectoptional
Additional field errors, quota details or reason information.
additionalProperties
true
messagestringrequired
Compatibility human message from the originating refusal.
codestringrequired
Same category as error.code.
enum
["validation_error","conflict","not_found","rate_limited","quota_exceeded","temporary_failure","permission_denied","not_supported","provider_failure"]
errorsobjectoptional
Optional field messages in Laravel validation shape.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "error": {
        "code": "not_found",
        "message": "No such record in this table.",
        "retryable": false,
        "field": "$id"
    },
    "message": "No such record in this table.",
    "code": "not_found"
}
default
{
    "error": {
        "code": "not_found",
        "message": "No such record in this table.",
        "retryable": false,
        "field": "$id"
    },
    "message": "No such record in this table.",
    "code": "not_found"
}
422Malformed query or invalid record value. Missing request data may use the standard v3 envelope; expected data refusals use DataError.
Alternative 1oneOfoptional
Expected refusal from the data store. Authentication/framework failures can instead use LegacyErrorEnvelope. Quotas are 402; conflicts 409; unsupported operators and sorts needing an index 501.
Show child properties
errorobjectrequired
Structured domain refusal with a stable machine code and retry guidance.
Show child properties
codestringrequired
Stable refusal category.
enum
["validation_error","conflict","not_found","rate_limited","quota_exceeded","temporary_failure","permission_denied","not_supported","provider_failure"]
messagestringrequired
Human-readable explanation.
retryablebooleanrequired
Whether repeating unchanged could succeed; still apply operation idempotency rules.
fieldstringoptional
Field key associated with this refusal.
expected_typestringoptional
Expected field type when available.
receivedstringoptional
JSON type/shape description, not the submitted sensitive value.
retry_after_secondsintegeroptional
Suggested wait before retry.
minimum
0
detailsobjectoptional
Additional field errors, quota details or reason information.
additionalProperties
true
messagestringrequired
Compatibility human message from the originating refusal.
codestringrequired
Same category as error.code.
enum
["validation_error","conflict","not_found","rate_limited","quota_exceeded","temporary_failure","permission_denied","not_supported","provider_failure"]
errorsobjectoptional
Optional field messages in Laravel validation shape.
additionalProperties
{"type":"array","items":{"type":"string"}}
Alternative 2oneOfoptional
Error response envelope for validation and server errors.
Show child properties
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "error": {
        "code": "validation_error",
        "message": "The supplied data or query is invalid.",
        "retryable": false
    },
    "message": "The supplied data or query is invalid.",
    "code": "validation_error"
}
default
{
    "error": {
        "code": "validation_error",
        "message": "The supplied data or query is invalid.",
        "retryable": false
    },
    "message": "The supplied data or query is invalid.",
    "code": "validation_error"
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}
402Table/account quota prevents storage; error.details names the quota and allowance.
errorobjectrequired
Structured domain refusal with a stable machine code and retry guidance.
Show child properties
codestringrequired
Stable refusal category.
enum
["validation_error","conflict","not_found","rate_limited","quota_exceeded","temporary_failure","permission_denied","not_supported","provider_failure"]
messagestringrequired
Human-readable explanation.
retryablebooleanrequired
Whether repeating unchanged could succeed; still apply operation idempotency rules.
fieldstringoptional
Field key associated with this refusal.
expected_typestringoptional
Expected field type when available.
receivedstringoptional
JSON type/shape description, not the submitted sensitive value.
retry_after_secondsintegeroptional
Suggested wait before retry.
minimum
0
detailsobjectoptional
Additional field errors, quota details or reason information.
additionalProperties
true
messagestringrequired
Compatibility human message from the originating refusal.
codestringrequired
Same category as error.code.
enum
["validation_error","conflict","not_found","rate_limited","quota_exceeded","temporary_failure","permission_denied","not_supported","provider_failure"]
errorsobjectoptional
Optional field messages in Laravel validation shape.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "error": {
        "code": "quota_exceeded",
        "message": "The table has reached its record allowance.",
        "retryable": false,
        "details": {
            "quota": "quota_records",
            "limit": 500000,
            "used": 500000
        }
    },
    "message": "The table has reached its record allowance.",
    "code": "quota_exceeded"
}
default
{
    "error": {
        "code": "quota_exceeded",
        "message": "The table has reached its record allowance.",
        "retryable": false,
        "details": {
            "quota": "quota_records",
            "limit": 500000,
            "used": 500000
        }
    },
    "message": "The table has reached its record allowance.",
    "code": "quota_exceeded"
}
409A unique-value or status-transition conflict prevented the write.
errorobjectrequired
Structured domain refusal with a stable machine code and retry guidance.
Show child properties
codestringrequired
Stable refusal category.
enum
["validation_error","conflict","not_found","rate_limited","quota_exceeded","temporary_failure","permission_denied","not_supported","provider_failure"]
messagestringrequired
Human-readable explanation.
retryablebooleanrequired
Whether repeating unchanged could succeed; still apply operation idempotency rules.
fieldstringoptional
Field key associated with this refusal.
expected_typestringoptional
Expected field type when available.
receivedstringoptional
JSON type/shape description, not the submitted sensitive value.
retry_after_secondsintegeroptional
Suggested wait before retry.
minimum
0
detailsobjectoptional
Additional field errors, quota details or reason information.
additionalProperties
true
messagestringrequired
Compatibility human message from the originating refusal.
codestringrequired
Same category as error.code.
enum
["validation_error","conflict","not_found","rate_limited","quota_exceeded","temporary_failure","permission_denied","not_supported","provider_failure"]
errorsobjectoptional
Optional field messages in Laravel validation shape.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "error": {
        "code": "conflict",
        "message": "The record was refused. A unique value or state transition conflicts with current data.",
        "retryable": false
    },
    "message": "The record was refused. A unique value or state transition conflicts with current data.",
    "code": "conflict"
}
default
{
    "error": {
        "code": "conflict",
        "message": "The record was refused. A unique value or state transition conflicts with current data.",
        "retryable": false
    },
    "message": "The record was refused. A unique value or state transition conflicts with current data.",
    "code": "conflict"
}

API REFERENCE / Data tables

Delete a record

DELETE/api/v3/data/tables/{table}/records/{record}

Soft-deletes one record: it leaves every list and read from now on and stops counting against the records quota. Deleting it twice is a 404.

AuthenticationTenant API token

Required permission: data.records.edit

Path parameters

tablestringrequired
The table id (from `GET /api/v3/data/tables`). Anything that is not a UUID, or a table belonging to another tenant, answers 404.
format
uuid

Example: 9d2f4c1e-7b8a-4e0f-9a3b-5c6d7e8f9a0b

recordstringrequired
The record id (from a records page, or the `record.id` returned when it was created).
format
uuid

Example: 2c7e1a9b-3d4f-4a5b-8c6d-7e8f9a0b1c2d

Responses

200Deleted.
okbooleanrequired
Always true on success.
enum
[true]
deletedintegerrequired
How many records were deleted — always 1 here.
enum
[1]
{
    "ok": true,
    "deleted": 1
}
default
{
    "ok": true,
    "deleted": 1
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The key's issuer does not hold data.records.edit, or the key has no issuer on record.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This API key was issued by a user without the \"data.records.edit\" permission."
}
default
{
    "status": "error",
    "message": "This API key was issued by a user without the \"data.records.edit\" permission."
}
404Unknown/hidden table uses the standard v3 envelope; unknown or deleted record uses the data error envelope.
Alternative 1oneOfoptional
Error response envelope for validation and server errors.
Show child properties
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
Alternative 2oneOfoptional
Expected refusal from the data store. Authentication/framework failures can instead use LegacyErrorEnvelope. Quotas are 402; conflicts 409; unsupported operators and sorts needing an index 501.
Show child properties
errorobjectrequired
Structured domain refusal with a stable machine code and retry guidance.
Show child properties
codestringrequired
Stable refusal category.
enum
["validation_error","conflict","not_found","rate_limited","quota_exceeded","temporary_failure","permission_denied","not_supported","provider_failure"]
messagestringrequired
Human-readable explanation.
retryablebooleanrequired
Whether repeating unchanged could succeed; still apply operation idempotency rules.
fieldstringoptional
Field key associated with this refusal.
expected_typestringoptional
Expected field type when available.
receivedstringoptional
JSON type/shape description, not the submitted sensitive value.
retry_after_secondsintegeroptional
Suggested wait before retry.
minimum
0
detailsobjectoptional
Additional field errors, quota details or reason information.
additionalProperties
true
messagestringrequired
Compatibility human message from the originating refusal.
codestringrequired
Same category as error.code.
enum
["validation_error","conflict","not_found","rate_limited","quota_exceeded","temporary_failure","permission_denied","not_supported","provider_failure"]
errorsobjectoptional
Optional field messages in Laravel validation shape.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "error": {
        "code": "not_found",
        "message": "No such record in this table.",
        "retryable": false,
        "field": "$id"
    },
    "message": "No such record in this table.",
    "code": "not_found"
}
default
{
    "error": {
        "code": "not_found",
        "message": "No such record in this table.",
        "retryable": false,
        "field": "$id"
    },
    "message": "No such record in this table.",
    "code": "not_found"
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Data tables

Read a record's history

GET/api/v3/data/tables/{table}/records/{record}/history

Every change made to one record, newest first: what was created, updated or deleted, which fields moved and from what to what, who did it and through which surface (the web app, an API key, an MCP connection, a message flow, an IVR call or a schedule), and the reason when one was given. Pages 50 at a time; pass the timestamp returned as next_before back as the before query parameter to read the page after it. A record nobody has changed answers with an empty list, not a 404.

AuthenticationTenant API token

Required permission: data.view

Path parameters

tablestringrequired
The table id (from `GET /api/v3/data/tables`). Anything that is not a UUID, or a table belonging to another tenant, answers 404.
format
uuid

Example: 9d2f4c1e-7b8a-4e0f-9a3b-5c6d7e8f9a0b

recordstringrequired
The record id (from a records page, or the `record.id` returned when it was created).
format
uuid

Example: 2c7e1a9b-3d4f-4a5b-8c6d-7e8f9a0b1c2d

Query parameters

beforestringoptional
Read the page older than this timestamp — the next_before value from the previous page.
format
date-time

Example: 2026-09-08 14:31:07.812345+03:00

Responses

200The record's history.
historyarray<object>required
The changes, newest first.
Show child properties
idstringoptional
The history entry's id.
format
uuid
actionstringoptional
What happened to the record.
enum
["create","update","delete","restore","bulk_delete"]
changesobjectoptional
Per field, the value before and after. A create lists every field from null; a delete lists every field to null.
additionalProperties
true
actorobjectoptional
Who made the change: kind (user, api, mcp, flow, ivr, schedule, system), id and a label.
additionalProperties
true
sourcestringoptional
The surface the write came through.
reasonstringoptional
Why, when the caller gave a reason.
nullable
true
created_atstringoptional
When the change was made.
format
date-time
record_idstringoptional
Record UUID described by this audit entry.
format
uuid
changedarray<string>optional
Keys changed in this entry.
has_morebooleanrequired
Whether an older page exists.
next_beforestringoptional
Pass back as before to read the next page.
format
date-time
nullable
true
columnsobjectoptional
Field key to its label, so a change can be shown with the field's name.
additionalProperties
true
{
    "history": [
        {
            "id": "0192f3c4-5a6b-7c8d-9e0f-1a2b3c4d5e6f",
            "action": "update",
            "changes": {
                "status": {
                    "from": "pending",
                    "to": "paid"
                }
            },
            "actor": {
                "kind": "mcp",
                "id": 41,
                "label": "Claude"
            },
            "source": "mcp",
            "reason": null,
            "created_at": "2026-09-08T14:31:07.812345+03:00"
        }
    ],
    "has_more": false,
    "next_before": null,
    "columns": {
        "status": "Status"
    }
}
default
{
    "history": [
        {
            "id": "0192f3c4-5a6b-7c8d-9e0f-1a2b3c4d5e6f",
            "action": "update",
            "changes": {
                "status": {
                    "from": "pending",
                    "to": "paid"
                }
            },
            "actor": {
                "kind": "mcp",
                "id": 41,
                "label": "Claude"
            },
            "source": "mcp",
            "reason": null,
            "created_at": "2026-09-08T14:31:07.812345+03:00"
        }
    ],
    "has_more": false,
    "next_before": null,
    "columns": {
        "status": "Status"
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The key's issuer does not hold data.view, or the key has no issuer on record.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This API key was issued by a user without the \"data.view\" permission."
}
default
{
    "status": "error",
    "message": "This API key was issued by a user without the \"data.view\" permission."
}
404Invalid UUID or unknown/hidden table. A valid record UUID with no history returns an empty history list, including after deletion.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}
default
{
    "status": "error",
    "message": "Resource not found."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Data tables

List table groups

GET/api/v3/data/groups

Every group this tenant has defined, in display order. A group is a named folder of related tables (customers, orders, payments) with a report layer across them; a table belongs to at most one group.

AuthenticationTenant API token

Required permission: data.view

Responses

200The groups.
groupsarray<object>required
The groups, in display order (position, then name).
Show child properties
idstringrequired
The group id; the `{group}` path parameter everywhere else.
format
uuid
namestringrequired
Display name.
slugstringrequired
URL-safe name, unique within the tenant.
descriptionstring | nulloptional
What the group holds.
iconstring | nulloptional
An emoji shown before the name, or null.
colorstring | nulloptional
One of the select-option palette keys (gray, red, orange, amber, yellow, lime, green, emerald, teal, cyan, sky, blue, indigo, violet, purple, fuchsia, pink, rose), or null.
positionintegerrequired
Order among the tenant's groups, first = 0.
tables_countintegerrequired
Member tables.
records_countintegerrequired
Live records across the member tables.
created_atstring | nulloptional
When it was created (ISO-8601).
format
date-time
updated_atstring | nulloptional
When it last changed (ISO-8601).
format
date-time
{
    "groups": [
        {
            "id": "7c1e2d3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
            "name": "Mauzo",
            "slug": "mauzo",
            "description": "Wateja na oda zao.",
            "icon": "\ud83d\uded2",
            "color": "amber",
            "position": 0,
            "tables_count": 2,
            "records_count": 61234,
            "created_at": "2026-09-08T09:00:00+00:00",
            "updated_at": "2026-09-08T09:00:00+00:00"
        }
    ]
}
default
{
    "groups": [
        {
            "id": "7c1e2d3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
            "name": "Mauzo",
            "slug": "mauzo",
            "description": "Wateja na oda zao.",
            "icon": "\ud83d\uded2",
            "color": "amber",
            "position": 0,
            "tables_count": 2,
            "records_count": 61234,
            "created_at": "2026-09-08T09:00:00+00:00",
            "updated_at": "2026-09-08T09:00:00+00:00"
        }
    ]
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The key's issuer does not hold data.view, or the key has no issuer on record.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This API key was issued by a user without the \"data.view\" permission."
}
default
{
    "status": "error",
    "message": "This API key was issued by a user without the \"data.view\" permission."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Data tables

Read a group

GET/api/v3/data/groups/{group}

The group, its member tables (each with its columns, headline total and records created in the last 30 days) and its saved cross-table reports.

AuthenticationTenant API token

Required permission: data.view

Path parameters

groupstringrequired
The group id (from `GET /api/v3/data/groups`). Anything that is not a UUID, or a group belonging to another tenant, answers 404.
format
uuid

Example: 7c1e2d3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f

Responses

200The group.
groupobjectrequired
The group.
Show child properties
idstringrequired
The group id; the `{group}` path parameter everywhere else.
format
uuid
namestringrequired
Display name.
slugstringrequired
URL-safe name, unique within the tenant.
descriptionstring | nulloptional
What the group holds.
iconstring | nulloptional
An emoji shown before the name, or null.
colorstring | nulloptional
One of the select-option palette keys (gray, red, orange, amber, yellow, lime, green, emerald, teal, cyan, sky, blue, indigo, violet, purple, fuchsia, pink, rose), or null.
positionintegerrequired
Order among the tenant's groups, first = 0.
tables_countintegerrequired
Member tables.
records_countintegerrequired
Live records across the member tables.
created_atstring | nulloptional
When it was created (ISO-8601).
format
date-time
updated_atstring | nulloptional
When it last changed (ISO-8601).
format
date-time
tablesarray<object>required
Member tables in the group's order, each with its columns and its last-30-days card.
Show child properties
idstringrequired
The id.
format
uuid
namestringrequired
Display name.
slugstringrequired
URL-safe name, unique within the tenant.
iconstring | nulloptional
An emoji shown before the name, or null.
records_countintegerrequired
Live records in the table.
columns_countintegerrequired
Columns defined on the table.
headlineobject | nullrequired
The first amount-like column's total over the range, or null when the table has none.
Show child properties
labelstringoptional
Human label.
fnstringoptional
The aggregate.
enum
["sum"]
columnstringoptional
The column key.
valuenumber | nulloptional
The computed value, or null when nothing matched.
unitstring | nulloptional
The column's unit (e.g. TZS), or null.
created_last_rangeintegerrequired
Records created inside the range.
drillobjectrequired
How to open the rows behind the number.
Show child properties
table_idstringrequired
The table id.
format
uuid
filterobjectrequired
A condition tree for the records endpoint.
Show child properties
allarray<object>optional
Every child must match (AND).
Show child properties
allarray<object>optional
Every child must match (AND).
Show child properties
allarray<object>optional
Every child must match (AND).
Show child properties
allarray<object>optional
Every child must match (AND).
Show child properties
allarray<object>optional
Every child must match (AND).
anyarray<object>optional
At least one child must match (OR).
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
anyarray<object>optional
At least one child must match (OR).
Show child properties
allarray<object>optional
Every child must match (AND).
anyarray<object>optional
At least one child must match (OR).
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
anyarray<object>optional
At least one child must match (OR).
Show child properties
allarray<object>optional
Every child must match (AND).
Show child properties
allarray<object>optional
Every child must match (AND).
anyarray<object>optional
At least one child must match (OR).
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
anyarray<object>optional
At least one child must match (OR).
Show child properties
allarray<object>optional
Every child must match (AND).
anyarray<object>optional
At least one child must match (OR).
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
anyarray<object>optional
At least one child must match (OR).
Show child properties
allarray<object>optional
Every child must match (AND).
Show child properties
allarray<object>optional
Every child must match (AND).
Show child properties
allarray<object>optional
Every child must match (AND).
anyarray<object>optional
At least one child must match (OR).
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
anyarray<object>optional
At least one child must match (OR).
Show child properties
allarray<object>optional
Every child must match (AND).
anyarray<object>optional
At least one child must match (OR).
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
anyarray<object>optional
At least one child must match (OR).
Show child properties
allarray<object>optional
Every child must match (AND).
Show child properties
allarray<object>optional
Every child must match (AND).
anyarray<object>optional
At least one child must match (OR).
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
anyarray<object>optional
At least one child must match (OR).
Show child properties
allarray<object>optional
Every child must match (AND).
anyarray<object>optional
At least one child must match (OR).
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
anyarray<object>optional
At least one child must match (OR).
Show child properties
allarray<object>optional
Every child must match (AND).
Show child properties
allarray<object>optional
Every child must match (AND).
Show child properties
allarray<object>optional
Every child must match (AND).
Show child properties
allarray<object>optional
Every child must match (AND).
anyarray<object>optional
At least one child must match (OR).
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
anyarray<object>optional
At least one child must match (OR).
Show child properties
allarray<object>optional
Every child must match (AND).
anyarray<object>optional
At least one child must match (OR).
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
anyarray<object>optional
At least one child must match (OR).
Show child properties
allarray<object>optional
Every child must match (AND).
Show child properties
allarray<object>optional
Every child must match (AND).
anyarray<object>optional
At least one child must match (OR).
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
anyarray<object>optional
At least one child must match (OR).
Show child properties
allarray<object>optional
Every child must match (AND).
anyarray<object>optional
At least one child must match (OR).
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
anyarray<object>optional
At least one child must match (OR).
Show child properties
allarray<object>optional
Every child must match (AND).
Show child properties
allarray<object>optional
Every child must match (AND).
Show child properties
allarray<object>optional
Every child must match (AND).
anyarray<object>optional
At least one child must match (OR).
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
anyarray<object>optional
At least one child must match (OR).
Show child properties
allarray<object>optional
Every child must match (AND).
anyarray<object>optional
At least one child must match (OR).
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
anyarray<object>optional
At least one child must match (OR).
Show child properties
allarray<object>optional
Every child must match (AND).
Show child properties
allarray<object>optional
Every child must match (AND).
anyarray<object>optional
At least one child must match (OR).
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
anyarray<object>optional
At least one child must match (OR).
Show child properties
allarray<object>optional
Every child must match (AND).
anyarray<object>optional
At least one child must match (OR).
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
rangeobjectrequired
The half-open [from, to) window.
Show child properties
fromstringrequired
Start of the window (inclusive, ISO-8601).
format
date-time
tostringrequired
End of the window (exclusive, ISO-8601).
format
date-time
columnsarray<object>optional
The table's columns in the schema shape (`GET /groups/{group}` only).
Show child properties
idstringrequired
The column id.
format
uuid
keystringrequired
The key this column has inside a record's `data`, and the `column` to name in a filter or a `sort`.
labelstringrequired
Display label.
typestringrequired
The field type. Its rules, operators and display hints are in `types` on the schema payload. `auto_number` is written by the platform: its `ui.readonly` is true and a value sent for it is refused.
enum
["text","long_text","number","currency","boolean","date","datetime","phone","email","select","multi_select","relation","file","auto_number","unknown"]
stored_typestringoptional
Only when `type` is `unknown`: the type name actually stored, which this version cannot render.
positionintegerrequired
Zero-based column order; record `data` keys come back in this order.
requiredbooleanrequired
A create must supply a value; an update may not clear it.
uniquebooleanrequired
No two live records may share a value. A duplicate answers 422 with `errors`.
indexedbooleanrequired
Whether the column has an index. Sorting a large table on a column needs one — see `sort_index_threshold`.
index_statusstring | nulloptional
State of the latest index job on this column, or null when none was ever requested. Only `ready` makes the column sortable at scale.
enum
["pending","building","ready","failed","dropping",null]
index_errorstring | nulloptional
Why the index build failed, when `index_status` is `failed`.
configobjectoptional
Type-specific settings: `options` for select/multi_select, `table_id` for relation, `default`, `ui` hints, and so on.
additionalProperties
true
rulesarray<string>required
The validation rules a write runs, Laravel-style (`required`, `phone:TZ`, `max:255`, …).
operatorsarray<string>required
The filter operators this column accepts. Any other operator answers 422.
items.enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
uiobjectrequired
Display hints for a grid or form: `cell` and `input` renderer names, `filter` widget, `width` in pixels, plus any of `hidden_in_grid`, `hidden_in_form`, `help_text`, `placeholder`, `is_title_field` set in the dashboard.
additionalProperties
true
Show child properties
cellstringoptional
Renderer for the value in a grid cell.
inputstringoptional
Renderer for the value in a form.
filterstring | nulloptional
Filter widget, or null when the column cannot be filtered.
widthintegeroptional
Suggested column width in pixels.
warningstringoptional
Only when `type` is `unknown`: why the column is read-only.
reportsarray<object>required
Saved cross-table reports, pinned first.
Show child properties
idstringrequired
The id.
format
uuid
group_idstringrequired
The group id.
format
uuid
namestringrequired
Display name.
descriptionstring | nulloptional
One line on what it shows, or null.
definitionobjectrequired
The report definition.
additionalProperties
true
is_pinnedbooleanrequired
Pinned to the top of the Reports tab.
is_defaultbooleanoptional
Always false for a saved report.
created_atstring | nulloptional
When it was created (ISO-8601).
format
date-time
updated_atstring | nulloptional
When it last changed (ISO-8601).
format
date-time
{
    "group": {
        "id": "7c1e2d3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
        "name": "Mauzo",
        "slug": "mauzo",
        "description": "Wateja na oda zao.",
        "icon": "\ud83d\uded2",
        "color": "amber",
        "position": 0,
        "tables_count": 2,
        "records_count": 61234,
        "created_at": "2026-09-08T09:00:00+00:00",
        "updated_at": "2026-09-08T09:00:00+00:00"
    },
    "tables": [
        {
            "id": "9d2f4c1e-7b8a-4e0f-9a3b-5c6d7e8f9a0b",
            "name": "Oda",
            "slug": "orders",
            "icon": "\ud83e\uddfe",
            "records_count": 52014,
            "columns_count": 4,
            "headline": {
                "label": "Total Kiasi (TZS)",
                "fn": "sum",
                "column": "amount",
                "value": 5466022000,
                "unit": "TZS"
            },
            "created_last_range": 812,
            "drill": {
                "table_id": "9d2f4c1e-7b8a-4e0f-9a3b-5c6d7e8f9a0b",
                "filter": {
                    "all": [
                        {
                            "column": "$created_at",
                            "op": "between",
                            "value": [
                                "2026-08-10T00:00:00Z",
                                "2026-09-09T00:00:00Z"
                            ]
                        }
                    ]
                },
                "range": {
                    "from": "2026-08-10T00:00:00Z",
                    "to": "2026-09-09T00:00:00Z"
                }
            },
            "columns": []
        }
    ],
    "reports": [
        {
            "id": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e",
            "group_id": "7c1e2d3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
            "name": "Mauzo kwa wiki",
            "description": null,
            "definition": {
                "series": [
                    {
                        "table_id": "9d2f4c1e-7b8a-4e0f-9a3b-5c6d7e8f9a0b",
                        "metric": {
                            "fn": "sum",
                            "column": "amount"
                        },
                        "filters": null,
                        "label": "Oda"
                    },
                    {
                        "table_id": "4e8d9c0b-1a2f-4b3c-8d4e-5f6a7b8c9d0e",
                        "metric": {
                            "fn": "count"
                        },
                        "filters": null,
                        "label": "Wateja"
                    }
                ],
                "dimension": {
                    "column": "$created_at",
                    "bucket": "week"
                },
                "date_range": {
                    "relative": "last_90_days"
                },
                "chart": "line"
            },
            "is_pinned": true,
            "is_default": false,
            "created_at": "2026-09-08T09:00:00+00:00",
            "updated_at": "2026-09-08T09:00:00+00:00"
        }
    ]
}
default
{
    "group": {
        "id": "7c1e2d3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
        "name": "Mauzo",
        "slug": "mauzo",
        "description": "Wateja na oda zao.",
        "icon": "\ud83d\uded2",
        "color": "amber",
        "position": 0,
        "tables_count": 2,
        "records_count": 61234,
        "created_at": "2026-09-08T09:00:00+00:00",
        "updated_at": "2026-09-08T09:00:00+00:00"
    },
    "tables": [
        {
            "id": "9d2f4c1e-7b8a-4e0f-9a3b-5c6d7e8f9a0b",
            "name": "Oda",
            "slug": "orders",
            "icon": "\ud83e\uddfe",
            "records_count": 52014,
            "columns_count": 4,
            "headline": {
                "label": "Total Kiasi (TZS)",
                "fn": "sum",
                "column": "amount",
                "value": 5466022000,
                "unit": "TZS"
            },
            "created_last_range": 812,
            "drill": {
                "table_id": "9d2f4c1e-7b8a-4e0f-9a3b-5c6d7e8f9a0b",
                "filter": {
                    "all": [
                        {
                            "column": "$created_at",
                            "op": "between",
                            "value": [
                                "2026-08-10T00:00:00Z",
                                "2026-09-09T00:00:00Z"
                            ]
                        }
                    ]
                },
                "range": {
                    "from": "2026-08-10T00:00:00Z",
                    "to": "2026-09-09T00:00:00Z"
                }
            },
            "columns": []
        }
    ],
    "reports": [
        {
            "id": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e",
            "group_id": "7c1e2d3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
            "name": "Mauzo kwa wiki",
            "description": null,
            "definition": {
                "series": [
                    {
                        "table_id": "9d2f4c1e-7b8a-4e0f-9a3b-5c6d7e8f9a0b",
                        "metric": {
                            "fn": "sum",
                            "column": "amount"
                        },
                        "filters": null,
                        "label": "Oda"
                    },
                    {
                        "table_id": "4e8d9c0b-1a2f-4b3c-8d4e-5f6a7b8c9d0e",
                        "metric": {
                            "fn": "count"
                        },
                        "filters": null,
                        "label": "Wateja"
                    }
                ],
                "dimension": {
                    "column": "$created_at",
                    "bucket": "week"
                },
                "date_range": {
                    "relative": "last_90_days"
                },
                "chart": "line"
            },
            "is_pinned": true,
            "is_default": false,
            "created_at": "2026-09-08T09:00:00+00:00",
            "updated_at": "2026-09-08T09:00:00+00:00"
        }
    ]
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The key's issuer does not hold data.view, or the key has no issuer on record.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This API key was issued by a user without the \"data.view\" permission."
}
default
{
    "status": "error",
    "message": "This API key was issued by a user without the \"data.view\" permission."
}
404No group with that id for this tenant (or the id is not a UUID).
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}
default
{
    "status": "error",
    "message": "Resource not found."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Data tables

Read a group's overview

GET/api/v3/data/groups/{group}/overview

Totals, one card per member table, records over time stacked by table, every amount-like column totalled, and the relations between member tables — for a range. Every card carries a drill you can pass to the records endpoint as filter.

AuthenticationTenant API token

Required permission: data.view

Path parameters

groupstringrequired
The group id (from `GET /api/v3/data/groups`). Anything that is not a UUID, or a group belonging to another tenant, answers 404.
format
uuid

Example: 7c1e2d3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f

Query parameters

rangestringoptional
A relative preset (today, yesterday, last_7_days, last_30_days, last_90_days, this_month, last_month) or a JSON period `{"from":"YYYY-MM-DD","to":"YYYY-MM-DD"}`. `from`/`to` query parameters are accepted too.
default
last_30_days

Example: last_30_days

Responses

200The overview.
totalsobjectrequired
Sums across the member tables.
Show child properties
tablesintegerrequired
Member tables.
recordsintegerrequired
Live records across the group.
storage_bytesintegerrequired
JSONB bytes across the group.
tablesarray<object>required
Member tables.
Show child properties
idstringrequired
The id.
format
uuid
namestringrequired
Display name.
slugstringrequired
URL-safe name, unique within the tenant.
iconstring | nulloptional
An emoji shown before the name, or null.
records_countintegerrequired
Live records in the table.
columns_countintegerrequired
Columns defined on the table.
headlineobject | nullrequired
The first amount-like column's total over the range, or null when the table has none.
Show child properties
labelstringoptional
Human label.
fnstringoptional
The aggregate.
enum
["sum"]
columnstringoptional
The column key.
valuenumber | nulloptional
The computed value, or null when nothing matched.
unitstring | nulloptional
The column's unit (e.g. TZS), or null.
created_last_rangeintegerrequired
Records created inside the range.
drillobjectrequired
How to open the rows behind the number.
Show child properties
table_idstringrequired
The table id.
format
uuid
filterobjectrequired
A condition tree for the records endpoint.
Show child properties
allarray<object>optional
Every child must match (AND).
Show child properties
allarray<object>optional
Every child must match (AND).
Show child properties
allarray<object>optional
Every child must match (AND).
Show child properties
allarray<object>optional
Every child must match (AND).
Show child properties
allarray<object>optional
Every child must match (AND).
anyarray<object>optional
At least one child must match (OR).
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
anyarray<object>optional
At least one child must match (OR).
Show child properties
allarray<object>optional
Every child must match (AND).
anyarray<object>optional
At least one child must match (OR).
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
anyarray<object>optional
At least one child must match (OR).
Show child properties
allarray<object>optional
Every child must match (AND).
Show child properties
allarray<object>optional
Every child must match (AND).
anyarray<object>optional
At least one child must match (OR).
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
anyarray<object>optional
At least one child must match (OR).
Show child properties
allarray<object>optional
Every child must match (AND).
anyarray<object>optional
At least one child must match (OR).
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
anyarray<object>optional
At least one child must match (OR).
Show child properties
allarray<object>optional
Every child must match (AND).
Show child properties
allarray<object>optional
Every child must match (AND).
Show child properties
allarray<object>optional
Every child must match (AND).
anyarray<object>optional
At least one child must match (OR).
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
anyarray<object>optional
At least one child must match (OR).
Show child properties
allarray<object>optional
Every child must match (AND).
anyarray<object>optional
At least one child must match (OR).
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
anyarray<object>optional
At least one child must match (OR).
Show child properties
allarray<object>optional
Every child must match (AND).
Show child properties
allarray<object>optional
Every child must match (AND).
anyarray<object>optional
At least one child must match (OR).
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
anyarray<object>optional
At least one child must match (OR).
Show child properties
allarray<object>optional
Every child must match (AND).
anyarray<object>optional
At least one child must match (OR).
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
anyarray<object>optional
At least one child must match (OR).
Show child properties
allarray<object>optional
Every child must match (AND).
Show child properties
allarray<object>optional
Every child must match (AND).
Show child properties
allarray<object>optional
Every child must match (AND).
Show child properties
allarray<object>optional
Every child must match (AND).
anyarray<object>optional
At least one child must match (OR).
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
anyarray<object>optional
At least one child must match (OR).
Show child properties
allarray<object>optional
Every child must match (AND).
anyarray<object>optional
At least one child must match (OR).
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
anyarray<object>optional
At least one child must match (OR).
Show child properties
allarray<object>optional
Every child must match (AND).
Show child properties
allarray<object>optional
Every child must match (AND).
anyarray<object>optional
At least one child must match (OR).
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
anyarray<object>optional
At least one child must match (OR).
Show child properties
allarray<object>optional
Every child must match (AND).
anyarray<object>optional
At least one child must match (OR).
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
anyarray<object>optional
At least one child must match (OR).
Show child properties
allarray<object>optional
Every child must match (AND).
Show child properties
allarray<object>optional
Every child must match (AND).
Show child properties
allarray<object>optional
Every child must match (AND).
anyarray<object>optional
At least one child must match (OR).
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
anyarray<object>optional
At least one child must match (OR).
Show child properties
allarray<object>optional
Every child must match (AND).
anyarray<object>optional
At least one child must match (OR).
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
anyarray<object>optional
At least one child must match (OR).
Show child properties
allarray<object>optional
Every child must match (AND).
Show child properties
allarray<object>optional
Every child must match (AND).
anyarray<object>optional
At least one child must match (OR).
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
anyarray<object>optional
At least one child must match (OR).
Show child properties
allarray<object>optional
Every child must match (AND).
anyarray<object>optional
At least one child must match (OR).
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
rangeobjectrequired
The half-open [from, to) window.
Show child properties
fromstringrequired
Start of the window (inclusive, ISO-8601).
format
date-time
tostringrequired
End of the window (exclusive, ISO-8601).
format
date-time
columnsarray<object>optional
The table's columns in the schema shape (`GET /groups/{group}` only).
Show child properties
idstringrequired
The column id.
format
uuid
keystringrequired
The key this column has inside a record's `data`, and the `column` to name in a filter or a `sort`.
labelstringrequired
Display label.
typestringrequired
The field type. Its rules, operators and display hints are in `types` on the schema payload. `auto_number` is written by the platform: its `ui.readonly` is true and a value sent for it is refused.
enum
["text","long_text","number","currency","boolean","date","datetime","phone","email","select","multi_select","relation","file","auto_number","unknown"]
stored_typestringoptional
Only when `type` is `unknown`: the type name actually stored, which this version cannot render.
positionintegerrequired
Zero-based column order; record `data` keys come back in this order.
requiredbooleanrequired
A create must supply a value; an update may not clear it.
uniquebooleanrequired
No two live records may share a value. A duplicate answers 422 with `errors`.
indexedbooleanrequired
Whether the column has an index. Sorting a large table on a column needs one — see `sort_index_threshold`.
index_statusstring | nulloptional
State of the latest index job on this column, or null when none was ever requested. Only `ready` makes the column sortable at scale.
enum
["pending","building","ready","failed","dropping",null]
index_errorstring | nulloptional
Why the index build failed, when `index_status` is `failed`.
configobjectoptional
Type-specific settings: `options` for select/multi_select, `table_id` for relation, `default`, `ui` hints, and so on.
additionalProperties
true
rulesarray<string>required
The validation rules a write runs, Laravel-style (`required`, `phone:TZ`, `max:255`, …).
operatorsarray<string>required
The filter operators this column accepts. Any other operator answers 422.
items.enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
uiobjectrequired
Display hints for a grid or form: `cell` and `input` renderer names, `filter` widget, `width` in pixels, plus any of `hidden_in_grid`, `hidden_in_form`, `help_text`, `placeholder`, `is_title_field` set in the dashboard.
additionalProperties
true
Show child properties
cellstringoptional
Renderer for the value in a grid cell.
inputstringoptional
Renderer for the value in a form.
filterstring | nulloptional
Filter widget, or null when the column cannot be filtered.
widthintegeroptional
Suggested column width in pixels.
warningstringoptional
Only when `type` is `unknown`: why the column is read-only.
over_timeobjectrequired
Records created per bucket, stacked by table. The bucket follows the range: day up to 31 days, week up to 182, else month.
Show child properties
bucketstringrequired
The bucket start date.
enum
["day","week","month"]
rowsarray<object>required
One row per bucket, oldest first.
Show child properties
bucketstringrequired
The bucket start date.
format
date
totalintegerrequired
Records across every table in the bucket.
by_tableobjectrequired
table id → records created in the bucket.
additionalProperties
{"type":"integer"}
drillobjectoptional
table id → drill for that bucket.
additionalProperties
{"$ref":"#/components/schemas/DataDrill"}
headlinesarray<object>required
Every amount-like number column across the group (a key or label naming money, or `config.ui.is_summary_metric`), totalled over the range.
Show child properties
table_idstringrequired
The table id.
format
uuid
tablestringrequired
The table's display name.
labelstringrequired
Human label.
fnstringrequired
The aggregate.
columnstringrequired
The column key.
valuenumber | nullrequired
The computed value, or null when nothing matched.
unitstring | nullrequired
The column's unit (e.g. TZS), or null.
chartstringrequired
How the card is drawn.
enum
["number"]
drillobjectrequired
How to open the rows behind the number.
Show child properties
table_idstringrequired
The table id.
format
uuid
filterobjectrequired
A condition tree for the records endpoint.
Show child properties
allarray<object>optional
Every child must match (AND).
Show child properties
allarray<object>optional
Every child must match (AND).
Show child properties
allarray<object>optional
Every child must match (AND).
Show child properties
allarray<object>optional
Every child must match (AND).
Show child properties
allarray<object>optional
Every child must match (AND).
anyarray<object>optional
At least one child must match (OR).
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
anyarray<object>optional
At least one child must match (OR).
Show child properties
allarray<object>optional
Every child must match (AND).
anyarray<object>optional
At least one child must match (OR).
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
anyarray<object>optional
At least one child must match (OR).
Show child properties
allarray<object>optional
Every child must match (AND).
Show child properties
allarray<object>optional
Every child must match (AND).
anyarray<object>optional
At least one child must match (OR).
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
anyarray<object>optional
At least one child must match (OR).
Show child properties
allarray<object>optional
Every child must match (AND).
anyarray<object>optional
At least one child must match (OR).
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
anyarray<object>optional
At least one child must match (OR).
Show child properties
allarray<object>optional
Every child must match (AND).
Show child properties
allarray<object>optional
Every child must match (AND).
Show child properties
allarray<object>optional
Every child must match (AND).
anyarray<object>optional
At least one child must match (OR).
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
anyarray<object>optional
At least one child must match (OR).
Show child properties
allarray<object>optional
Every child must match (AND).
anyarray<object>optional
At least one child must match (OR).
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
anyarray<object>optional
At least one child must match (OR).
Show child properties
allarray<object>optional
Every child must match (AND).
Show child properties
allarray<object>optional
Every child must match (AND).
anyarray<object>optional
At least one child must match (OR).
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
anyarray<object>optional
At least one child must match (OR).
Show child properties
allarray<object>optional
Every child must match (AND).
anyarray<object>optional
At least one child must match (OR).
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
anyarray<object>optional
At least one child must match (OR).
Show child properties
allarray<object>optional
Every child must match (AND).
Show child properties
allarray<object>optional
Every child must match (AND).
Show child properties
allarray<object>optional
Every child must match (AND).
Show child properties
allarray<object>optional
Every child must match (AND).
anyarray<object>optional
At least one child must match (OR).
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
anyarray<object>optional
At least one child must match (OR).
Show child properties
allarray<object>optional
Every child must match (AND).
anyarray<object>optional
At least one child must match (OR).
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
anyarray<object>optional
At least one child must match (OR).
Show child properties
allarray<object>optional
Every child must match (AND).
Show child properties
allarray<object>optional
Every child must match (AND).
anyarray<object>optional
At least one child must match (OR).
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
anyarray<object>optional
At least one child must match (OR).
Show child properties
allarray<object>optional
Every child must match (AND).
anyarray<object>optional
At least one child must match (OR).
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
anyarray<object>optional
At least one child must match (OR).
Show child properties
allarray<object>optional
Every child must match (AND).
Show child properties
allarray<object>optional
Every child must match (AND).
Show child properties
allarray<object>optional
Every child must match (AND).
anyarray<object>optional
At least one child must match (OR).
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
anyarray<object>optional
At least one child must match (OR).
Show child properties
allarray<object>optional
Every child must match (AND).
anyarray<object>optional
At least one child must match (OR).
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
anyarray<object>optional
At least one child must match (OR).
Show child properties
allarray<object>optional
Every child must match (AND).
Show child properties
allarray<object>optional
Every child must match (AND).
anyarray<object>optional
At least one child must match (OR).
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
anyarray<object>optional
At least one child must match (OR).
Show child properties
allarray<object>optional
Every child must match (AND).
anyarray<object>optional
At least one child must match (OR).
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
columnstringoptional
Leaf only: the column key, or a `$`-prefixed system column.
opstringoptional
Leaf only: the operator. Must be one the column's type accepts.
enum
["equals","not_equals","contains","starts_with","greater_than","less_than","between","is_empty","is_not_empty","in"]
valueanyoptional
Leaf only: the value to compare with — a scalar, a list for `in`/`between`, a `{"relative": preset}` window on temporal columns, or omitted for `is_empty`/`is_not_empty`.
rangeobjectrequired
The half-open [from, to) window.
Show child properties
fromstringrequired
Start of the window (inclusive, ISO-8601).
format
date-time
tostringrequired
End of the window (exclusive, ISO-8601).
format
date-time
relationsarray<object>required
Relation columns whose target table is inside the group.
Show child properties
from_table_idstringrequired
The table holding the relation column.
format
uuid
from_columnstringrequired
The relation column key.
to_table_idstringrequired
The table the relation points at.
format
uuid
rangeobjectrequired
The half-open [from, to) window.
Show child properties
fromstringrequired
Start of the window (inclusive, ISO-8601).
format
date-time
tostringrequired
End of the window (exclusive, ISO-8601).
format
date-time
relativestringoptional
The preset the range came from, when it did.
computed_atstringoptional
When the numbers were computed (ISO-8601).
format
date-time
{
    "totals": {
        "tables": 2,
        "records": 61234,
        "storage_bytes": 12345678
    },
    "tables": [
        {
            "id": "9d2f4c1e-7b8a-4e0f-9a3b-5c6d7e8f9a0b",
            "name": "Oda",
            "slug": "orders",
            "icon": "\ud83e\uddfe",
            "records_count": 52014,
            "columns_count": 4,
            "headline": {
                "label": "Total Kiasi (TZS)",
                "fn": "sum",
                "column": "amount",
                "value": 5466022000,
                "unit": "TZS"
            },
            "created_last_range": 812,
            "drill": {
                "table_id": "9d2f4c1e-7b8a-4e0f-9a3b-5c6d7e8f9a0b",
                "filter": {
                    "all": [
                        {
                            "column": "$created_at",
                            "op": "between",
                            "value": [
                                "2026-08-10T00:00:00Z",
                                "2026-09-09T00:00:00Z"
                            ]
                        }
                    ]
                },
                "range": {
                    "from": "2026-08-10T00:00:00Z",
                    "to": "2026-09-09T00:00:00Z"
                }
            }
        }
    ],
    "over_time": {
        "bucket": "day",
        "rows": [
            {
                "bucket": "2026-09-01",
                "total": 42,
                "by_table": {
                    "9d2f4c1e-7b8a-4e0f-9a3b-5c6d7e8f9a0b": 30,
                    "4e8d9c0b-1a2f-4b3c-8d4e-5f6a7b8c9d0e": 12
                },
                "drill": {
                    "9d2f4c1e-7b8a-4e0f-9a3b-5c6d7e8f9a0b": {
                        "table_id": "9d2f4c1e-7b8a-4e0f-9a3b-5c6d7e8f9a0b",
                        "filter": {
                            "all": [
                                {
                                    "column": "$created_at",
                                    "op": "between",
                                    "value": [
                                        "2026-09-01T00:00:00Z",
                                        "2026-09-02T00:00:00Z"
                                    ]
                                }
                            ]
                        },
                        "range": {
                            "from": "2026-09-01T00:00:00Z",
                            "to": "2026-09-02T00:00:00Z"
                        }
                    }
                }
            }
        ]
    },
    "headlines": [
        {
            "table_id": "9d2f4c1e-7b8a-4e0f-9a3b-5c6d7e8f9a0b",
            "table": "Oda",
            "label": "Total Kiasi (TZS)",
            "fn": "sum",
            "column": "amount",
            "value": 5466022000,
            "unit": "TZS",
            "chart": "number",
            "drill": {
                "table_id": "9d2f4c1e-7b8a-4e0f-9a3b-5c6d7e8f9a0b",
                "filter": {
                    "all": [
                        {
                            "column": "$created_at",
                            "op": "between",
                            "value": [
                                "2026-08-10T00:00:00Z",
                                "2026-09-09T00:00:00Z"
                            ]
                        }
                    ]
                },
                "range": {
                    "from": "2026-08-10T00:00:00Z",
                    "to": "2026-09-09T00:00:00Z"
                }
            }
        }
    ],
    "relations": [
        {
            "from_table_id": "9d2f4c1e-7b8a-4e0f-9a3b-5c6d7e8f9a0b",
            "from_column": "customer",
            "to_table_id": "4e8d9c0b-1a2f-4b3c-8d4e-5f6a7b8c9d0e"
        }
    ],
    "range": {
        "from": "2026-08-10T00:00:00Z",
        "to": "2026-09-09T00:00:00Z",
        "relative": "last_30_days"
    },
    "computed_at": "2026-09-08T10:11:12Z"
}
default
{
    "totals": {
        "tables": 2,
        "records": 61234,
        "storage_bytes": 12345678
    },
    "tables": [
        {
            "id": "9d2f4c1e-7b8a-4e0f-9a3b-5c6d7e8f9a0b",
            "name": "Oda",
            "slug": "orders",
            "icon": "\ud83e\uddfe",
            "records_count": 52014,
            "columns_count": 4,
            "headline": {
                "label": "Total Kiasi (TZS)",
                "fn": "sum",
                "column": "amount",
                "value": 5466022000,
                "unit": "TZS"
            },
            "created_last_range": 812,
            "drill": {
                "table_id": "9d2f4c1e-7b8a-4e0f-9a3b-5c6d7e8f9a0b",
                "filter": {
                    "all": [
                        {
                            "column": "$created_at",
                            "op": "between",
                            "value": [
                                "2026-08-10T00:00:00Z",
                                "2026-09-09T00:00:00Z"
                            ]
                        }
                    ]
                },
                "range": {
                    "from": "2026-08-10T00:00:00Z",
                    "to": "2026-09-09T00:00:00Z"
                }
            }
        }
    ],
    "over_time": {
        "bucket": "day",
        "rows": [
            {
                "bucket": "2026-09-01",
                "total": 42,
                "by_table": {
                    "9d2f4c1e-7b8a-4e0f-9a3b-5c6d7e8f9a0b": 30,
                    "4e8d9c0b-1a2f-4b3c-8d4e-5f6a7b8c9d0e": 12
                },
                "drill": {
                    "9d2f4c1e-7b8a-4e0f-9a3b-5c6d7e8f9a0b": {
                        "table_id": "9d2f4c1e-7b8a-4e0f-9a3b-5c6d7e8f9a0b",
                        "filter": {
                            "all": [
                                {
                                    "column": "$created_at",
                                    "op": "between",
                                    "value": [
                                        "2026-09-01T00:00:00Z",
                                        "2026-09-02T00:00:00Z"
                                    ]
                                }
                            ]
                        },
                        "range": {
                            "from": "2026-09-01T00:00:00Z",
                            "to": "2026-09-02T00:00:00Z"
                        }
                    }
                }
            }
        ]
    },
    "headlines": [
        {
            "table_id": "9d2f4c1e-7b8a-4e0f-9a3b-5c6d7e8f9a0b",
            "table": "Oda",
            "label": "Total Kiasi (TZS)",
            "fn": "sum",
            "column": "amount",
            "value": 5466022000,
            "unit": "TZS",
            "chart": "number",
            "drill": {
                "table_id": "9d2f4c1e-7b8a-4e0f-9a3b-5c6d7e8f9a0b",
                "filter": {
                    "all": [
                        {
                            "column": "$created_at",
                            "op": "between",
                            "value": [
                                "2026-08-10T00:00:00Z",
                                "2026-09-09T00:00:00Z"
                            ]
                        }
                    ]
                },
                "range": {
                    "from": "2026-08-10T00:00:00Z",
                    "to": "2026-09-09T00:00:00Z"
                }
            }
        }
    ],
    "relations": [
        {
            "from_table_id": "9d2f4c1e-7b8a-4e0f-9a3b-5c6d7e8f9a0b",
            "from_column": "customer",
            "to_table_id": "4e8d9c0b-1a2f-4b3c-8d4e-5f6a7b8c9d0e"
        }
    ],
    "range": {
        "from": "2026-08-10T00:00:00Z",
        "to": "2026-09-09T00:00:00Z",
        "relative": "last_30_days"
    },
    "computed_at": "2026-09-08T10:11:12Z"
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The key's issuer does not hold data.view, or the key has no issuer on record.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This API key was issued by a user without the \"data.view\" permission."
}
default
{
    "status": "error",
    "message": "This API key was issued by a user without the \"data.view\" permission."
}
404No group with that id for this tenant (or the id is not a UUID).
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}
default
{
    "status": "error",
    "message": "Resource not found."
}
422Malformed query or invalid record value. Missing request data may use the standard v3 envelope; expected data refusals use DataError.
Alternative 1oneOfoptional
Expected refusal from the data store. Authentication/framework failures can instead use LegacyErrorEnvelope. Quotas are 402; conflicts 409; unsupported operators and sorts needing an index 501.
Show child properties
errorobjectrequired
Structured domain refusal with a stable machine code and retry guidance.
Show child properties
codestringrequired
Stable refusal category.
enum
["validation_error","conflict","not_found","rate_limited","quota_exceeded","temporary_failure","permission_denied","not_supported","provider_failure"]
messagestringrequired
Human-readable explanation.
retryablebooleanrequired
Whether repeating unchanged could succeed; still apply operation idempotency rules.
fieldstringoptional
Field key associated with this refusal.
expected_typestringoptional
Expected field type when available.
receivedstringoptional
JSON type/shape description, not the submitted sensitive value.
retry_after_secondsintegeroptional
Suggested wait before retry.
minimum
0
detailsobjectoptional
Additional field errors, quota details or reason information.
additionalProperties
true
messagestringrequired
Compatibility human message from the originating refusal.
codestringrequired
Same category as error.code.
enum
["validation_error","conflict","not_found","rate_limited","quota_exceeded","temporary_failure","permission_denied","not_supported","provider_failure"]
errorsobjectoptional
Optional field messages in Laravel validation shape.
additionalProperties
{"type":"array","items":{"type":"string"}}
Alternative 2oneOfoptional
Error response envelope for validation and server errors.
Show child properties
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "error": {
        "code": "validation_error",
        "message": "The supplied data or query is invalid.",
        "retryable": false
    },
    "message": "The supplied data or query is invalid.",
    "code": "validation_error"
}
default
{
    "error": {
        "code": "validation_error",
        "message": "The supplied data or query is invalid.",
        "retryable": false
    },
    "message": "The supplied data or query is invalid.",
    "code": "validation_error"
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Payments

List payments

GET/api/v3/payments

Read tenant payment intents newest first. Requires payments.view on the key issuer. The native response is {data:[...],meta:{current_page,per_page,total,last_page}}; pagination defaults to 25 and caps at 100. state=open selects draft, pending and authorised; unknown state input currently leaves results unfiltered. amount_minor and refunded_minor are integers on a fixed 100-minor-units-per-major-unit scale, including TZS. Use formatted amount for display. paid, partly_refunded and refunded mean funds settled at some point; inspect refunded_minor to determine what has been returned. This REST surface provides reads only.

AuthenticationTenant API token

Required permission: payments.view

Query parameters

statestringoptional
Filter by state. open means draft, pending or authorised; omitted/all means no filter. Unknown input also currently leaves the list unfiltered.
enum
["all","open","draft","pending","authorised","paid","failed","expired","cancelled","refunded","partly_refunded"]

Example: open

subject_idstringoptional
Only payments raised for this record — an order id, a Daftari record id, an invoice number.

Example: 1214

per_pageintegeroptional
Alias of `limit`, for clients that already speak Laravel pagination. `limit` wins if both are sent.
minimum
1
maximum
100

Example: 25

pageintegeroptional
Page number, 1-based. Read `data.pagination.has_more_pages` to know when to stop.
minimum
1
default
1

Example: 1

Responses

200The page of payments.
dataarray<object>required
The payments on this page, newest first.
Show child properties
idstringoptional
The payment's id. Time-ordered, so sorting by it sorts by when it was raised.
format
uuid
referencestringoptional
The human reference, unique in this workspace: PAY-YYYYMMDD-NNNN. This is what a person quotes down a phone line.
statestringoptional
Where the ask got to. Only paid, partly_refunded and refunded mean money actually arrived.
enum
["draft","pending","authorised","paid","failed","expired","cancelled","refunded","partly_refunded"]
state_labelstringoptional
The state written for a person to read.
is_openbooleanoptional
Whether the payment is still waiting on the customer.
is_settledbooleanoptional
Whether money arrived, whatever has since been given back.
amount_minorintegeroptional
Integer minor units at a fixed scale of 100 per major unit, including TZS: 40000 represents TZS 400. Use amount for formatted display.
amountstringoptional
The same amount formatted with its currency code, for showing to a person.
currencystringoptional
ISO 4217 code.
refunded_minorintegeroptional
How much of the amount has already been given back, in minor units.
refundedstringoptional
The refunded total formatted, or null when nothing has been refunded.
nullable
true
refundable_minorintegeroptional
How much could still be refunded, in minor units.
payerobjectoptional
Who is paying: name, phone and email, only as far as they were given.
nullable
true
additionalProperties
true
methodstringoptional
How the customer was asked: ussd_push, link or lipa.
nullable
true
providerstringoptional
The gateway the request went to.
nullable
true
subject_typestringoptional
What is being paid for — an order, a data table, an invoice.
nullable
true
subject_idstringoptional
The id of the thing being paid for.
nullable
true
is_refundbooleanoptional
Whether this payment is itself a refund of another one.
refund_ofstringoptional
The payment this one gives money back for.
format
uuid
nullable
true
refund_reasonstringoptional
Why the refund was raised.
nullable
true
attemptsintegeroptional
How many times the provider has been asked.
last_errorstringoptional
What went wrong last time, in plain words. Never carries a credential.
nullable
true
expires_atstringoptional
When the ask stops being answerable.
format
date-time
nullable
true
settled_atstringoptional
When the money arrived.
format
date-time
nullable
true
created_atstringoptional
When the payment was raised.
format
date-time
next_statesarray<string>optional
The states this payment may legally move to next.
checkout_urlstringoptional
The page to send the customer to, when the method produced one.
nullable
true
payment_tokenstringoptional
The short lipa number the customer pays from any wallet app.
nullable
true
token_expires_atstringoptional
When that lipa number stops working.
format
date-time
nullable
true
metaobjectrequired
Where this page sits in the whole set.
Show child properties
current_pageintegeroptional
The page returned.
per_pageintegeroptional
How many rows a page holds.
totalintegeroptional
How many payments match in total.
last_pageintegeroptional
The highest page number available.
{
    "data": [
        {
            "id": "0192f3c4-5a6b-7c8d-9e0f-1a2b3c4d5e6f",
            "reference": "PAY-20260908-0042",
            "state": "paid",
            "state_label": "Paid",
            "is_open": false,
            "is_settled": true,
            "amount_minor": 4000000,
            "amount": "TZS 40,000",
            "currency": "TZS",
            "refunded_minor": 0,
            "refunded": null,
            "refundable_minor": 4000000,
            "payer": {
                "name": "Asha Mushi",
                "phone": "255712345678"
            },
            "method": "ussd_push",
            "provider": "selcom",
            "subject_type": "App\\Models\\WaOrder",
            "subject_id": "1214",
            "is_refund": false,
            "refund_of": null,
            "refund_reason": null,
            "attempts": 1,
            "last_error": null,
            "expires_at": "2026-09-08T17:31:07+03:00",
            "settled_at": "2026-09-08T14:34:52+03:00",
            "created_at": "2026-09-08T14:31:07+03:00",
            "next_states": [
                "partly_refunded",
                "refunded"
            ],
            "checkout_url": null,
            "payment_token": null,
            "token_expires_at": null
        }
    ],
    "meta": {
        "current_page": 1,
        "per_page": 25,
        "total": 1,
        "last_page": 1
    }
}
default
{
    "data": [
        {
            "id": "0192f3c4-5a6b-7c8d-9e0f-1a2b3c4d5e6f",
            "reference": "PAY-20260908-0042",
            "state": "paid",
            "state_label": "Paid",
            "is_open": false,
            "is_settled": true,
            "amount_minor": 4000000,
            "amount": "TZS 40,000",
            "currency": "TZS",
            "refunded_minor": 0,
            "refunded": null,
            "refundable_minor": 4000000,
            "payer": {
                "name": "Asha Mushi",
                "phone": "255712345678"
            },
            "method": "ussd_push",
            "provider": "selcom",
            "subject_type": "App\\Models\\WaOrder",
            "subject_id": "1214",
            "is_refund": false,
            "refund_of": null,
            "refund_reason": null,
            "attempts": 1,
            "last_error": null,
            "expires_at": "2026-09-08T17:31:07+03:00",
            "settled_at": "2026-09-08T14:34:52+03:00",
            "created_at": "2026-09-08T14:31:07+03:00",
            "next_states": [
                "partly_refunded",
                "refunded"
            ],
            "checkout_url": null,
            "payment_token": null,
            "token_expires_at": null
        }
    ],
    "meta": {
        "current_page": 1,
        "per_page": 25,
        "total": 1,
        "last_page": 1
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The key's issuer does not hold payments.view, or the key has no issuer on record.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This API key was issued by a user without the \"payments.view\" permission."
}
default
{
    "status": "error",
    "message": "This API key was issued by a user without the \"payments.view\" permission."
}
404No such workspace for this token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}
default
{
    "status": "error",
    "message": "Resource not found."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Payments

Read one payment

GET/api/v3/payments/{payment}

One payment in full: the amount, who was asked, where it got to, everything that has happened to it in order, what it wrote in the books, and any refunds raised against it. The timeline is the answer to "the customer says they paid and the record says otherwise", and it is append-only — nothing in it is ever edited. Accepts the payment id or its human reference.

AuthenticationTenant API token

Required permission: payments.view

Path parameters

paymentstringrequired
The payment id, or its human reference (PAY-YYYYMMDD-NNNN). A payment belonging to another tenant answers 404.

Example: PAY-20260908-0042

Responses

200The payment, its timeline, its ledger entries and its refunds.
dataobjectrequired
The payment with its whole story.
Show child properties
idstringoptional
The payment's id. Time-ordered, so sorting by it sorts by when it was raised.
format
uuid
referencestringoptional
The human reference, unique in this workspace: PAY-YYYYMMDD-NNNN. This is what a person quotes down a phone line.
statestringoptional
Where the ask got to. Only paid, partly_refunded and refunded mean money actually arrived.
enum
["draft","pending","authorised","paid","failed","expired","cancelled","refunded","partly_refunded"]
state_labelstringoptional
The state written for a person to read.
is_openbooleanoptional
Whether the payment is still waiting on the customer.
is_settledbooleanoptional
Whether money arrived, whatever has since been given back.
amount_minorintegeroptional
Integer minor units at a fixed scale of 100 per major unit, including TZS: 40000 represents TZS 400. Use amount for formatted display.
amountstringoptional
The same amount formatted with its currency code, for showing to a person.
currencystringoptional
ISO 4217 code.
refunded_minorintegeroptional
How much of the amount has already been given back, in minor units.
refundedstringoptional
The refunded total formatted, or null when nothing has been refunded.
nullable
true
refundable_minorintegeroptional
How much could still be refunded, in minor units.
payerobjectoptional
Who is paying: name, phone and email, only as far as they were given.
nullable
true
additionalProperties
true
methodstringoptional
How the customer was asked: ussd_push, link or lipa.
nullable
true
providerstringoptional
The gateway the request went to.
nullable
true
subject_typestringoptional
What is being paid for — an order, a data table, an invoice.
nullable
true
subject_idstringoptional
The id of the thing being paid for.
nullable
true
is_refundbooleanoptional
Whether this payment is itself a refund of another one.
refund_ofstringoptional
The payment this one gives money back for.
format
uuid
nullable
true
refund_reasonstringoptional
Why the refund was raised.
nullable
true
attemptsintegeroptional
How many times the provider has been asked.
last_errorstringoptional
What went wrong last time, in plain words. Never carries a credential.
nullable
true
expires_atstringoptional
When the ask stops being answerable.
format
date-time
nullable
true
settled_atstringoptional
When the money arrived.
format
date-time
nullable
true
created_atstringoptional
When the payment was raised.
format
date-time
next_statesarray<string>optional
The states this payment may legally move to next.
checkout_urlstringoptional
The page to send the customer to, when the method produced one.
nullable
true
payment_tokenstringoptional
The short lipa number the customer pays from any wallet app.
nullable
true
token_expires_atstringoptional
When that lipa number stops working.
format
date-time
nullable
true
created_bystringoptional
The person who raised the payment, when a person did.
nullable
true
timelinearray<object>optional
Everything that has happened to this payment, oldest first. Append-only: nothing here is ever edited.
Show child properties
idstringoptional
The event's id.
format
uuid
typestringoptional
created, collect_requested, state_changed, refund_requested, reconciled or drift.
from_statestringoptional
The state before this event.
nullable
true
to_statestringoptional
The state after it.
nullable
true
sourcestringoptional
Which door caused it: api, webhook, reconciler, flow, mcp, client or system.
messagestringoptional
What happened, in plain words.
nullable
true
occurred_atstringoptional
When.
format
date-time
ledgerarray<object>optional
What this payment wrote in the books. Append-only and always balanced: every posting moves the same amount out of one account as into another.
Show child properties
entry_nointegeroptional
The ledger's own sequence number.
kindstringoptional
payment, refund, payout, fee, adjustment, charge, hold or release.
directionstringoptional
Which way the money went. The amount is always positive; this carries the sign.
enum
["debit","credit"]
accountstringoptional
Whose position moved.
enum
["customer","business","platform","provider"]
amount_minorintegeroptional
Integer amount on the payment layer fixed scale of 100 minor units per major currency unit.
amountstringoptional
The same amount formatted.
occurred_atstringoptional
When the money moved.
format
date-time
refundsarray<object>optional
Refunds raised against this payment. Each is a payment in its own right, linked back by refund_of.
items.additionalProperties
true
{
    "data": {
        "id": "0192f3c4-5a6b-7c8d-9e0f-1a2b3c4d5e6f",
        "reference": "PAY-20260908-0042",
        "state": "paid",
        "state_label": "Paid",
        "is_open": false,
        "is_settled": true,
        "amount_minor": 4000000,
        "amount": "TZS 40,000",
        "currency": "TZS",
        "refunded_minor": 0,
        "refunded": null,
        "refundable_minor": 4000000,
        "payer": {
            "name": "Asha Mushi",
            "phone": "255712345678"
        },
        "method": "ussd_push",
        "provider": "selcom",
        "subject_type": "App\\Models\\WaOrder",
        "subject_id": "1214",
        "is_refund": false,
        "refund_of": null,
        "refund_reason": null,
        "attempts": 1,
        "last_error": null,
        "expires_at": "2026-09-08T17:31:07+03:00",
        "settled_at": "2026-09-08T14:34:52+03:00",
        "created_at": "2026-09-08T14:31:07+03:00",
        "next_states": [
            "partly_refunded",
            "refunded"
        ],
        "checkout_url": null,
        "payment_token": null,
        "token_expires_at": null,
        "created_by": "Neema Kimaro",
        "timeline": [
            {
                "id": "0192f3c4-5a6b-7c8d-9e0f-1a2b3c4d5e70",
                "type": "created",
                "from_state": null,
                "to_state": "draft",
                "source": "api",
                "message": "TZS 40,000 asked for, for App\\Models\\WaOrder 1214.",
                "occurred_at": "2026-09-08T14:31:07+03:00"
            },
            {
                "id": "0192f3c4-5a6b-7c8d-9e0f-1a2b3c4d5e71",
                "type": "collect_requested",
                "from_state": "draft",
                "to_state": "pending",
                "source": "api",
                "message": "Asked the provider for the money.",
                "occurred_at": "2026-09-08T14:31:08+03:00"
            },
            {
                "id": "0192f3c4-5a6b-7c8d-9e0f-1a2b3c4d5e72",
                "type": "state_changed",
                "from_state": "pending",
                "to_state": "paid",
                "source": "webhook",
                "message": "",
                "occurred_at": "2026-09-08T14:34:52+03:00"
            }
        ],
        "ledger": [
            {
                "entry_no": 8121,
                "kind": "payment",
                "direction": "debit",
                "account": "customer",
                "amount_minor": 4000000,
                "amount": "TZS 40,000",
                "occurred_at": "2026-09-08T14:34:52+03:00"
            },
            {
                "entry_no": 8122,
                "kind": "payment",
                "direction": "credit",
                "account": "business",
                "amount_minor": 4000000,
                "amount": "TZS 40,000",
                "occurred_at": "2026-09-08T14:34:52+03:00"
            }
        ],
        "refunds": []
    }
}
default
{
    "data": {
        "id": "0192f3c4-5a6b-7c8d-9e0f-1a2b3c4d5e6f",
        "reference": "PAY-20260908-0042",
        "state": "paid",
        "state_label": "Paid",
        "is_open": false,
        "is_settled": true,
        "amount_minor": 4000000,
        "amount": "TZS 40,000",
        "currency": "TZS",
        "refunded_minor": 0,
        "refunded": null,
        "refundable_minor": 4000000,
        "payer": {
            "name": "Asha Mushi",
            "phone": "255712345678"
        },
        "method": "ussd_push",
        "provider": "selcom",
        "subject_type": "App\\Models\\WaOrder",
        "subject_id": "1214",
        "is_refund": false,
        "refund_of": null,
        "refund_reason": null,
        "attempts": 1,
        "last_error": null,
        "expires_at": "2026-09-08T17:31:07+03:00",
        "settled_at": "2026-09-08T14:34:52+03:00",
        "created_at": "2026-09-08T14:31:07+03:00",
        "next_states": [
            "partly_refunded",
            "refunded"
        ],
        "checkout_url": null,
        "payment_token": null,
        "token_expires_at": null,
        "created_by": "Neema Kimaro",
        "timeline": [
            {
                "id": "0192f3c4-5a6b-7c8d-9e0f-1a2b3c4d5e70",
                "type": "created",
                "from_state": null,
                "to_state": "draft",
                "source": "api",
                "message": "TZS 40,000 asked for, for App\\Models\\WaOrder 1214.",
                "occurred_at": "2026-09-08T14:31:07+03:00"
            },
            {
                "id": "0192f3c4-5a6b-7c8d-9e0f-1a2b3c4d5e71",
                "type": "collect_requested",
                "from_state": "draft",
                "to_state": "pending",
                "source": "api",
                "message": "Asked the provider for the money.",
                "occurred_at": "2026-09-08T14:31:08+03:00"
            },
            {
                "id": "0192f3c4-5a6b-7c8d-9e0f-1a2b3c4d5e72",
                "type": "state_changed",
                "from_state": "pending",
                "to_state": "paid",
                "source": "webhook",
                "message": "",
                "occurred_at": "2026-09-08T14:34:52+03:00"
            }
        ],
        "ledger": [
            {
                "entry_no": 8121,
                "kind": "payment",
                "direction": "debit",
                "account": "customer",
                "amount_minor": 4000000,
                "amount": "TZS 40,000",
                "occurred_at": "2026-09-08T14:34:52+03:00"
            },
            {
                "entry_no": 8122,
                "kind": "payment",
                "direction": "credit",
                "account": "business",
                "amount_minor": 4000000,
                "amount": "TZS 40,000",
                "occurred_at": "2026-09-08T14:34:52+03:00"
            }
        ],
        "refunds": []
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The key's issuer does not hold payments.view, or the key has no issuer on record.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This API key was issued by a user without the \"payments.view\" permission."
}
default
{
    "status": "error",
    "message": "This API key was issued by a user without the \"payments.view\" permission."
}
404No payment with that id or reference in this workspace.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}
default
{
    "status": "error",
    "message": "Resource not found."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Data tables

Read a table’s status fields and legal transitions

GET/api/v3/data/tables/{table}/states

Read state keys and allowed transitions before creating or changing a status value. Every status field includes its initial states and next moves with permission-aware allowed flags. Use ordinary record PATCH to apply a state value; this read does not reserve a transition.

AuthenticationTenant API token

Required permission: data.view

Path parameters

tablestringrequired
The table id (from `GET /api/v3/data/tables`). Anything that is not a UUID, or a table belonging to another tenant, answers 404.
format
uuid

Example: 9d2f4c1e-7b8a-4e0f-9a3b-5c6d7e8f9a0b

Responses

200Status state machines visible to this caller.
tableobjectrequired
Identity of the table whose status fields are described.
Show child properties
idstringrequired
Table UUID.
format
uuid
namestringrequired
Table name.
slugstringrequired
Table slug.
fieldsarray<object>required
One entry per status field; empty when the table has none.
Show child properties
keystringrequired
Column key.
labelstringrequired
Field label.
requiredbooleanrequired
Whether a value is required.
strictbooleanrequired
Whether configured transition restrictions apply.
initialarray<string>required
Allowed initial state keys.
statesarray<object>required
All configured states and moves.
Show child properties
keystringrequired
State key to write.
labelstringrequired
Display label.
colorstringrequired
Display color.
initialbooleanrequired
May be an initial record state.
finalbooleanrequired
Final-state marker.
nextarray<object>required
Available outgoing moves, annotated for the caller.
Show child properties
keystringrequired
Destination state key.
labelstringrequired
Destination label.
colorstringrequired
Display color.
finalbooleanrequired
Whether destination is final.
actionstringrequired
Label for this move.
requiresstring | nullrequired
Additional permission required for this move.
allowedbooleanrequired
Whether this caller may make this move.
{
    "table": {
        "id": "2c7e1a9b-3d4f-4a5b-8c6d-7e8f9a0b1c2d",
        "name": "Orders",
        "slug": "orders"
    },
    "fields": []
}
default
{
    "table": {
        "id": "2c7e1a9b-3d4f-4a5b-8c6d-7e8f9a0b1c2d",
        "name": "Orders",
        "slug": "orders"
    },
    "fields": []
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403Key issuer lacks data.view.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "You do not have permission to perform this action."
}
404Invalid UUID, unknown table or table hidden from this caller.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Automations

Read the business event log

GET/api/v3/automations/events

Read business events newest first in the native {events,has_more,next_before,event_keys} envelope. Requires automations.view. The event_keys catalog declares publisher availability with live; keys marked false do not currently publish. delivered_at records fan-out processing, and delivered_count counts successful dispatch outcomes, including queued webhook/agent jobs whose external work may still be pending. limit defaults to 50 and clamps to 1–50. Pass next_before as before for a strict older-than timestamp filter. A full page sets has_more=true without proving another row exists. Invalid before values restart at the newest page. The timestamp cursor has no ID tie-breaker and is not a lossless high-volume export cursor.

AuthenticationTenant API token

Required permission: automations.view

Query parameters

keystringoptional
Only this event. One of the platform's closed list; anything else is refused with a 422 naming the ones that exist.

Example: record.transitioned

subject_idstringoptional
Everything that ever happened to one thing — a record id, an approval id.

Example: 9f1c2d3e-4a5b-4c6d-8e9f-0a1b2c3d4e5f

beforestringoptional
Read the page older than this timestamp — the next_before value from the previous page.
format
date-time

Example: 2026-09-08T09:14:22+03:00

limitintegeroptional
How many events to return, 1 to 50.
minimum
1
maximum
50
default
50

Responses

200The event log, newest first.
eventsarray<object>required
The events, newest first.
Show child properties
idstringoptional
The event's id — a uuid v7, so sorting by it is sorting by time.
format
uuid
keystringoptional
What happened, from the platform's closed list, e.g. record.transitioned.
enum
["record.created","record.updated","record.deleted","record.transitioned","payment.paid","payment.failed","payment.refunded","order.completed","approval.requested","approval.settled","booking.confirmed","ticket.opened","ticket.closed","call.completed","message.received"]
labelstringoptional
The same thing in a sentence, e.g. "A record moved to a new state".
subject_typestringoptional
What the event is about, e.g. data_record or approval.
nullable
true
subject_idstringoptional
The id of that thing, so every event about one record can be read together.
nullable
true
payloadobjectoptional
What happened, in full. For a record event: the table, the record id, the record itself, and the fields that moved.
additionalProperties
true
actorobjectoptional
Who did it: kind (user, api, mcp, flow, ivr, schedule, system), id and a label.
additionalProperties
true
occurred_atstringoptional
When it happened, not when it was written.
format
date-time
delivered_atstringoptional
When the fan-out finished with it. Null means it has not been processed yet.
format
date-time
nullable
true
delivered_countintegeroptional
How many subscriptions acted on it. Zero with a delivered_at means nothing was listening — the usual reason an automation "did not run".
has_morebooleanrequired
Whether there is an older page.
next_beforestringoptional
Pass this back as before to read the next page.
format
date-time
nullable
true
event_keysarray<object>optional
The closed list of events this platform publishes, so a caller never has to guess one.
Show child properties
keystringoptional
The event key.
labelstringoptional
What it means, in a sentence.
groupstringoptional
Which part of the business it belongs to.
subjectstringoptional
What kind of thing the event is about.
publisherstringoptional
Which part of the platform publishes it.
livebooleanoptional
Whether that publisher has shipped yet. A key that is not live is part of the contract but never fires.
{
    "events": [
        {
            "id": "01a08270-0000-7000-8000-2a3b4c5d6e7f",
            "key": "record.transitioned",
            "label": "A record moved to a new state",
            "subject_type": "data_record",
            "subject_id": "9f1c2d3e-4a5b-4c6d-8e9f-0a1b2c3d4e5f",
            "payload": {
                "table": {
                    "id": "1b2c3d4e-5f60-4718-9a2b-3c4d5e6f7a8b",
                    "name": "Orders",
                    "slug": "orders"
                },
                "record_id": "9f1c2d3e-4a5b-4c6d-8e9f-0a1b2c3d4e5f",
                "record": {
                    "customer": "Asha Mwinyi",
                    "status": "paid",
                    "total": 45000
                },
                "changes": {
                    "status": {
                        "from": "confirmed",
                        "to": "paid"
                    }
                },
                "moved": [
                    "status"
                ],
                "source": "ui"
            },
            "actor": {
                "kind": "user",
                "id": 42,
                "label": "Asha Mwinyi"
            },
            "occurred_at": "2026-09-08T09:14:22+03:00",
            "delivered_at": "2026-09-08T09:14:23+03:00",
            "delivered_count": 2
        }
    ],
    "has_more": false,
    "next_before": null,
    "event_keys": [
        {
            "key": "record.transitioned",
            "label": "A record moved to a new state",
            "group": "Records",
            "subject": "data_record",
            "publisher": "Daftari",
            "live": true
        }
    ]
}
default
{
    "events": [
        {
            "id": "01a08270-0000-7000-8000-2a3b4c5d6e7f",
            "key": "record.transitioned",
            "label": "A record moved to a new state",
            "subject_type": "data_record",
            "subject_id": "9f1c2d3e-4a5b-4c6d-8e9f-0a1b2c3d4e5f",
            "payload": {
                "table": {
                    "id": "1b2c3d4e-5f60-4718-9a2b-3c4d5e6f7a8b",
                    "name": "Orders",
                    "slug": "orders"
                },
                "record_id": "9f1c2d3e-4a5b-4c6d-8e9f-0a1b2c3d4e5f",
                "record": {
                    "customer": "Asha Mwinyi",
                    "status": "paid",
                    "total": 45000
                },
                "changes": {
                    "status": {
                        "from": "confirmed",
                        "to": "paid"
                    }
                },
                "moved": [
                    "status"
                ],
                "source": "ui"
            },
            "actor": {
                "kind": "user",
                "id": 42,
                "label": "Asha Mwinyi"
            },
            "occurred_at": "2026-09-08T09:14:22+03:00",
            "delivered_at": "2026-09-08T09:14:23+03:00",
            "delivered_count": 2
        }
    ],
    "has_more": false,
    "next_before": null,
    "event_keys": [
        {
            "key": "record.transitioned",
            "label": "A record moved to a new state",
            "group": "Records",
            "subject": "data_record",
            "publisher": "Daftari",
            "live": true
        }
    ]
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The key's issuer does not hold automations.view, or the key has no issuer on record.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This API key was issued by a user without the \"automations.view\" permission."
}
default
{
    "status": "error",
    "message": "This API key was issued by a user without the \"automations.view\" permission."
}
404The automations module is switched off for this workspace.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}
default
{
    "status": "error",
    "message": "Resource not found."
}
422The key query parameter named an event this platform does not publish.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Unknown event key. The ones this platform publishes are: record.created, record.updated, record.deleted, record.transitioned, payment.paid, payment.failed, payment.refunded, order.completed, approval.requested, approval.settled, booking.confirmed, ticket.opened, ticket.closed, call.completed, message.received."
}
default
{
    "status": "error",
    "message": "Unknown event key. The ones this platform publishes are: record.created, record.updated, record.deleted, record.transitioned, payment.paid, payment.failed, payment.refunded, order.completed, approval.requested, approval.settled, booking.confirmed, ticket.opened, ticket.closed, call.completed, message.received."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Automations

List the event subscriptions

GET/api/v3/automations/subscriptions

What this workspace has arranged to happen when something occurs: start a flow, tell a team, call a URL, or hand it to an assistant. Read this to find out whether an integration is already set up, and to see whether one has been failing — last_error carries the platform's own sentence, and a subscription that has failed ten times in a row switches itself off and says so. Signing secrets are never returned; signed only says whether a webhook's deliveries carry one. Returns at most 200 rows, ordered by key and label, without pagination. A key filter includes wildcard subscriptions. Intermediate webhook retries do not each increment the consecutive failure counter; the terminal failed delivery does.

AuthenticationTenant API token

Required permission: automations.view

Query parameters

keystringoptional
Only subscriptions listening for this event. Wildcard subscriptions are always included, because they do listen for it.

Example: payment.paid

enabled_onlybooleanoptional
Leave out the ones that are switched off.
default
false

Responses

200The subscriptions on this account.
subscriptionsarray<object>required
The subscriptions, by event then name.
Show child properties
idintegeroptional
The subscription's id.
keystringoptional
The event it listens for, or * for every event.
key_labelstringoptional
That event in a sentence.
kindstringoptional
What it does when the event happens.
enum
["flow","notification","webhook","agent"]
targetstringoptional
What it does it to: a flow id, who to tell, a URL, or an assistant id.
labelstringoptional
What a person calls it.
filterobjectoptional
A condition over the event; null means it fires on every one.
additionalProperties
true
nullable
true
configobjectoptional
Per-kind extras — a notification's title and body, an assistant's instruction, a flow's variables.
additionalProperties
true
nullable
true
signedbooleanoptional
Whether a webhook's deliveries carry a signature. The secret itself is never returned by this API.
enabledbooleanoptional
Whether it is switched on. Ten failures in a row switch one off.
last_fired_atstringoptional
When it last acted on an event.
format
date-time
nullable
true
fire_countintegeroptional
How many times it has acted.
last_errorstringoptional
Why the last attempt failed, in the platform's own words.
nullable
true
last_failed_atstringoptional
When that failure was.
format
date-time
nullable
true
failure_countintegeroptional
How many failures in a row. Any success resets it to zero.
created_atstringoptional
When it was set up.
format
date-time
nullable
true
{
    "subscriptions": [
        {
            "id": 7,
            "key": "payment.paid",
            "key_label": "A payment settled",
            "kind": "webhook",
            "target": "https://orders.example.co.tz/hooks/momo",
            "label": "Paid orders to the warehouse",
            "filter": {
                "all": [
                    {
                        "column": "amount",
                        "op": "greater_than",
                        "value": 10000
                    }
                ]
            },
            "config": null,
            "signed": true,
            "enabled": true,
            "last_fired_at": "2026-09-08T09:14:23+03:00",
            "fire_count": 412,
            "last_error": null,
            "last_failed_at": null,
            "failure_count": 0,
            "created_at": "2026-08-01T11:02:00+03:00"
        }
    ]
}
default
{
    "subscriptions": [
        {
            "id": 7,
            "key": "payment.paid",
            "key_label": "A payment settled",
            "kind": "webhook",
            "target": "https://orders.example.co.tz/hooks/momo",
            "label": "Paid orders to the warehouse",
            "filter": {
                "all": [
                    {
                        "column": "amount",
                        "op": "greater_than",
                        "value": 10000
                    }
                ]
            },
            "config": null,
            "signed": true,
            "enabled": true,
            "last_fired_at": "2026-09-08T09:14:23+03:00",
            "fire_count": 412,
            "last_error": null,
            "last_failed_at": null,
            "failure_count": 0,
            "created_at": "2026-08-01T11:02:00+03:00"
        }
    ]
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The key's issuer does not hold automations.view, or the key has no issuer on record.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This API key was issued by a user without the \"automations.view\" permission."
}
default
{
    "status": "error",
    "message": "This API key was issued by a user without the \"automations.view\" permission."
}
404The automations module is switched off for this workspace.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}
default
{
    "status": "error",
    "message": "Resource not found."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Automations

List the schedules

GET/api/v3/automations/schedules

Read recurring schedules in the native {schedules:[...]} envelope. Requires automations.view. At most 300 rows are returned, ordered by name, without pagination. kind and enabled_only narrow the list. describes is the readable rhythm; spec retains its timezone and recurrence rules. A null next_run_at can indicate a disabled, exhausted or invalid schedule. last_result describes execution, with optional misfire details; follow any resulting resource reference for final delivery. run_once runs one late occurrence, skip advances without firing missed occurrences, and run_all replays at most 12 missed slots per runner tick. Lateness of up to 90 seconds is within the grace period.

AuthenticationTenant API token

Required permission: automations.view

Query parameters

kindstringoptional
Only this kind of schedule.
enum
["flow","report","record","call","message"]

Example: report

enabled_onlybooleanoptional
Leave out the ones that are switched off.
default
false

Responses

200The schedules on this account.
schedulesarray<object>required
The schedules, by name.
Show child properties
idstringoptional
The schedule's id.
format
uuid
namestringoptional
What a person calls it.
kindstringoptional
What it does each time it runs.
enum
["flow","report","record","call","message"]
specobjectoptional
The rhythm: every, unit, at, weekdays, day_of_month, timezone, until, count.
additionalProperties
true
describesstringoptional
The same rhythm as one checkable sentence, e.g. "Every week on Monday at 09:00 (Africa/Dar_es_Salaam)".
targetstringoptional
What it acts on: a table id, a phone number, a flow id, a contact group id.
nullable
true
payloadobjectoptional
The kind's own arguments — the export spec, the record to write, the message body.
additionalProperties
true
misfire_policystringoptional
What happens to runs missed while the platform was down. run_once fires once and carries on; skip fires not at all; run_all catches up, capped.
enum
["run_once","skip","run_all"]
enabledbooleanoptional
Whether it runs.
next_run_atstringoptional
The next slot, in UTC. Null when it is switched off or has run out.
format
date-time
nullable
true
last_run_atstringoptional
When it last ran.
format
date-time
nullable
true
last_resultobjectoptional
What the last run produced: ok, ref (the export, message or record it made), message, and a misfire block when slots were missed.
additionalProperties
true
nullable
true
last_errorstringoptional
Why the last run failed.
nullable
true
run_countintegeroptional
How many times it has run.
created_atstringoptional
When it was set up.
format
date-time
nullable
true
{
    "schedules": [
        {
            "id": "3f2a1b0c-9d8e-4f70-8a1b-2c3d4e5f6a7b",
            "name": "Monday sales report",
            "kind": "report",
            "spec": {
                "every": 1,
                "unit": "weeks",
                "at": "09:00",
                "weekdays": [
                    1
                ],
                "timezone": "Africa/Dar_es_Salaam"
            },
            "describes": "Every week on Monday at 09:00 (Africa/Dar_es_Salaam)",
            "target": null,
            "payload": {
                "export": {
                    "kind": "records",
                    "table_id": "1b2c3d4e-5f60-4718-9a2b-3c4d5e6f7a8b",
                    "format": "xlsx"
                },
                "deliver": {
                    "via": "email",
                    "to": "owner@example.co.tz"
                }
            },
            "misfire_policy": "run_once",
            "enabled": true,
            "next_run_at": "2026-09-14T06:00:00+00:00",
            "last_run_at": "2026-09-07T06:00:00+00:00",
            "last_result": {
                "ok": true,
                "ref": "7a8b9c0d-1e2f-4304-8516-27384950a6b7",
                "message": "Export queued."
            },
            "last_error": null,
            "run_count": 6,
            "created_at": "2026-07-20T08:11:00+03:00"
        }
    ]
}
default
{
    "schedules": [
        {
            "id": "3f2a1b0c-9d8e-4f70-8a1b-2c3d4e5f6a7b",
            "name": "Monday sales report",
            "kind": "report",
            "spec": {
                "every": 1,
                "unit": "weeks",
                "at": "09:00",
                "weekdays": [
                    1
                ],
                "timezone": "Africa/Dar_es_Salaam"
            },
            "describes": "Every week on Monday at 09:00 (Africa/Dar_es_Salaam)",
            "target": null,
            "payload": {
                "export": {
                    "kind": "records",
                    "table_id": "1b2c3d4e-5f60-4718-9a2b-3c4d5e6f7a8b",
                    "format": "xlsx"
                },
                "deliver": {
                    "via": "email",
                    "to": "owner@example.co.tz"
                }
            },
            "misfire_policy": "run_once",
            "enabled": true,
            "next_run_at": "2026-09-14T06:00:00+00:00",
            "last_run_at": "2026-09-07T06:00:00+00:00",
            "last_result": {
                "ok": true,
                "ref": "7a8b9c0d-1e2f-4304-8516-27384950a6b7",
                "message": "Export queued."
            },
            "last_error": null,
            "run_count": 6,
            "created_at": "2026-07-20T08:11:00+03:00"
        }
    ]
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The key's issuer does not hold automations.view, or the key has no issuer on record.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This API key was issued by a user without the \"automations.view\" permission."
}
default
{
    "status": "error",
    "message": "This API key was issued by a user without the \"automations.view\" permission."
}
404The automations module is switched off for this workspace.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}
default
{
    "status": "error",
    "message": "Resource not found."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Agent tasks

Submit a task to an account agent

POST/api/engine/tasks

Submit a prompt and context to an enabled agent available for API execution. Mode defaults sync; queued mode returns a run for polling. Idempotency-Key reuses the tenant/trigger run for the same fingerprint and conflicts on a changed request. The API ceiling is 60 seconds, reduced by profile/request budgets. A successful HTTP response can contain failed/denied/timed_out domain status.

AuthenticationTenant API token

Header parameters

Idempotency-Keystringoptional
Optional stable key for this business request. Same request reuses the run; different fingerprint returns 409. Unlike messaging sends, this endpoint implements request-key deduplication.
minLength
1
maxLength
191

Example: order-1042-summary-v1

Request body

application/json · required

agent_idintegerrequired
Agent ID belonging to this account.
promptstringrequired
Task instruction.
maxLength
20000
contextobject | array | nulloptional
Additional task context.
additionalProperties
true
modestringoptional
Wait synchronously or enqueue for polling.
enum
["sync","queued"]
default
sync
max_duration_msintegeroptional
Requested maximum duration; cannot extend the API/profile ceiling.
minimum
1
Complete request schema
{
    "type": "object",
    "properties": {
        "agent_id": {
            "type": "integer",
            "description": "Agent ID belonging to this account."
        },
        "prompt": {
            "type": "string",
            "description": "Task instruction.",
            "maxLength": 20000
        },
        "context": {
            "type": [
                "object",
                "array",
                "null"
            ],
            "description": "Additional task context.",
            "additionalProperties": true,
            "items": []
        },
        "mode": {
            "type": "string",
            "description": "Wait synchronously or enqueue for polling.",
            "enum": [
                "sync",
                "queued"
            ],
            "default": "sync"
        },
        "max_duration_ms": {
            "type": "integer",
            "description": "Requested maximum duration; cannot extend the API/profile ceiling.",
            "minimum": 1
        }
    },
    "required": [
        "agent_id",
        "prompt"
    ]
}
default
{
    "agent_id": 42,
    "prompt": "Summarize this order and suggest the next action.",
    "context": {
        "order_reference": "ORD-1042"
    },
    "mode": "queued",
    "max_duration_ms": 30000
}

Responses

200Synchronous outcome; inspect status rather than assuming successful execution.
statusstringrequired
Domain execution outcome; inspect even on HTTP 200.
run_uuidstringrequired
Stable run UUID.
format
uuid
outputobject | array | nullrequired
Agent output shaped by its execution contract.
additionalProperties
true
denial_reasonstring | nullrequired
Machine-readable reason when denied.
usageobjectrequired
Execution usage including token/cost values when available.
additionalProperties
true
{
    "status": "succeeded",
    "run_uuid": "2c7e1a9b-3d4f-4a5b-8c6d-7e8f9a0b1c2d",
    "output": {
        "summary": "Order summary."
    },
    "denial_reason": null,
    "usage": []
}
default
{
    "status": "succeeded",
    "run_uuid": "2c7e1a9b-3d4f-4a5b-8c6d-7e8f9a0b1c2d",
    "output": {
        "summary": "Order summary."
    },
    "denial_reason": null,
    "usage": []
}
202Accepted for background execution. Poll status_url using the same REST credential.
statusstringrequired
Current run status.
run_uuidstringrequired
Run UUID.
format
uuid
execution_statestring | nullrequired
Execution progress state.
delivery_statestring | nullrequired
Delivery progress separate from computation.
deadline_atstring | nullrequired
Execution deadline.
format
date-time
status_urlstringrequired
Authenticated run polling URL.
format
uri
denial_reasonstring | nullrequired
Reason when admission was denied.
{
    "status": "queued",
    "run_uuid": "2c7e1a9b-3d4f-4a5b-8c6d-7e8f9a0b1c2d",
    "execution_state": "queued",
    "delivery_state": "none",
    "deadline_at": "2030-10-12T06:00:30+00:00",
    "status_url": "https://business.momo.tz/api/engine/runs/2c7e1a9b-3d4f-4a5b-8c6d-7e8f9a0b1c2d",
    "denial_reason": null
}
default
{
    "status": "queued",
    "run_uuid": "2c7e1a9b-3d4f-4a5b-8c6d-7e8f9a0b1c2d",
    "execution_state": "queued",
    "delivery_state": "none",
    "deadline_at": "2030-10-12T06:00:30+00:00",
    "status_url": "https://business.momo.tz/api/engine/runs/2c7e1a9b-3d4f-4a5b-8c6d-7e8f9a0b1c2d",
    "denial_reason": null
}
401REST credential failure.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403Suspended/inactive account.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Account is suspended."
}
404Agent not found in this account.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Agent not found."
}
default
{
    "status": "error",
    "message": "Agent not found."
}
409Idempotency key already identifies a different request.
statusstringrequired
busy, denied or conflict depending on the refusal.
messagestringoptional
Optional explanation.
reasonstringoptional
Ingress refusal code, e.g. ingress_busy.
run_uuidstringoptional
Accepted/denied run UUID when known.
format
uuid
denial_reasonstringoptional
Capacity refusal code.
retryablebooleanoptional
Whether a safe retry can succeed.
retry_afterintegeroptional
Suggested delay in seconds.
status_urlstringoptional
Run lookup URL when known.
format
uri
{
    "status": "conflict",
    "message": "Idempotency-Key was already used for a different request."
}
default
{
    "status": "conflict",
    "message": "Idempotency-Key was already used for a different request."
}
422Invalid request fields, or a queued task denied/timed out on submission.
Alternative 1oneOfoptional
Show child properties
messagestringrequired
Framework validation error.
errorsobjectoptional
Field errors, including idempotency_key for an invalid header.
additionalProperties
{"type":"array","items":{"type":"string"}}
Alternative 2oneOfoptional
Show child properties
statusstringrequired
Current run status.
run_uuidstringrequired
Run UUID.
format
uuid
execution_statestring | nullrequired
Execution progress state.
delivery_statestring | nullrequired
Delivery progress separate from computation.
deadline_atstring | nullrequired
Execution deadline.
format
date-time
status_urlstringrequired
Authenticated run polling URL.
format
uri
denial_reasonstring | nullrequired
Reason when admission was denied.
{
    "message": "The prompt field is required.",
    "errors": {
        "prompt": [
            "The prompt field is required."
        ]
    }
}
Request validation failed before a run was created
{
    "message": "The prompt field is required.",
    "errors": {
        "prompt": [
            "The prompt field is required."
        ]
    }
}
Queued submission could not execute
{
    "status": "denied",
    "run_uuid": "01953b60-4ce0-7000-8000-000000000001",
    "execution_state": "denied",
    "delivery_state": "none",
    "deadline_at": "2030-10-12T06:01:00+00:00",
    "status_url": "https://business.momo.tz/api/engine/runs/01953b60-4ce0-7000-8000-000000000001",
    "denial_reason": "surface_disabled"
}
429Pending capacity exceeded. Retry-After is 5 seconds.

Response headers

Retry-Afterintegeroptional
Seconds to wait before attempting a safe retry.

Example: 5

Cache-Controlstringoptional
Admission response must not be cached.

Example: no-store

statusstringrequired
busy, denied or conflict depending on the refusal.
messagestringoptional
Optional explanation.
reasonstringoptional
Ingress refusal code, e.g. ingress_busy.
run_uuidstringoptional
Accepted/denied run UUID when known.
format
uuid
denial_reasonstringoptional
Capacity refusal code.
retryablebooleanoptional
Whether a safe retry can succeed.
retry_afterintegeroptional
Suggested delay in seconds.
status_urlstringoptional
Run lookup URL when known.
format
uri
{
    "status": "denied",
    "run_uuid": "2c7e1a9b-3d4f-4a5b-8c6d-7e8f9a0b1c2d",
    "denial_reason": "tenant_pending_capacity",
    "retryable": true,
    "retry_after": 5,
    "status_url": "https://business.momo.tz/api/engine/runs/2c7e1a9b-3d4f-4a5b-8c6d-7e8f9a0b1c2d"
}
default
{
    "status": "denied",
    "run_uuid": "2c7e1a9b-3d4f-4a5b-8c6d-7e8f9a0b1c2d",
    "denial_reason": "tenant_pending_capacity",
    "retryable": true,
    "retry_after": 5,
    "status_url": "https://business.momo.tz/api/engine/runs/2c7e1a9b-3d4f-4a5b-8c6d-7e8f9a0b1c2d"
}
503Temporary ingress contention. Retry-After is 1 second.

Response headers

Retry-Afterintegeroptional
Seconds to wait before attempting a safe retry.

Example: 1

Cache-Controlstringoptional
Admission response must not be cached.

Example: no-store

statusstringrequired
busy, denied or conflict depending on the refusal.
messagestringoptional
Optional explanation.
reasonstringoptional
Ingress refusal code, e.g. ingress_busy.
run_uuidstringoptional
Accepted/denied run UUID when known.
format
uuid
denial_reasonstringoptional
Capacity refusal code.
retryablebooleanoptional
Whether a safe retry can succeed.
retry_afterintegeroptional
Suggested delay in seconds.
status_urlstringoptional
Run lookup URL when known.
format
uri
{
    "status": "busy",
    "reason": "ingress_busy",
    "message": "Task ingress is busy.",
    "retryable": true,
    "retry_after": 1
}
default
{
    "status": "busy",
    "reason": "ingress_busy",
    "message": "Task ingress is busy.",
    "retryable": true,
    "retry_after": 1
}

API REFERENCE / Agent tasks

Read an agent run, children and trace steps

GET/api/engine/runs/{uuid}

Tenant-scoped polling and inspection. Response contains run outcome, execution/delivery state, output, model usage, costs, child runs and trace steps. Sent with Cache-Control: no-store. Treat trace arguments/results as sensitive business data.

AuthenticationTenant API token

Path parameters

uuidstringrequired
Run UUID returned by task submission.
format
uuid

Example: 2c7e1a9b-3d4f-4a5b-8c6d-7e8f9a0b1c2d

Responses

200Run and available trace. Cache-Control: no-store.
runobjectrequired
Current run outcome, execution state and accumulated model usage.
Show child properties
uuidstringrequired
Run UUID.
format
uuid
triggerstringrequired
Trigger that created this run.
statusstringrequired
Execution outcome/status.
execution_statestring | nullrequired
Fine-grained execution state.
delivery_statestring | nullrequired
Delivery state.
deadline_atstring | nullrequired
Execution deadline.
format
date-time
denial_reasonstring | nullrequired
Denial reason, when present.
outputobject | array | nullrequired
Agent output.
additionalProperties
true
providerstring | nullrequired
Model provider.
model_namestring | nullrequired
Model identifier.
prompt_tokensintegerrequired
Prompt tokens.
completion_tokensintegerrequired
Completion tokens.
cost_walletnumberrequired
Usage cost in cost_currency.
cost_currencystring | nullrequired
Cost currency.
duration_msinteger | nullrequired
Duration in milliseconds.
created_atstring | nullrequired
Run creation time.
format
date-time
childrenarray<object>required
Child executions belonging to this tenant.
Show child properties
uuidstringrequired
Run UUID.
format
uuid
statusstringrequired
Execution outcome/status.
execution_statestring | nullrequired
Fine-grained execution state.
deadline_atstring | nullrequired
Execution deadline.
format
date-time
stepsarray<object>required
Trace steps, including linked result steps when available.
Show child properties
positionintegerrequired
Step order.
kindstringrequired
Step type.
tool_namestring | nullrequired
Tool invoked, when applicable.
argumentsobject | array | nullrequired
Tool arguments; can contain sensitive business data.
additionalProperties
true
result_previewstring | object | array | nullrequired
Recorded result preview.
additionalProperties
true
statusstring | nullrequired
Step status.
duration_msinteger | nullrequired
Step duration.
{
    "run": {
        "uuid": "01953b60-4ce0-7000-8000-000000000001",
        "trigger": "api",
        "status": "succeeded",
        "execution_state": "succeeded",
        "delivery_state": "none",
        "deadline_at": "2030-10-12T06:01:00+00:00",
        "denial_reason": null,
        "output": {
            "answer": "The report is ready."
        },
        "provider": "example-provider",
        "model_name": "configured-model",
        "prompt_tokens": 120,
        "completion_tokens": 45,
        "cost_wallet": 0.01,
        "cost_currency": "TZS",
        "duration_ms": 840,
        "created_at": "2030-10-12T06:00:00+00:00"
    },
    "children": [],
    "steps": []
}
401REST credential failure.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403Suspended/inactive account.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Account is suspended."
}
404No run with this UUID in the authenticated account.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Run not found."
}
default
{
    "status": "error",
    "message": "Run not found."
}

API REFERENCE / Operations

Run a named operation

POST/api/v3/operations/{key}

Do one of this workspace's named operations and get back what it produced.

Send the values it asks for either as a top-level object or wrapped in inputs; both are read. Every value is validated first, by the same rules a chat flow and an assistant are held to, and a refusal names the field and changes nothing — status is then invalid and steps is empty, which is how you tell "we did not start" from "we started and stopped".

Set Idempotency-Key on anything you might retry. A repeat of a key whose call SUCCEEDED replays the identical response with X-Idempotent-Replay: 1, so a request that timed out can be sent again without creating a second booking or a second bill. A key whose call was refused is not spent — fix the value and send it again under the same key. A key whose run failed part-way replays that failure rather than redoing half of it, because steps before the break really happened.

When a step fails part-way, the record writes made before it are undone and rolled_back says how many went back, how many were left alone because somebody else had changed them, and how many could not be found. not_undone says what stayed done — a message already sent, money already asked for. Nothing outside the data store is reversible, and this endpoint says so rather than implying otherwise.

An operation never waits. If one of its steps raises an approval, the answer comes back as soon as the approvers are notified: it means they were asked, not that they said yes.

AuthenticationTenant API token

Required permission: operations.run

Path parameters

keystringrequired
The operation's key, in snake_case, as it appears on the Operations page. An unknown key answers 404 — and so does one belonging to another workspace.

Example: create_booking

Header parameters

Idempotency-Keystringoptional
Any string you choose. A repeat of a key whose call succeeded replays the identical response with X-Idempotent-Replay: 1. A refused call does not spend its key.

Example: booking-2026-09-09-0042

Request body

application/json

The values the operation asks for. Either wrapped in `inputs` or at the top level.

inputsobjectoptional
The values the operation asks for, keyed by its own input names. Leave it out and the top level of the body is read instead.
additionalProperties
true
idempotency_keystringoptional
The same thing as the Idempotency-Key header, for clients that cannot set one. The header wins.
Complete request schema
{
    "type": "object",
    "properties": {
        "inputs": {
            "type": "object",
            "additionalProperties": true,
            "description": "The values the operation asks for, keyed by its own input names. Leave it out and the top level of the body is read instead."
        },
        "idempotency_key": {
            "type": "string",
            "description": "The same thing as the Idempotency-Key header, for clients that cannot set one. The header wins."
        }
    }
}

Responses

200The operation ran. Every step succeeded and `outputs` is what it promised.
okbooleanrequired
True only when every step ran.
statusstringrequired
ok — it ran. invalid — the inputs were refused and nothing ran. failed — a step broke part-way.
enum
["ok","failed","invalid"]
run_idstringoptional
This run's id. Time-ordered, and what the Operations page's run log is keyed by.
format
uuid
outputsobjectrequired
What the operation promised back — a booking reference, a record id, an amount.
additionalProperties
true
stepsarray<object>optional
One entry per step that ran, in order.
Show child properties
stepstringoptional
The step's id, as the definition names it.
typestringoptional
What kind of step it was: data_save, rule, payment_intent, send…
okbooleanoptional
Whether that step succeeded.
msintegeroptional
How long that step took, in milliseconds. This is the number that answers "why was it slow".
detailobjectoptional
What the step produced — the record it wrote, the rule's answer, the payment reference.
additionalProperties
true
not_undonestringoptional
Present when this step did something a rollback cannot take back.
replayedbooleanoptional
Present and true when this answer was replayed for a repeated idempotency key rather than run again.
messagestringoptional
Absent on success. Present on a refusal, carrying the same sentence as error.message for older v3 clients.
{
    "ok": true,
    "status": "ok",
    "run_id": "0192f3b8-6c2a-7c31-9f2e-5b1c0a7d4e11",
    "outputs": {
        "booking_ref": "BKG-0042",
        "record_id": "9a1c0c7e-1f8c-4a41-9b1e-0d2f7c9b3a55"
    },
    "steps": [
        {
            "step": "check_limit",
            "type": "rule",
            "ok": true,
            "ms": 12,
            "detail": {
                "passed": true,
                "value": 2
            }
        },
        {
            "step": "booking",
            "type": "data_save",
            "ok": true,
            "ms": 41,
            "detail": {
                "record_id": "9a1c0c7e-1f8c-4a41-9b1e-0d2f7c9b3a55"
            }
        }
    ]
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The key's issuer does not hold operations.run, or the key has no issuer on record.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This API key was issued by a user without the \"operations.run\" permission."
}
default
{
    "status": "error",
    "message": "This API key was issued by a user without the \"operations.run\" permission."
}
404No operation with that key in this workspace.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "No operation with that key on this account."
}
default
{
    "status": "error",
    "message": "No operation with that key on this account."
}
422The inputs were refused, or a step failed. `status` says which: `invalid` means nothing ran, `failed` means a step broke and the record writes before it were undone.
okbooleanoptional
False.
statusstringoptional
invalid — the inputs were refused before step one. failed — a step broke part-way.
enum
["invalid","failed"]
run_idstringoptional
The run this refusal was recorded against; it is in the run log either way.
format
uuid
outputsobjectoptional
Empty on a refusal: an operation promises nothing it did not finish.
additionalProperties
true
errorobjectoptional
The platform error envelope: a machine code, a sentence a person can act on, and the field at fault.
Show child properties
codestringoptional
validation_error, conflict, not_found, quota_exceeded, rate_limited, temporary_failure, permission_denied, not_supported or provider_failure.
messagestringoptional
What went wrong, written for a person to read.
fieldstringoptional
The input or the step at fault.
rolled_backobjectoptional
Present on a `failed` run: what the compensating rollback managed to put back.
Show child properties
attemptedintegeroptional
How many record writes had inverses to replay.
restoredintegeroptional
How many went back.
conflictsintegeroptional
How many were left alone because somebody else had changed them since. A rollback never overwrites another person's work.
missingintegeroptional
How many rows could no longer be found.
failedintegeroptional
How many inverses could not be applied at all.
not_journalledbooleanoptional
True when the operation wrote more than the journal holds, so later writes are not reversible.
not_undonearray<string>optional
What stayed done: a message already sent, money already asked for, an approval already raised. Nothing outside the data store comes back.
stepsarray<object>optional
The steps that ran before it stopped, with their timing. Empty when the status is `invalid`.
Show child properties
stepstringoptional
The step's id, as the definition names it.
typestringoptional
What kind of step it was: data_save, rule, payment_intent, send…
okbooleanoptional
Whether that step succeeded.
msintegeroptional
How long that step took, in milliseconds. This is the number that answers "why was it slow".
detailobjectoptional
What the step produced — the record it wrote, the rule's answer, the payment reference.
additionalProperties
true
not_undonestringoptional
Present when this step did something a rollback cannot take back.
messagestringoptional
The same sentence as error.message, for older v3 clients.
{
    "ok": false,
    "status": "invalid",
    "run_id": "0192f3b8-6c2a-7c31-9f2e-5b1c0a7d4e11",
    "outputs": [],
    "steps": [],
    "error": {
        "code": "validation_error",
        "message": "Customer phone: That does not look like a phone number. Send it as 0712 345 678.",
        "field": "customer_phone"
    },
    "message": "Customer phone: That does not look like a phone number. Send it as 0712 345 678."
}
refused
{
    "ok": false,
    "status": "invalid",
    "run_id": "0192f3b8-6c2a-7c31-9f2e-5b1c0a7d4e11",
    "outputs": [],
    "steps": [],
    "error": {
        "code": "validation_error",
        "message": "Customer phone: That does not look like a phone number. Send it as 0712 345 678.",
        "field": "customer_phone"
    },
    "message": "Customer phone: That does not look like a phone number. Send it as 0712 345 678."
}
failed_partway
{
    "ok": false,
    "status": "failed",
    "run_id": "0192f3b8-7a10-7bd2-8c44-2e9f1a6b0c93",
    "outputs": [],
    "error": {
        "code": "conflict",
        "message": "There is nothing left to give out just now.",
        "field": "seat"
    },
    "rolled_back": {
        "attempted": 1,
        "restored": 1,
        "conflicts": 0,
        "missing": 0,
        "failed": 0,
        "not_journalled": false
    },
    "not_undone": [
        "A sms message was sent to 255712345678."
    ],
    "message": "There is nothing left to give out just now."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Catalogue

Read a product by your own code

GET/api/v3/catalogues/{catalogue}/products/by-sku/{sku}

The same product as GET /products/{product}, addressed by the code your system already knows it by, so a sync never has to keep a map of our numeric ids.

AuthenticationTenant API token

Path parameters

catalogueintegerrequired
Catalogue (shop) id, as returned by `GET /api/v3/catalogues`.

Example: 1

skustringrequired
Your own product code — `sku`, or the older `retailer_id`.
maxLength
100

Example: MNG-45W

Responses

200The product.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
The product record.
Show child properties
idintegerrequired
Product id.
catalogue_idintegeroptional
Catalogue this product belongs to.
skustringoptional
Your product code, and the identity this API addresses a product by. The shop issues one (`AMY-00042`) when you send none, and it is frozen once the product is live on any platform.
retailer_idstringrequired
The older name for `sku`, kept in step with it. Prefer `sku` in new code.
meta_product_idstring | nulloptional
Meta product id once mirrored; null for a local-only product.
namestringrequired
Product name (max 100 characters).
descriptionstring | nulloptional
Long description (max 5000 characters).
urlstring | nulloptional
Link to the product page on your own site.
priceintegerrequired
Price in the minor unit of `currency`.
currencystringrequired
ISO 4217 currency code.
sale_priceinteger | nulloptional
Optional sale price in the minor unit.
image_urlstring | nulloptional
Publicly reachable product image. Meta fetches it directly.
availabilitystringoptional
Stock state.
enum
["in stock","out of stock","preorder","available for order","discontinued"]
conditionstringoptional
Product condition.
enum
["new","refurbished","used"]
brandstring | nulloptional
Brand name.
categorystring | nulloptional
Category label.
product_typestring | nulloptional
Your own product taxonomy string.
inventoryinteger | nulloptional
Stock count.
visibilitystringoptional
Whether customers can see it.
enum
["staging","published"]
review_statusstring | nulloptional
Meta review outcome: pending, approved or rejected.
sync_statusstring | nulloptional
A roll-up of `listings`. A product on a shop with no platforms is `synced`, because there is nothing to sync.
enum
["pending","syncing","synced","failed","blocked","drifted",null]
listingsarray<object>optional
One row per platform this shop sells on.
Show child properties
platformstringrequired
Which platform this listing is on.
statestringrequired
`blocked` is not a failure: nothing was attempted because the product is missing something this platform requires. `failed` means the platform refused a push and it will be retried. `drifted` means the platform's copy no longer matches ours — someone edited it there — and the shop's `source_of_truth` decides which copy wins.
enum
["pending","syncing","synced","failed","blocked","drifted"]
external_idstring | nulloptional
The platform's own product id once it is live.
problemstring | nulloptional
Why this platform will not show the product yet.
review_statusstring | nulloptional
The platform's review verdict, where it has one.
last_synced_atstring | nulloptional
When this listing last reached the platform.
format
date-time
driftobject | nulloptional
Field by field, what differs, while the listing is `drifted`. Cleared once resolved.
additionalProperties
{"type":"object","properties":{"ours":{"description":"The value we hold."},"theirs":{"description":"The value the platform holds."}}}
drift_detected_atstring | nulloptional
When the difference was last seen.
format
date-time
last_synced_atstring | nulloptional
When the product last synced to Meta.
format
date-time
created_atstring | nulloptional
ISO 8601 creation timestamp.
format
date-time
updated_atstring | nulloptional
ISO 8601 update timestamp.
format
date-time
{
    "status": "success",
    "data": {
        "id": 9301,
        "catalogue_id": 42,
        "sku": "MNG-45W",
        "retailer_id": "MNG-45W",
        "meta_product_id": "7766554433",
        "name": "Charger Mango 45W",
        "description": "USB-C PD, 1 m cable",
        "url": "https://shop.example.com/mng45",
        "price": 4500000,
        "currency": "TZS",
        "sale_price": 3900000,
        "image_url": "https://cdn.example.com/mng45.jpg",
        "availability": "in stock",
        "condition": "new",
        "brand": "Mango",
        "category": "Chargers",
        "product_type": "Electronics > Chargers",
        "inventory": 12,
        "visibility": "published",
        "review_status": "approved",
        "sync_status": "synced",
        "listings": [
            {
                "platform": "whatsapp",
                "state": "synced",
                "external_id": "7766554433",
                "problem": null,
                "review_status": "approved",
                "last_synced_at": "2026-09-11T02:00:41+00:00"
            }
        ],
        "last_synced_at": "2026-09-11T02:00:41+00:00",
        "created_at": "2026-09-01T08:20:00+00:00",
        "updated_at": "2026-09-11T02:00:41+00:00"
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}
422The payload failed validation. `errors` maps each rejected field to its messages.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Validation failed.",
    "errors": {
        "recipient": [
            "Provide recipient or recipients."
        ]
    }
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}
404No such record for this tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}

API REFERENCE / Catalogue

Create or update a product by your own code

PUT/api/v3/catalogues/{catalogue}/products/by-sku/{sku}

Changes the product with this code, or creates it when there is none — so a store that has just added an item does not have to know whether we have seen it before. Only the fields you send are touched.

AuthenticationTenant API token

Path parameters

catalogueintegerrequired
Catalogue (shop) id, as returned by `GET /api/v3/catalogues`.

Example: 1

skustringrequired
Your own product code — `sku`, or the older `retailer_id`.
maxLength
100

Example: MNG-45W

Request body

application/json · required

skustringoptional
Your product code. Left out, the shop issues one.
maxLength
100
retailer_idstringoptional
The older name for `sku`.
maxLength
100
namestringoptional
Product name as customers see it.
maxLength
200
priceintegeroptional
Price in the minor unit of `currency` — 25000 is TZS 250.00 for a 2-decimal currency.
minimum
0
currencystringoptional
ISO 4217 code. Left out, the shop's own currency is used.
minLength
3
maxLength
3
descriptionstringoptional
Long description.
maxLength
9000
sale_priceintegeroptional
Optional sale price in the minor unit. Ignored when it is higher than `price`.
minimum
0
image_urlstringoptional
Publicly reachable image. Platforms fetch it themselves, so it cannot sit behind a login.
format
uri
maxLength
2048
additional_image_urlsarray<string>optional
Up to ten more images.
maxItems
10
items.format
uri
urlstringoptional
The product page on your own site.
format
uri
maxLength
2048
availabilitystringoptional
Left out, it is derived from `inventory`: a count of zero means `out of stock` unless the shop allows backorders. Sent explicitly, what you send wins.
enum
["in stock","out of stock","preorder","available for order","discontinued"]
conditionstringoptional
Item condition.
enum
["new","refurbished","used"]
brandstringoptional
Brand name. Becomes a brand record on first use.
maxLength
255
categorystringoptional
Category name. Becomes a category record on first use.
maxLength
255
product_typestringoptional
Your own taxonomy path.
maxLength
750
inventoryintegeroptional
Units on hand.
minimum
0
visibilitystringoptional
Whether customers may see it.
enum
["staging","published"]
custom_labelsarray<string>optional
Up to five free labels for your own segmentation.
maxItems
5
Complete request schema
{
    "type": "object",
    "required": [],
    "description": "A product. Only a name and a price are required here; an image, a non-zero price and the rest are what individual PLATFORMS require, and a product missing them is stored and reported as `blocked` on that platform rather than refused.",
    "properties": {
        "sku": {
            "type": "string",
            "maxLength": 100,
            "description": "Your product code. Left out, the shop issues one."
        },
        "retailer_id": {
            "type": "string",
            "maxLength": 100,
            "description": "The older name for `sku`."
        },
        "name": {
            "type": "string",
            "maxLength": 200,
            "description": "Product name as customers see it."
        },
        "price": {
            "type": "integer",
            "minimum": 0,
            "description": "Price in the minor unit of `currency` \u2014 25000 is TZS 250.00 for a 2-decimal currency."
        },
        "currency": {
            "type": "string",
            "minLength": 3,
            "maxLength": 3,
            "description": "ISO 4217 code. Left out, the shop's own currency is used."
        },
        "description": {
            "type": "string",
            "maxLength": 9000,
            "description": "Long description."
        },
        "sale_price": {
            "type": "integer",
            "minimum": 0,
            "description": "Optional sale price in the minor unit. Ignored when it is higher than `price`."
        },
        "image_url": {
            "type": "string",
            "format": "uri",
            "maxLength": 2048,
            "description": "Publicly reachable image. Platforms fetch it themselves, so it cannot sit behind a login."
        },
        "additional_image_urls": {
            "type": "array",
            "maxItems": 10,
            "items": {
                "type": "string",
                "format": "uri"
            },
            "description": "Up to ten more images."
        },
        "url": {
            "type": "string",
            "format": "uri",
            "maxLength": 2048,
            "description": "The product page on your own site."
        },
        "availability": {
            "type": "string",
            "enum": [
                "in stock",
                "out of stock",
                "preorder",
                "available for order",
                "discontinued"
            ],
            "description": "Left out, it is derived from `inventory`: a count of zero means `out of stock` unless the shop allows backorders. Sent explicitly, what you send wins."
        },
        "condition": {
            "type": "string",
            "enum": [
                "new",
                "refurbished",
                "used"
            ],
            "description": "Item condition."
        },
        "brand": {
            "type": "string",
            "maxLength": 255,
            "description": "Brand name. Becomes a brand record on first use."
        },
        "category": {
            "type": "string",
            "maxLength": 255,
            "description": "Category name. Becomes a category record on first use."
        },
        "product_type": {
            "type": "string",
            "maxLength": 750,
            "description": "Your own taxonomy path."
        },
        "inventory": {
            "type": "integer",
            "minimum": 0,
            "description": "Units on hand."
        },
        "visibility": {
            "type": "string",
            "enum": [
                "staging",
                "published"
            ],
            "description": "Whether customers may see it."
        },
        "custom_labels": {
            "type": "array",
            "maxItems": 5,
            "items": {
                "type": "string"
            },
            "description": "Up to five free labels for your own segmentation."
        }
    }
}

Responses

200The updated product.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
The product record.
Show child properties
idintegerrequired
Product id.
catalogue_idintegeroptional
Catalogue this product belongs to.
skustringoptional
Your product code, and the identity this API addresses a product by. The shop issues one (`AMY-00042`) when you send none, and it is frozen once the product is live on any platform.
retailer_idstringrequired
The older name for `sku`, kept in step with it. Prefer `sku` in new code.
meta_product_idstring | nulloptional
Meta product id once mirrored; null for a local-only product.
namestringrequired
Product name (max 100 characters).
descriptionstring | nulloptional
Long description (max 5000 characters).
urlstring | nulloptional
Link to the product page on your own site.
priceintegerrequired
Price in the minor unit of `currency`.
currencystringrequired
ISO 4217 currency code.
sale_priceinteger | nulloptional
Optional sale price in the minor unit.
image_urlstring | nulloptional
Publicly reachable product image. Meta fetches it directly.
availabilitystringoptional
Stock state.
enum
["in stock","out of stock","preorder","available for order","discontinued"]
conditionstringoptional
Product condition.
enum
["new","refurbished","used"]
brandstring | nulloptional
Brand name.
categorystring | nulloptional
Category label.
product_typestring | nulloptional
Your own product taxonomy string.
inventoryinteger | nulloptional
Stock count.
visibilitystringoptional
Whether customers can see it.
enum
["staging","published"]
review_statusstring | nulloptional
Meta review outcome: pending, approved or rejected.
sync_statusstring | nulloptional
A roll-up of `listings`. A product on a shop with no platforms is `synced`, because there is nothing to sync.
enum
["pending","syncing","synced","failed","blocked","drifted",null]
listingsarray<object>optional
One row per platform this shop sells on.
Show child properties
platformstringrequired
Which platform this listing is on.
statestringrequired
`blocked` is not a failure: nothing was attempted because the product is missing something this platform requires. `failed` means the platform refused a push and it will be retried. `drifted` means the platform's copy no longer matches ours — someone edited it there — and the shop's `source_of_truth` decides which copy wins.
enum
["pending","syncing","synced","failed","blocked","drifted"]
external_idstring | nulloptional
The platform's own product id once it is live.
problemstring | nulloptional
Why this platform will not show the product yet.
review_statusstring | nulloptional
The platform's review verdict, where it has one.
last_synced_atstring | nulloptional
When this listing last reached the platform.
format
date-time
driftobject | nulloptional
Field by field, what differs, while the listing is `drifted`. Cleared once resolved.
additionalProperties
{"type":"object","properties":{"ours":{"description":"The value we hold."},"theirs":{"description":"The value the platform holds."}}}
drift_detected_atstring | nulloptional
When the difference was last seen.
format
date-time
last_synced_atstring | nulloptional
When the product last synced to Meta.
format
date-time
created_atstring | nulloptional
ISO 8601 creation timestamp.
format
date-time
updated_atstring | nulloptional
ISO 8601 update timestamp.
format
date-time
{
    "status": "success",
    "data": {
        "id": 9301,
        "catalogue_id": 42,
        "sku": "MNG-45W",
        "retailer_id": "MNG-45W",
        "meta_product_id": "7766554433",
        "name": "Charger Mango 45W",
        "description": "USB-C PD, 1 m cable",
        "url": "https://shop.example.com/mng45",
        "price": 4500000,
        "currency": "TZS",
        "sale_price": 3900000,
        "image_url": "https://cdn.example.com/mng45.jpg",
        "availability": "in stock",
        "condition": "new",
        "brand": "Mango",
        "category": "Chargers",
        "product_type": "Electronics > Chargers",
        "inventory": 12,
        "visibility": "published",
        "review_status": "approved",
        "sync_status": "synced",
        "listings": [
            {
                "platform": "whatsapp",
                "state": "synced",
                "external_id": "7766554433",
                "problem": null,
                "review_status": "approved",
                "last_synced_at": "2026-09-11T02:00:41+00:00"
            }
        ],
        "last_synced_at": "2026-09-11T02:00:41+00:00",
        "created_at": "2026-09-01T08:20:00+00:00",
        "updated_at": "2026-09-11T02:00:41+00:00"
    }
}
201The product did not exist and was created.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
The product record.
Show child properties
idintegerrequired
Product id.
catalogue_idintegeroptional
Catalogue this product belongs to.
skustringoptional
Your product code, and the identity this API addresses a product by. The shop issues one (`AMY-00042`) when you send none, and it is frozen once the product is live on any platform.
retailer_idstringrequired
The older name for `sku`, kept in step with it. Prefer `sku` in new code.
meta_product_idstring | nulloptional
Meta product id once mirrored; null for a local-only product.
namestringrequired
Product name (max 100 characters).
descriptionstring | nulloptional
Long description (max 5000 characters).
urlstring | nulloptional
Link to the product page on your own site.
priceintegerrequired
Price in the minor unit of `currency`.
currencystringrequired
ISO 4217 currency code.
sale_priceinteger | nulloptional
Optional sale price in the minor unit.
image_urlstring | nulloptional
Publicly reachable product image. Meta fetches it directly.
availabilitystringoptional
Stock state.
enum
["in stock","out of stock","preorder","available for order","discontinued"]
conditionstringoptional
Product condition.
enum
["new","refurbished","used"]
brandstring | nulloptional
Brand name.
categorystring | nulloptional
Category label.
product_typestring | nulloptional
Your own product taxonomy string.
inventoryinteger | nulloptional
Stock count.
visibilitystringoptional
Whether customers can see it.
enum
["staging","published"]
review_statusstring | nulloptional
Meta review outcome: pending, approved or rejected.
sync_statusstring | nulloptional
A roll-up of `listings`. A product on a shop with no platforms is `synced`, because there is nothing to sync.
enum
["pending","syncing","synced","failed","blocked","drifted",null]
listingsarray<object>optional
One row per platform this shop sells on.
Show child properties
platformstringrequired
Which platform this listing is on.
statestringrequired
`blocked` is not a failure: nothing was attempted because the product is missing something this platform requires. `failed` means the platform refused a push and it will be retried. `drifted` means the platform's copy no longer matches ours — someone edited it there — and the shop's `source_of_truth` decides which copy wins.
enum
["pending","syncing","synced","failed","blocked","drifted"]
external_idstring | nulloptional
The platform's own product id once it is live.
problemstring | nulloptional
Why this platform will not show the product yet.
review_statusstring | nulloptional
The platform's review verdict, where it has one.
last_synced_atstring | nulloptional
When this listing last reached the platform.
format
date-time
driftobject | nulloptional
Field by field, what differs, while the listing is `drifted`. Cleared once resolved.
additionalProperties
{"type":"object","properties":{"ours":{"description":"The value we hold."},"theirs":{"description":"The value the platform holds."}}}
drift_detected_atstring | nulloptional
When the difference was last seen.
format
date-time
last_synced_atstring | nulloptional
When the product last synced to Meta.
format
date-time
created_atstring | nulloptional
ISO 8601 creation timestamp.
format
date-time
updated_atstring | nulloptional
ISO 8601 update timestamp.
format
date-time
{
    "status": "success",
    "data": {
        "id": 9301,
        "catalogue_id": 42,
        "sku": "MNG-45W",
        "retailer_id": "MNG-45W",
        "meta_product_id": "7766554433",
        "name": "Charger Mango 45W",
        "description": "USB-C PD, 1 m cable",
        "url": "https://shop.example.com/mng45",
        "price": 4500000,
        "currency": "TZS",
        "sale_price": 3900000,
        "image_url": "https://cdn.example.com/mng45.jpg",
        "availability": "in stock",
        "condition": "new",
        "brand": "Mango",
        "category": "Chargers",
        "product_type": "Electronics > Chargers",
        "inventory": 12,
        "visibility": "published",
        "review_status": "approved",
        "sync_status": "synced",
        "listings": [
            {
                "platform": "whatsapp",
                "state": "synced",
                "external_id": "7766554433",
                "problem": null,
                "review_status": "approved",
                "last_synced_at": "2026-09-11T02:00:41+00:00"
            }
        ],
        "last_synced_at": "2026-09-11T02:00:41+00:00",
        "created_at": "2026-09-01T08:20:00+00:00",
        "updated_at": "2026-09-11T02:00:41+00:00"
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}
422The payload failed validation. `errors` maps each rejected field to its messages.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Validation failed.",
    "errors": {
        "recipient": [
            "Provide recipient or recipients."
        ]
    }
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}
404No such record for this tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}

API REFERENCE / Catalogue

Delete a product by your own code

DELETE/api/v3/catalogues/{catalogue}/products/by-sku/{sku}

Takes the product off every platform it is on, then off the shelf.

AuthenticationTenant API token

Path parameters

catalogueintegerrequired
Catalogue (shop) id, as returned by `GET /api/v3/catalogues`.

Example: 1

skustringrequired
Your own product code — `sku`, or the older `retailer_id`.
maxLength
100

Example: MNG-45W

Responses

200The product was removed.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
What was removed.
Show child properties
deletedbooleanoptional
Always true.
idintegeroptional
The id of the product that was removed.
skustringoptional
The code it was known by.
retailer_idstring | nulloptional
The older name for that code.
{
    "status": "success",
    "data": {
        "deleted": true,
        "id": 9301,
        "sku": "MNG-45W",
        "retailer_id": "MNG-45W"
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}
422The payload failed validation. `errors` maps each rejected field to its messages.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Validation failed.",
    "errors": {
        "recipient": [
            "Provide recipient or recipients."
        ]
    }
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}
404No such record for this tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}

API REFERENCE / Catalogue

List bulk writes

GET/api/v3/catalogues/{catalogue}/syncs

Every bulk write into this catalogue, newest first — the API, a scheduled feed pull, or a spreadsheet import.

AuthenticationTenant API token

Path parameters

catalogueintegerrequired
Catalogue (shop) id, as returned by `GET /api/v3/catalogues`.

Example: 1

Query parameters

limitintegeroptional
Rows per page, 1–100. Defaults to 20 (25 for catalogue endpoints). Values above 100 are clamped.
minimum
1
maximum
100
default
20

Example: 20

pageintegeroptional
Page number, 1-based. Read `data.pagination.has_more_pages` to know when to stop.
minimum
1
default
1

Example: 1

Responses

200A page of syncs.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
A page of syncs.
Show child properties
itemsarray<object>optional
The syncs on this page.
Show child properties
idintegerrequired
Sync id. Use it to read this report back.
catalogue_idintegerrequired
The catalogue that was written to.
sourcestringoptional
Where the rows came from.
enum
["api","feed","import","ui"]
modestringoptional
`upsert` leaves products the payload did not mention alone; `replace` retires them.
enum
["upsert","replace"]
statusstringrequired
Where the run has got to.
enum
["queued","running","completed","failed"]
idempotency_keystring | nulloptional
The key the caller sent, if any.
receivedintegerrequired
How many rows arrived.
createdintegeroptional
New products.
updatedintegeroptional
Products that changed.
unchangedintegeroptional
Products that were already identical — nothing was re-published for these.
rejectedintegeroptional
Rows that could not be stored. Each one is in `problems`.
retiredintegeroptional
Products taken off sale, in `replace` mode only.
platformsobjectoptional
Per platform, how many listings ended in each state — for example `{"whatsapp": {"synced": 1960, "blocked": 34}}`.
additionalProperties
{"type":"object","additionalProperties":{"type":"integer"}}
problemsarray<object>optional
Up to 200 problems, ingest refusals first.
Show child properties
skustringrequired
The product code, or the row number when the row had no code.
stagestringrequired
`ingest`, or a platform name such as `whatsapp`.
reasonstringrequired
What is wrong, in words a merchant can act on.
problems_truncatedbooleanoptional
True when there were more than 200 problems and the list was cut.
errorstring | nulloptional
Set only when the run itself failed.
started_atstring | nulloptional
When the run began.
format
date-time
finished_atstring | nulloptional
When it finished.
format
date-time
created_atstring | nulloptional
When it was accepted.
format
date-time
paginationobjectoptional
Page state.
Show child properties
current_pageintegerrequired
1-based current page index.
per_pageintegerrequired
Number of items per page.
last_pageintegerrequired
1-based index of the last page.
totalintegerrequired
Total number of items across all pages.
has_more_pagesbooleanrequired
True if more pages exist after the current page.
{
    "status": "success",
    "data": {
        "items": [
            {
                "id": 812,
                "catalogue_id": 42,
                "source": "api",
                "mode": "upsert",
                "status": "completed",
                "idempotency_key": "nightly-2026-09-11",
                "received": 2000,
                "created": 12,
                "updated": 1982,
                "unchanged": 0,
                "rejected": 6,
                "retired": 0,
                "platforms": {
                    "whatsapp": {
                        "synced": 1960,
                        "blocked": 34
                    }
                },
                "problems": [
                    {
                        "sku": "MNG-CABLE",
                        "stage": "ingest",
                        "reason": "Price must be a whole number of minor units, 0 or more."
                    },
                    {
                        "sku": "MNG-KNIFE",
                        "stage": "whatsapp",
                        "reason": "WhatsApp needs a product image it can fetch."
                    }
                ],
                "problems_truncated": false,
                "error": null,
                "started_at": "2026-09-11T02:00:05+00:00",
                "finished_at": "2026-09-11T02:00:41+00:00",
                "created_at": "2026-09-11T02:00:04+00:00"
            }
        ],
        "pagination": {
            "current_page": 1,
            "per_page": 25,
            "last_page": 1,
            "total": 1,
            "has_more_pages": false
        }
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}
422The payload failed validation. `errors` maps each rejected field to its messages.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Validation failed.",
    "errors": {
        "recipient": [
            "Provide recipient or recipients."
        ]
    }
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}
404No such record for this tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}

API REFERENCE / Catalogue

Read a sync report

GET/api/v3/catalogues/{catalogue}/syncs/{sync}

What became of one bulk write. problems[].stage is the field to read first: ingest means the row was not stored at all, and any other value is the name of a platform that stored it but will not show it — two very different things that both look like "my product is not live".

AuthenticationTenant API token

Path parameters

catalogueintegerrequired
Catalogue (shop) id, as returned by `GET /api/v3/catalogues`.

Example: 1

syncintegerrequired
Sync id, from the batch response.

Example: 812

Responses

200The sync report.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
The sync report.
Show child properties
idintegerrequired
Sync id. Use it to read this report back.
catalogue_idintegerrequired
The catalogue that was written to.
sourcestringoptional
Where the rows came from.
enum
["api","feed","import","ui"]
modestringoptional
`upsert` leaves products the payload did not mention alone; `replace` retires them.
enum
["upsert","replace"]
statusstringrequired
Where the run has got to.
enum
["queued","running","completed","failed"]
idempotency_keystring | nulloptional
The key the caller sent, if any.
receivedintegerrequired
How many rows arrived.
createdintegeroptional
New products.
updatedintegeroptional
Products that changed.
unchangedintegeroptional
Products that were already identical — nothing was re-published for these.
rejectedintegeroptional
Rows that could not be stored. Each one is in `problems`.
retiredintegeroptional
Products taken off sale, in `replace` mode only.
platformsobjectoptional
Per platform, how many listings ended in each state — for example `{"whatsapp": {"synced": 1960, "blocked": 34}}`.
additionalProperties
{"type":"object","additionalProperties":{"type":"integer"}}
problemsarray<object>optional
Up to 200 problems, ingest refusals first.
Show child properties
skustringrequired
The product code, or the row number when the row had no code.
stagestringrequired
`ingest`, or a platform name such as `whatsapp`.
reasonstringrequired
What is wrong, in words a merchant can act on.
problems_truncatedbooleanoptional
True when there were more than 200 problems and the list was cut.
errorstring | nulloptional
Set only when the run itself failed.
started_atstring | nulloptional
When the run began.
format
date-time
finished_atstring | nulloptional
When it finished.
format
date-time
created_atstring | nulloptional
When it was accepted.
format
date-time
{
    "status": "success",
    "data": {
        "id": 812,
        "catalogue_id": 42,
        "source": "api",
        "mode": "upsert",
        "status": "completed",
        "idempotency_key": "nightly-2026-09-11",
        "received": 2000,
        "created": 12,
        "updated": 1982,
        "unchanged": 0,
        "rejected": 6,
        "retired": 0,
        "platforms": {
            "whatsapp": {
                "synced": 1960,
                "blocked": 34
            }
        },
        "problems": [
            {
                "sku": "MNG-CABLE",
                "stage": "ingest",
                "reason": "Price must be a whole number of minor units, 0 or more."
            },
            {
                "sku": "MNG-KNIFE",
                "stage": "whatsapp",
                "reason": "WhatsApp needs a product image it can fetch."
            }
        ],
        "problems_truncated": false,
        "error": null,
        "started_at": "2026-09-11T02:00:05+00:00",
        "finished_at": "2026-09-11T02:00:41+00:00",
        "created_at": "2026-09-11T02:00:04+00:00"
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}
422The payload failed validation. `errors` maps each rejected field to its messages.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Validation failed.",
    "errors": {
        "recipient": [
            "Provide recipient or recipients."
        ]
    }
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}
404No such record for this tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}

API REFERENCE / Catalogue

Set stock levels in bulk

POST/api/v3/catalogues/{catalogue}/inventory

The endpoint to call the moment something sells on your own site. Levels are absolute, never deltas — your system is stating what it has, and a delta would drift out of step the first time a message was delivered twice.

Availability follows the count: unless the shop allows backorders, a level of 0 sets the product to out of stock and stock coming back lifts it to in stock again. derived in the response names every product where that happened. A discontinued or preorder product is never quietly put back on sale by a delivery arriving.

Codes we do not have come back in unknown_skus rather than being ignored, and one push is sent to every platform the shop is on rather than one per product.

AuthenticationTenant API token

Path parameters

catalogueintegerrequired
Catalogue (shop) id, as returned by `GET /api/v3/catalogues`.

Example: 1

Request body

application/json · required

levelsarray<object>required
Up to 5,000 levels. A row needs a code and a whole-number count.
minItems
1
maxItems
5000
Show child properties
skustringrequired
Your product code (`retailer_id` also accepted).
maxLength
100
inventoryintegerrequired
Units on hand, absolute.
minimum
0
Complete request schema
{
    "type": "object",
    "required": [
        "levels"
    ],
    "properties": {
        "levels": {
            "type": "array",
            "minItems": 1,
            "maxItems": 5000,
            "description": "Up to 5,000 levels. A row needs a code and a whole-number count.",
            "items": {
                "type": "object",
                "required": [
                    "sku",
                    "inventory"
                ],
                "properties": {
                    "sku": {
                        "type": "string",
                        "maxLength": 100,
                        "description": "Your product code (`retailer_id` also accepted)."
                    },
                    "inventory": {
                        "type": "integer",
                        "minimum": 0,
                        "description": "Units on hand, absolute."
                    }
                }
            }
        }
    }
}

Responses

200What the levels changed.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
What changed.
Show child properties
updatedintegeroptional
Products whose count changed.
unchangedintegeroptional
Products already at that level; nothing was re-published for these.
unknown_skusarray<string>optional
Codes this catalogue does not have.
malformedarray<string>optional
Rows missing a code or a usable count.
derivedarray<object>optional
Products whose availability changed as a result.
Show child properties
skustringoptional
The product code.
availabilitystringoptional
What it became.
platformsobjectoptional
Per platform, how many listings ended in each state after the push.
additionalProperties
{"type":"object","additionalProperties":{"type":"integer"}}
{
    "status": "success",
    "data": {
        "updated": 2,
        "unchanged": 0,
        "unknown_skus": [],
        "malformed": [],
        "derived": [
            {
                "sku": "MNG-45W",
                "availability": "out of stock"
            }
        ],
        "platforms": {
            "whatsapp": {
                "pending": 2
            }
        }
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}
404No such record for this tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}
422The payload failed validation. `errors` maps each rejected field to its messages.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Validation failed.",
    "errors": {
        "recipient": [
            "Provide recipient or recipients."
        ]
    }
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Catalogue

Read stock levels

GET/api/v3/catalogues/{catalogue}/inventory

On hand, held by pending orders, and the difference — which is the number that decides whether a customer may buy. A product with tracked: false carries no count and is never held back.

AuthenticationTenant API token

Path parameters

catalogueintegerrequired
Catalogue (shop) id, as returned by `GET /api/v3/catalogues`.

Example: 1

Query parameters

skuarrayoptional
Only these product codes.

Example: ["MNG-45W"]

tracked_onlybooleanoptional
Only products that carry a count.
default
false
limitintegeroptional
Rows per page, 1–100. Defaults to 20 (25 for catalogue endpoints). Values above 100 are clamped.
minimum
1
maximum
100
default
20

Example: 20

pageintegeroptional
Page number, 1-based. Read `data.pagination.has_more_pages` to know when to stop.
minimum
1
default
1

Example: 1

Responses

200A page of stock levels.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
A page of stock levels.
Show child properties
itemsarray<object>optional
The rows on this page.
Show child properties
skustringrequired
Your product code.
inventoryinteger | nulloptional
Units on hand. Null means this product is not counted at all, which is normal and not a fault.
reservedintegeroptional
Units pending orders are holding.
availableinteger | nullrequired
What a customer can still buy: `inventory` minus `reserved`.
availabilitystring | nulloptional
The word platforms show, derived from `available` unless you state one.
trackedbooleanrequired
False when the product carries no count. Orders for it are never held back.
paginationobjectoptional
Page state.
Show child properties
current_pageintegerrequired
1-based current page index.
per_pageintegerrequired
Number of items per page.
last_pageintegerrequired
1-based index of the last page.
totalintegerrequired
Total number of items across all pages.
has_more_pagesbooleanrequired
True if more pages exist after the current page.
{
    "status": "success",
    "data": {
        "items": [
            {
                "sku": "MNG-45W",
                "inventory": 12,
                "reserved": 2,
                "available": 10,
                "availability": "in stock",
                "tracked": true
            }
        ],
        "pagination": {
            "current_page": 1,
            "per_page": 100,
            "last_page": 1,
            "total": 1,
            "has_more_pages": false
        }
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}
404No such record for this tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}
422The payload failed validation. `errors` maps each rejected field to its messages.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Validation failed.",
    "errors": {
        "recipient": [
            "Provide recipient or recipients."
        ]
    }
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Catalogue

Read a product’s stock history

GET/api/v3/catalogues/{catalogue}/products/by-sku/{sku}/movements

Every event that has ever moved this count, newest first — who moved it, why, and what the figures became. This is the audit trail behind inventory and reserved.

AuthenticationTenant API token

Path parameters

catalogueintegerrequired
Catalogue (shop) id, as returned by `GET /api/v3/catalogues`.

Example: 1

skustringrequired
Your own product code.
maxLength
100

Example: MNG-45W

Query parameters

limitintegeroptional
Rows per page, 1–100. Defaults to 20 (25 for catalogue endpoints). Values above 100 are clamped.
minimum
1
maximum
100
default
20

Example: 20

pageintegeroptional
Page number, 1-based. Read `data.pagination.has_more_pages` to know when to stop.
minimum
1
default
1

Example: 1

Responses

200A page of movements.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
A page of movements.
Show child properties
itemsarray<object>optional
The rows on this page.
Show child properties
idintegerrequired
Movement id.
kindstringrequired
`reserve` a pending order holding units · `release` that hold ending without a sale · `commit` the sale happening · `adjust` a person changing the count · `sync` your own system stating the level.
enum
["reserve","release","commit","adjust","sync"]
quantityintegerrequired
Signed. `reserve` and `release` move the held figure; the rest move the shelf count.
on_hand_afterinteger | nulloptional
Units on hand once this movement was applied.
reserved_afterinteger | nulloptional
Units held once this movement was applied.
order_idinteger | nulloptional
The order that caused it, where there was one.
reasonstring | nulloptional
Why, for adjustments and releases.
actorstring | nulloptional
Who or what moved it — a person, `api`, `flow`, `ivr`.
created_atstring | nulloptional
When it happened.
format
date-time
paginationobjectoptional
Page state.
Show child properties
current_pageintegerrequired
1-based current page index.
per_pageintegerrequired
Number of items per page.
last_pageintegerrequired
1-based index of the last page.
totalintegerrequired
Total number of items across all pages.
has_more_pagesbooleanrequired
True if more pages exist after the current page.
{
    "status": "success",
    "data": {
        "items": [
            {
                "id": 4411,
                "kind": "commit",
                "quantity": -2,
                "on_hand_after": 10,
                "reserved_after": 0,
                "order_id": 9182,
                "reason": null,
                "actor": "whatsapp",
                "created_at": "2026-09-11T09:14:02+00:00"
            }
        ],
        "pagination": {
            "current_page": 1,
            "per_page": 100,
            "last_page": 1,
            "total": 1,
            "has_more_pages": false
        }
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}
404No such record for this tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}
422The payload failed validation. `errors` maps each rejected field to its messages.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Validation failed.",
    "errors": {
        "recipient": [
            "Provide recipient or recipients."
        ]
    }
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Webhooks

List every event we can send

GET/api/v3/webhooks/events

The whole catalogue, grouped, with a sample payload for each. This is what the dashboard picker shows and what the event enum in the delivery schema is generated from, so a name here is by definition one the platform sends.

AuthenticationTenant API token

Responses

200The event catalogue.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
The event catalogue.
Show child properties
eventsarray<object>optional
One entry per event.
Show child properties
eventstringoptional
The name to subscribe with.
groupstringoptional
Family: Messages, Orders, Catalogue, Campaigns, WhatsApp groups, Webhooks.
labelstringoptional
Human name.
descriptionstringoptional
When it fires and what to do with it.
sampleobjectoptional
A complete example body.
{
    "status": "success",
    "data": {
        "events": [
            {
                "event": "message.received",
                "group": "Messages",
                "label": "A message arrived",
                "description": "An inbound message from a customer on any channel.",
                "sample": {
                    "event": "message.received",
                    "message_id": 101,
                    "direction": "inbound",
                    "sender": "255712345678",
                    "recipient": "MyBrand",
                    "status": "received",
                    "body": "Habari, mna kanga?",
                    "media_url": null,
                    "channel_type": "whatsapp",
                    "timestamp": "2026-09-11T09:14:02+00:00"
                }
            },
            {
                "event": "message.sent",
                "group": "Messages",
                "label": "A message was sent",
                "description": "An outbound message was handed to the carrier.",
                "sample": {
                    "event": "message.sent",
                    "message_id": 102,
                    "direction": "outbound",
                    "sender": "MyBrand",
                    "recipient": "255712345678",
                    "status": "sent",
                    "body": "Ndiyo, tuna kanga.",
                    "media_url": null,
                    "channel_type": "whatsapp",
                    "timestamp": "2026-09-11T09:14:02+00:00"
                }
            }
        ]
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Webhooks

List your receivers

GET/api/v3/webhooks

Every endpoint registered on this account, with its health.

AuthenticationTenant API token

Responses

200Your receivers.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
Your receivers.
Show child properties
itemsarray<object>optional
The receivers.
Show child properties
idintegerrequired
Webhook id.
urlstringrequired
Where we POST. Must be a public http(s) address.
format
uri
eventsarray<string>required
The events this receiver gets.
is_activebooleanrequired
Switched on by you.
paused_atstring | nulloptional
Set when the platform paused it after too many failures in a row.
format
date-time
paused_reasonstring | nulloptional
Why it was paused.
consecutive_failuresintegeroptional
Failures since the last successful delivery.
last_delivered_atstring | nulloptional
The last time this receiver answered 2xx.
format
date-time
created_atstring | nulloptional
When it was registered.
format
date-time
updated_atstring | nulloptional
When it last changed.
format
date-time
{
    "status": "success",
    "data": {
        "items": [
            {
                "id": 7,
                "url": "https://store.example.com/momo",
                "events": [
                    "order.received",
                    "order.status_changed"
                ],
                "is_active": true,
                "paused_at": null,
                "paused_reason": null,
                "consecutive_failures": 0,
                "last_delivered_at": "2026-09-11T09:14:03+00:00",
                "created_at": "2026-09-01T08:15:00+00:00",
                "updated_at": "2026-09-11T09:14:03+00:00"
            }
        ]
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Webhooks

Register a receiver

POST/api/v3/webhooks

Subscribe a URL to events. The response carries secret — the only time it is ever shown. Store it; your receiver verifies every delivery with it. Needs a key issued by a user with webhooks.manage.

AuthenticationTenant API token

Request body

application/json · required

urlstringrequired
A public http(s) address. Private and internal targets are refused.
format
uri
maxLength
2048
eventsarray<string>required
Event names from `GET /api/v3/webhooks/events`. An unknown name is refused.
minItems
1
maxItems
60
is_activebooleanoptional
Switch the receiver on or off. Turning a paused endpoint back on resumes it.
Complete request schema
{
    "type": "object",
    "properties": {
        "url": {
            "type": "string",
            "format": "uri",
            "maxLength": 2048,
            "description": "A public http(s) address. Private and internal targets are refused."
        },
        "events": {
            "type": "array",
            "minItems": 1,
            "maxItems": 60,
            "items": {
                "type": "string"
            },
            "description": "Event names from `GET /api/v3/webhooks/events`. An unknown name is refused."
        },
        "is_active": {
            "type": "boolean",
            "description": "Switch the receiver on or off. Turning a paused endpoint back on resumes it."
        }
    },
    "required": [
        "url",
        "events"
    ]
}

Responses

201The receiver, with its secret.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
The receiver, with its secret.
Show child properties
idintegerrequired
Webhook id.
urlstringrequired
Where we POST. Must be a public http(s) address.
format
uri
eventsarray<string>required
The events this receiver gets.
is_activebooleanrequired
Switched on by you.
paused_atstring | nulloptional
Set when the platform paused it after too many failures in a row.
format
date-time
paused_reasonstring | nulloptional
Why it was paused.
consecutive_failuresintegeroptional
Failures since the last successful delivery.
last_delivered_atstring | nulloptional
The last time this receiver answered 2xx.
format
date-time
created_atstring | nulloptional
When it was registered.
format
date-time
updated_atstring | nulloptional
When it last changed.
format
date-time
secretstringoptional
The signing secret. Shown here and never again.
{
    "status": "success",
    "data": {
        "id": 7,
        "url": "https://store.example.com/momo",
        "events": [
            "order.received",
            "order.status_changed"
        ],
        "is_active": true,
        "paused_at": null,
        "paused_reason": null,
        "consecutive_failures": 0,
        "last_delivered_at": "2026-09-11T09:14:03+00:00",
        "created_at": "2026-09-01T08:15:00+00:00",
        "updated_at": "2026-09-11T09:14:03+00:00",
        "secret": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}
422The payload failed validation. `errors` maps each rejected field to its messages.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Validation failed.",
    "errors": {
        "recipient": [
            "Provide recipient or recipients."
        ]
    }
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Webhooks

Read a receiver

GET/api/v3/webhooks/{webhook}

One receiver and its health. The secret is not included.

AuthenticationTenant API token

Path parameters

webhookintegerrequired
Webhook id.

Example: 7

Responses

200The receiver.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
The receiver.
Show child properties
idintegerrequired
Webhook id.
urlstringrequired
Where we POST. Must be a public http(s) address.
format
uri
eventsarray<string>required
The events this receiver gets.
is_activebooleanrequired
Switched on by you.
paused_atstring | nulloptional
Set when the platform paused it after too many failures in a row.
format
date-time
paused_reasonstring | nulloptional
Why it was paused.
consecutive_failuresintegeroptional
Failures since the last successful delivery.
last_delivered_atstring | nulloptional
The last time this receiver answered 2xx.
format
date-time
created_atstring | nulloptional
When it was registered.
format
date-time
updated_atstring | nulloptional
When it last changed.
format
date-time
{
    "status": "success",
    "data": {
        "id": 7,
        "url": "https://store.example.com/momo",
        "events": [
            "order.received",
            "order.status_changed"
        ],
        "is_active": true,
        "paused_at": null,
        "paused_reason": null,
        "consecutive_failures": 0,
        "last_delivered_at": "2026-09-11T09:14:03+00:00",
        "created_at": "2026-09-01T08:15:00+00:00",
        "updated_at": "2026-09-11T09:14:03+00:00"
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}
422The payload failed validation. `errors` maps each rejected field to its messages.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Validation failed.",
    "errors": {
        "recipient": [
            "Provide recipient or recipients."
        ]
    }
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}
404No such record for this tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}

API REFERENCE / Webhooks

Change a receiver

PATCH/api/v3/webhooks/{webhook}

Change the URL, the events, or switch it on or off. Setting is_active: true on a paused endpoint resumes it.

AuthenticationTenant API token

Path parameters

webhookintegerrequired
Webhook id.

Example: 7

Request body

application/json · required

urlstringoptional
A public http(s) address. Private and internal targets are refused.
format
uri
maxLength
2048
eventsarray<string>optional
Event names from `GET /api/v3/webhooks/events`. An unknown name is refused.
minItems
1
maxItems
60
is_activebooleanoptional
Switch the receiver on or off. Turning a paused endpoint back on resumes it.
Complete request schema
{
    "type": "object",
    "properties": {
        "url": {
            "type": "string",
            "format": "uri",
            "maxLength": 2048,
            "description": "A public http(s) address. Private and internal targets are refused."
        },
        "events": {
            "type": "array",
            "minItems": 1,
            "maxItems": 60,
            "items": {
                "type": "string"
            },
            "description": "Event names from `GET /api/v3/webhooks/events`. An unknown name is refused."
        },
        "is_active": {
            "type": "boolean",
            "description": "Switch the receiver on or off. Turning a paused endpoint back on resumes it."
        }
    }
}

Responses

200The updated receiver.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
The updated receiver.
Show child properties
idintegerrequired
Webhook id.
urlstringrequired
Where we POST. Must be a public http(s) address.
format
uri
eventsarray<string>required
The events this receiver gets.
is_activebooleanrequired
Switched on by you.
paused_atstring | nulloptional
Set when the platform paused it after too many failures in a row.
format
date-time
paused_reasonstring | nulloptional
Why it was paused.
consecutive_failuresintegeroptional
Failures since the last successful delivery.
last_delivered_atstring | nulloptional
The last time this receiver answered 2xx.
format
date-time
created_atstring | nulloptional
When it was registered.
format
date-time
updated_atstring | nulloptional
When it last changed.
format
date-time
{
    "status": "success",
    "data": {
        "id": 7,
        "url": "https://store.example.com/momo",
        "events": [
            "order.received",
            "order.status_changed"
        ],
        "is_active": true,
        "paused_at": null,
        "paused_reason": null,
        "consecutive_failures": 0,
        "last_delivered_at": "2026-09-11T09:14:03+00:00",
        "created_at": "2026-09-01T08:15:00+00:00",
        "updated_at": "2026-09-11T09:14:03+00:00"
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}
422The payload failed validation. `errors` maps each rejected field to its messages.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Validation failed.",
    "errors": {
        "recipient": [
            "Provide recipient or recipients."
        ]
    }
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}
404No such record for this tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}

API REFERENCE / Webhooks

Remove a receiver

DELETE/api/v3/webhooks/{webhook}

Stops every delivery to it. The delivery log goes with it.

AuthenticationTenant API token

Path parameters

webhookintegerrequired
Webhook id.

Example: 7

Responses

200Removed.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
What was removed.
Show child properties
deletedbooleanoptional
Always true.
idintegeroptional
The id that was removed.
{
    "status": "success",
    "data": {
        "deleted": true,
        "id": 7
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}
422The payload failed validation. `errors` maps each rejected field to its messages.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Validation failed.",
    "errors": {
        "recipient": [
            "Provide recipient or recipients."
        ]
    }
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}
404No such record for this tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}

API REFERENCE / Webhooks

Issue a new signing secret

POST/api/v3/webhooks/{webhook}/rotate-secret

The old secret stops verifying immediately, so update your receiver first and rotate second. The new one is in the response and is never shown again.

AuthenticationTenant API token

Path parameters

webhookintegerrequired
Webhook id.

Example: 7

Responses

200The receiver, with its new secret.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
The receiver, with its new secret.
Show child properties
idintegerrequired
Webhook id.
urlstringrequired
Where we POST. Must be a public http(s) address.
format
uri
eventsarray<string>required
The events this receiver gets.
is_activebooleanrequired
Switched on by you.
paused_atstring | nulloptional
Set when the platform paused it after too many failures in a row.
format
date-time
paused_reasonstring | nulloptional
Why it was paused.
consecutive_failuresintegeroptional
Failures since the last successful delivery.
last_delivered_atstring | nulloptional
The last time this receiver answered 2xx.
format
date-time
created_atstring | nulloptional
When it was registered.
format
date-time
updated_atstring | nulloptional
When it last changed.
format
date-time
secretstringoptional
The new signing secret. Shown here and never again.
{
    "status": "success",
    "data": {
        "id": 7,
        "url": "https://store.example.com/momo",
        "events": [
            "order.received",
            "order.status_changed"
        ],
        "is_active": true,
        "paused_at": null,
        "paused_reason": null,
        "consecutive_failures": 0,
        "last_delivered_at": "2026-09-11T09:14:03+00:00",
        "created_at": "2026-09-01T08:15:00+00:00",
        "updated_at": "2026-09-11T09:14:03+00:00",
        "secret": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}
422The payload failed validation. `errors` maps each rejected field to its messages.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Validation failed.",
    "errors": {
        "recipient": [
            "Provide recipient or recipients."
        ]
    }
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}
404No such record for this tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}

API REFERENCE / Webhooks

Read the delivery log

GET/api/v3/webhooks/{webhook}/deliveries

Every delivery to this receiver, newest first: what was sent, how many times, what came back. This is how you find out an endpoint has been failing without waiting for a customer to complain.

AuthenticationTenant API token

Path parameters

webhookintegerrequired
Webhook id.

Example: 7

Query parameters

statusstringoptional
Only deliveries in this state.
enum
["pending","delivered","failed","skipped"]

Example: failed

eventstringoptional
Only this event.

Example: order.received

sincestringoptional
Only deliveries created at or after this moment.
format
date-time

Example: 2026-09-11T00:00:00+00:00

limitintegeroptional
Rows per page, 1–100. Defaults to 20 (25 for catalogue endpoints). Values above 100 are clamped.
minimum
1
maximum
100
default
20

Example: 20

pageintegeroptional
Page number, 1-based. Read `data.pagination.has_more_pages` to know when to stop.
minimum
1
default
1

Example: 1

Responses

200A page of deliveries.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
A page of deliveries.
Show child properties
itemsarray<object>optional
The deliveries on this page.
Show child properties
idintegerrequired
Delivery id.
delivery_uidstringrequired
The `X-Delivery-Id` the receiver saw.
eventstringrequired
Event name.
statusstringrequired
`pending` is waiting for its next attempt. `failed` used every retry, or was refused with a 4xx. `skipped` was recorded while the endpoint was paused and never sent.
enum
["pending","delivered","failed","skipped"]
attemptsintegerrequired
How many times it was sent.
next_attempt_atstring | nulloptional
When the next retry is due, while pending.
format
date-time
last_attempt_atstring | nulloptional
When it was last sent.
format
date-time
delivered_atstring | nulloptional
When the receiver answered 2xx.
format
date-time
response_codeinteger | nulloptional
The last HTTP status the receiver answered.
response_excerptstring | nulloptional
The first kilobyte of the last response body.
errorstring | nulloptional
What went wrong, in words.
replay_of_idinteger | nulloptional
Set when this delivery is a replay of an earlier one.
payloadobjectoptional
The body that was (or will be) sent.
created_atstring | nulloptional
When the event happened.
format
date-time
paginationobjectoptional
Page state.
Show child properties
current_pageintegerrequired
1-based current page index.
per_pageintegerrequired
Number of items per page.
last_pageintegerrequired
1-based index of the last page.
totalintegerrequired
Total number of items across all pages.
has_more_pagesbooleanrequired
True if more pages exist after the current page.
{
    "status": "success",
    "data": {
        "items": [
            {
                "id": 4410,
                "delivery_uid": "dlv_01j9qk3v8x2m7n4p5r6s",
                "event": "order.received",
                "status": "delivered",
                "attempts": 1,
                "next_attempt_at": null,
                "last_attempt_at": "2026-09-11T09:14:03+00:00",
                "delivered_at": "2026-09-11T09:14:03+00:00",
                "response_code": 200,
                "response_excerpt": "ok",
                "error": null,
                "replay_of_id": null,
                "payload": {
                    "event": "order.received",
                    "order": {
                        "id": 9182,
                        "catalogue_id": 42,
                        "platform": "whatsapp",
                        "status": "pending",
                        "needs_attention": false,
                        "stock_policy": "external",
                        "customer_handle": "255712345678",
                        "customer_name": "Asha Mrisho",
                        "customer_phone": "255712345678",
                        "customer_note": null,
                        "lines": [
                            {
                                "sku": "MNG-45W",
                                "name": "Charger Mango 45W",
                                "quantity": 1,
                                "unit_price_minor": 3900000,
                                "line_total_minor": 3900000,
                                "currency": "TZS",
                                "reserved": 1,
                                "stock_short": false,
                                "unresolved": false
                            }
                        ],
                        "total_minor": 3900000,
                        "currency": "TZS",
                        "conversation_id": 771,
                        "priced_at": "2026-09-11T09:14:02+00:00",
                        "created_at": "2026-09-11T09:14:02+00:00",
                        "order_id": 9182,
                        "customer_wa_id": "255712345678",
                        "total_amount": 3900000,
                        "total_currency": "TZS"
                    },
                    "timestamp": "2026-09-11T09:14:02+00:00"
                },
                "created_at": "2026-09-11T09:14:02+00:00"
            }
        ],
        "pagination": {
            "current_page": 1,
            "per_page": 50,
            "last_page": 1,
            "total": 1,
            "has_more_pages": false
        }
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}
422The payload failed validation. `errors` maps each rejected field to its messages.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Validation failed.",
    "errors": {
        "recipient": [
            "Provide recipient or recipients."
        ]
    }
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}
404No such record for this tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}

API REFERENCE / Webhooks

Send a delivery again

POST/api/v3/webhooks/{webhook}/deliveries/{delivery}/replay

Queues a fresh delivery of the same payload with a new delivery id. If the endpoint was paused, this resumes it — a replay is you saying "it is fixed, try again".

AuthenticationTenant API token

Path parameters

webhookintegerrequired
Webhook id.

Example: 7

deliveryintegerrequired
Delivery id, from the log.

Example: 4410

Responses

202The new delivery, queued.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
The new delivery.
Show child properties
idintegerrequired
Delivery id.
delivery_uidstringrequired
The `X-Delivery-Id` the receiver saw.
eventstringrequired
Event name.
statusstringrequired
`pending` is waiting for its next attempt. `failed` used every retry, or was refused with a 4xx. `skipped` was recorded while the endpoint was paused and never sent.
enum
["pending","delivered","failed","skipped"]
attemptsintegerrequired
How many times it was sent.
next_attempt_atstring | nulloptional
When the next retry is due, while pending.
format
date-time
last_attempt_atstring | nulloptional
When it was last sent.
format
date-time
delivered_atstring | nulloptional
When the receiver answered 2xx.
format
date-time
response_codeinteger | nulloptional
The last HTTP status the receiver answered.
response_excerptstring | nulloptional
The first kilobyte of the last response body.
errorstring | nulloptional
What went wrong, in words.
replay_of_idinteger | nulloptional
Set when this delivery is a replay of an earlier one.
payloadobjectoptional
The body that was (or will be) sent.
created_atstring | nulloptional
When the event happened.
format
date-time
{
    "status": "success",
    "data": {
        "id": 4411,
        "delivery_uid": "dlv_01j9qk4a1b2c3d4e5f6g",
        "event": "order.received",
        "status": "pending",
        "attempts": 0,
        "next_attempt_at": null,
        "last_attempt_at": "2026-09-11T09:14:03+00:00",
        "delivered_at": null,
        "response_code": null,
        "response_excerpt": null,
        "error": null,
        "replay_of_id": 4410,
        "payload": {
            "event": "order.received",
            "order": {
                "id": 9182,
                "catalogue_id": 42,
                "platform": "whatsapp",
                "status": "pending",
                "needs_attention": false,
                "stock_policy": "external",
                "customer_handle": "255712345678",
                "customer_name": "Asha Mrisho",
                "customer_phone": "255712345678",
                "customer_note": null,
                "lines": [
                    {
                        "sku": "MNG-45W",
                        "name": "Charger Mango 45W",
                        "quantity": 1,
                        "unit_price_minor": 3900000,
                        "line_total_minor": 3900000,
                        "currency": "TZS",
                        "reserved": 1,
                        "stock_short": false,
                        "unresolved": false
                    }
                ],
                "total_minor": 3900000,
                "currency": "TZS",
                "conversation_id": 771,
                "priced_at": "2026-09-11T09:14:02+00:00",
                "created_at": "2026-09-11T09:14:02+00:00",
                "order_id": 9182,
                "customer_wa_id": "255712345678",
                "total_amount": 3900000,
                "total_currency": "TZS"
            },
            "timestamp": "2026-09-11T09:14:02+00:00"
        },
        "created_at": "2026-09-11T09:14:02+00:00"
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}
422The payload failed validation. `errors` maps each rejected field to its messages.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Validation failed.",
    "errors": {
        "recipient": [
            "Provide recipient or recipients."
        ]
    }
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}
404No such record for this tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}

API REFERENCE / Catalogue

List a catalogue’s feeds

GET/api/v3/catalogues/{catalogue}/feeds

Every feed URL this shop pulls from, with when each last ran and whether it worked.

AuthenticationTenant API token

Path parameters

catalogueintegerrequired
Catalogue (shop) id, as returned by `GET /api/v3/catalogues`.

Example: 1

Responses

200The feeds.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
The feeds.
Show child properties
itemsarray<object>optional
The feeds.
Show child properties
idintegerrequired
Feed id.
catalogue_idintegerrequired
The shop it fills.
urlstringrequired
Where the feed lives. Must be a public http(s) address; redirects are not followed.
format
uri
schedulestringrequired
How often it is pulled. `manual` only on request.
enum
["hourly","daily","manual"]
modestringrequired
`replace` (the default) treats the feed as the whole catalogue and retires anything missing from it; `upsert` leaves unmentioned products alone.
enum
["upsert","replace"]
is_activebooleanrequired
Switched off automatically after ten failed pulls in a row; switch it back on once fixed.
mappingobject | nulloptional
Field key → column position. Guessed from the headers on the first pull and kept; send your own to correct it.
additionalProperties
{"type":"integer"}
last_pulled_atstring | nulloptional
When it was last fetched.
format
date-time
next_pull_atstring | nulloptional
When it is next due.
format
date-time
last_sync_idinteger | nulloptional
The sync report from the last pull — read it at `GET /catalogues/{catalogue}/syncs/{sync}`.
last_errorstring | nulloptional
Why the last pull failed, if it did.
consecutive_failuresintegeroptional
Failed pulls since the last good one.
created_atstring | nulloptional
When it was registered.
format
date-time
updated_atstring | nulloptional
When it last changed.
format
date-time
{
    "status": "success",
    "data": {
        "items": [
            {
                "id": 3,
                "catalogue_id": 42,
                "url": "https://shop.example.com/google-feed.xml",
                "schedule": "daily",
                "mode": "replace",
                "is_active": true,
                "mapping": {
                    "retailer_id": 0,
                    "name": 1,
                    "price": 5,
                    "availability": 7
                },
                "last_pulled_at": "2026-09-12T02:00:41+00:00",
                "next_pull_at": "2026-09-13T02:00:41+00:00",
                "last_sync_id": 812,
                "last_error": null,
                "consecutive_failures": 0,
                "created_at": "2026-09-01T08:15:00+00:00",
                "updated_at": "2026-09-12T02:00:41+00:00"
            }
        ]
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}
404No such record for this tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}
422The payload failed validation. `errors` maps each rejected field to its messages.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Validation failed.",
    "errors": {
        "recipient": [
            "Provide recipient or recipients."
        ]
    }
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Catalogue

Register a feed

POST/api/v3/catalogues/{catalogue}/feeds

Point the shop at a product feed. It is pulled at once, and the first pull tells you whether the column mapping was guessed right — read last_sync_id. A feed is one-way, so the shop should be under stock_policy: external: the store owns the count and the feed states it.

AuthenticationTenant API token

Path parameters

catalogueintegerrequired
Catalogue (shop) id, as returned by `GET /api/v3/catalogues`.

Example: 1

Request body

application/json · required

urlstringrequired
The feed address. Google Shopping / Meta XML, or CSV, or JSON — detected from the content.
format
uri
maxLength
2048
schedulestringoptional
How often to pull.
enum
["hourly","daily","manual"]
default
daily
modestringoptional
Whether the feed is the whole catalogue.
enum
["upsert","replace"]
default
replace
is_activebooleanoptional
Switch it on or off. Turning it back on clears the failure count.
mappingobject | nulloptional
Field key → column position, to override the guess.
additionalProperties
{"type":"integer"}
Complete request schema
{
    "type": "object",
    "properties": {
        "url": {
            "type": "string",
            "format": "uri",
            "maxLength": 2048,
            "description": "The feed address. Google Shopping / Meta XML, or CSV, or JSON \u2014 detected from the content."
        },
        "schedule": {
            "type": "string",
            "enum": [
                "hourly",
                "daily",
                "manual"
            ],
            "default": "daily",
            "description": "How often to pull."
        },
        "mode": {
            "type": "string",
            "enum": [
                "upsert",
                "replace"
            ],
            "default": "replace",
            "description": "Whether the feed is the whole catalogue."
        },
        "is_active": {
            "type": "boolean",
            "description": "Switch it on or off. Turning it back on clears the failure count."
        },
        "mapping": {
            "type": [
                "object",
                "null"
            ],
            "additionalProperties": {
                "type": "integer"
            },
            "description": "Field key \u2192 column position, to override the guess."
        }
    },
    "required": [
        "url"
    ]
}

Responses

201The feed, queued for its first pull.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
The feed.
Show child properties
idintegerrequired
Feed id.
catalogue_idintegerrequired
The shop it fills.
urlstringrequired
Where the feed lives. Must be a public http(s) address; redirects are not followed.
format
uri
schedulestringrequired
How often it is pulled. `manual` only on request.
enum
["hourly","daily","manual"]
modestringrequired
`replace` (the default) treats the feed as the whole catalogue and retires anything missing from it; `upsert` leaves unmentioned products alone.
enum
["upsert","replace"]
is_activebooleanrequired
Switched off automatically after ten failed pulls in a row; switch it back on once fixed.
mappingobject | nulloptional
Field key → column position. Guessed from the headers on the first pull and kept; send your own to correct it.
additionalProperties
{"type":"integer"}
last_pulled_atstring | nulloptional
When it was last fetched.
format
date-time
next_pull_atstring | nulloptional
When it is next due.
format
date-time
last_sync_idinteger | nulloptional
The sync report from the last pull — read it at `GET /catalogues/{catalogue}/syncs/{sync}`.
last_errorstring | nulloptional
Why the last pull failed, if it did.
consecutive_failuresintegeroptional
Failed pulls since the last good one.
created_atstring | nulloptional
When it was registered.
format
date-time
updated_atstring | nulloptional
When it last changed.
format
date-time
{
    "status": "success",
    "data": {
        "id": 3,
        "catalogue_id": 42,
        "url": "https://shop.example.com/google-feed.xml",
        "schedule": "daily",
        "mode": "replace",
        "is_active": true,
        "mapping": {
            "retailer_id": 0,
            "name": 1,
            "price": 5,
            "availability": 7
        },
        "last_pulled_at": "2026-09-12T02:00:41+00:00",
        "next_pull_at": "2026-09-13T02:00:41+00:00",
        "last_sync_id": 812,
        "last_error": null,
        "consecutive_failures": 0,
        "created_at": "2026-09-01T08:15:00+00:00",
        "updated_at": "2026-09-12T02:00:41+00:00"
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}
404No such record for this tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}
422The payload failed validation. `errors` maps each rejected field to its messages.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Validation failed.",
    "errors": {
        "recipient": [
            "Provide recipient or recipients."
        ]
    }
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Catalogue

Change a feed

PATCH/api/v3/catalogues/{catalogue}/feeds/{feed}

Change the URL, schedule, mode or mapping, or switch it on or off. A new URL clears the mapping so it is guessed afresh.

AuthenticationTenant API token

Path parameters

catalogueintegerrequired
Catalogue (shop) id, as returned by `GET /api/v3/catalogues`.

Example: 1

feedintegerrequired
Feed id.

Example: 3

Request body

application/json · required

urlstringoptional
The feed address. Google Shopping / Meta XML, or CSV, or JSON — detected from the content.
format
uri
maxLength
2048
schedulestringoptional
How often to pull.
enum
["hourly","daily","manual"]
default
daily
modestringoptional
Whether the feed is the whole catalogue.
enum
["upsert","replace"]
default
replace
is_activebooleanoptional
Switch it on or off. Turning it back on clears the failure count.
mappingobject | nulloptional
Field key → column position, to override the guess.
additionalProperties
{"type":"integer"}
Complete request schema
{
    "type": "object",
    "properties": {
        "url": {
            "type": "string",
            "format": "uri",
            "maxLength": 2048,
            "description": "The feed address. Google Shopping / Meta XML, or CSV, or JSON \u2014 detected from the content."
        },
        "schedule": {
            "type": "string",
            "enum": [
                "hourly",
                "daily",
                "manual"
            ],
            "default": "daily",
            "description": "How often to pull."
        },
        "mode": {
            "type": "string",
            "enum": [
                "upsert",
                "replace"
            ],
            "default": "replace",
            "description": "Whether the feed is the whole catalogue."
        },
        "is_active": {
            "type": "boolean",
            "description": "Switch it on or off. Turning it back on clears the failure count."
        },
        "mapping": {
            "type": [
                "object",
                "null"
            ],
            "additionalProperties": {
                "type": "integer"
            },
            "description": "Field key \u2192 column position, to override the guess."
        }
    }
}

Responses

200The updated feed.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
The feed.
Show child properties
idintegerrequired
Feed id.
catalogue_idintegerrequired
The shop it fills.
urlstringrequired
Where the feed lives. Must be a public http(s) address; redirects are not followed.
format
uri
schedulestringrequired
How often it is pulled. `manual` only on request.
enum
["hourly","daily","manual"]
modestringrequired
`replace` (the default) treats the feed as the whole catalogue and retires anything missing from it; `upsert` leaves unmentioned products alone.
enum
["upsert","replace"]
is_activebooleanrequired
Switched off automatically after ten failed pulls in a row; switch it back on once fixed.
mappingobject | nulloptional
Field key → column position. Guessed from the headers on the first pull and kept; send your own to correct it.
additionalProperties
{"type":"integer"}
last_pulled_atstring | nulloptional
When it was last fetched.
format
date-time
next_pull_atstring | nulloptional
When it is next due.
format
date-time
last_sync_idinteger | nulloptional
The sync report from the last pull — read it at `GET /catalogues/{catalogue}/syncs/{sync}`.
last_errorstring | nulloptional
Why the last pull failed, if it did.
consecutive_failuresintegeroptional
Failed pulls since the last good one.
created_atstring | nulloptional
When it was registered.
format
date-time
updated_atstring | nulloptional
When it last changed.
format
date-time
{
    "status": "success",
    "data": {
        "id": 3,
        "catalogue_id": 42,
        "url": "https://shop.example.com/google-feed.xml",
        "schedule": "daily",
        "mode": "replace",
        "is_active": true,
        "mapping": {
            "retailer_id": 0,
            "name": 1,
            "price": 5,
            "availability": 7
        },
        "last_pulled_at": "2026-09-12T02:00:41+00:00",
        "next_pull_at": "2026-09-13T02:00:41+00:00",
        "last_sync_id": 812,
        "last_error": null,
        "consecutive_failures": 0,
        "created_at": "2026-09-01T08:15:00+00:00",
        "updated_at": "2026-09-12T02:00:41+00:00"
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}
404No such record for this tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}
422The payload failed validation. `errors` maps each rejected field to its messages.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Validation failed.",
    "errors": {
        "recipient": [
            "Provide recipient or recipients."
        ]
    }
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Catalogue

Remove a feed

DELETE/api/v3/catalogues/{catalogue}/feeds/{feed}

Stops pulling. Products already imported stay.

AuthenticationTenant API token

Path parameters

catalogueintegerrequired
Catalogue (shop) id, as returned by `GET /api/v3/catalogues`.

Example: 1

feedintegerrequired
Feed id.

Example: 3

Responses

200Removed.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
What was removed.
Show child properties
deletedbooleanoptional
Always true.
idintegeroptional
The id that was removed.
{
    "status": "success",
    "data": {
        "deleted": true,
        "id": 3
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}
404No such record for this tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}
422The payload failed validation. `errors` maps each rejected field to its messages.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Validation failed.",
    "errors": {
        "recipient": [
            "Provide recipient or recipients."
        ]
    }
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Catalogue

Pull a feed now

POST/api/v3/catalogues/{catalogue}/feeds/{feed}/run

Whatever the schedule says. Answers 202; read the outcome from the sync the feed's last_sync_id points at once it has run.

AuthenticationTenant API token

Path parameters

catalogueintegerrequired
Catalogue (shop) id, as returned by `GET /api/v3/catalogues`.

Example: 1

feedintegerrequired
Feed id.

Example: 3

Responses

202Queued.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
The feed.
Show child properties
idintegerrequired
Feed id.
catalogue_idintegerrequired
The shop it fills.
urlstringrequired
Where the feed lives. Must be a public http(s) address; redirects are not followed.
format
uri
schedulestringrequired
How often it is pulled. `manual` only on request.
enum
["hourly","daily","manual"]
modestringrequired
`replace` (the default) treats the feed as the whole catalogue and retires anything missing from it; `upsert` leaves unmentioned products alone.
enum
["upsert","replace"]
is_activebooleanrequired
Switched off automatically after ten failed pulls in a row; switch it back on once fixed.
mappingobject | nulloptional
Field key → column position. Guessed from the headers on the first pull and kept; send your own to correct it.
additionalProperties
{"type":"integer"}
last_pulled_atstring | nulloptional
When it was last fetched.
format
date-time
next_pull_atstring | nulloptional
When it is next due.
format
date-time
last_sync_idinteger | nulloptional
The sync report from the last pull — read it at `GET /catalogues/{catalogue}/syncs/{sync}`.
last_errorstring | nulloptional
Why the last pull failed, if it did.
consecutive_failuresintegeroptional
Failed pulls since the last good one.
created_atstring | nulloptional
When it was registered.
format
date-time
updated_atstring | nulloptional
When it last changed.
format
date-time
{
    "status": "success",
    "data": {
        "id": 3,
        "catalogue_id": 42,
        "url": "https://shop.example.com/google-feed.xml",
        "schedule": "daily",
        "mode": "replace",
        "is_active": true,
        "mapping": {
            "retailer_id": 0,
            "name": 1,
            "price": 5,
            "availability": 7
        },
        "last_pulled_at": "2026-09-12T02:00:41+00:00",
        "next_pull_at": "2026-09-13T02:00:41+00:00",
        "last_sync_id": 812,
        "last_error": null,
        "consecutive_failures": 0,
        "created_at": "2026-09-01T08:15:00+00:00",
        "updated_at": "2026-09-12T02:00:41+00:00"
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}
404No such record for this tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}
422The payload failed validation. `errors` maps each rejected field to its messages.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Validation failed.",
    "errors": {
        "recipient": [
            "Provide recipient or recipients."
        ]
    }
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Flows

List message flows

GET/api/v3/flows

The workspace's message flows as cards, newest change first. Archived flows are left out unless status=archived (or status=all) asks for them. A key issued with scopes needs flows:read.

AuthenticationTenant API token

Required permission: flows.view

Query parameters

statusstringoptional
draft, active, paused, archived, or all.

Example: active

qstringoptional
Part of the flow's name.

Example: oda

pageintegeroptional
Page number, 1-based. Read `data.pagination.has_more_pages` to know when to stop.
minimum
1
default
1

Example: 1

limitintegeroptional
Rows per page, 1–100. Defaults to 20 (25 for catalogue endpoints). Values above 100 are clamped.
minimum
1
maximum
100
default
20

Example: 20

Responses

200The flows and the page.
dataarray<object>required
The flows on this page.
Show child properties
idintegeroptional
The flow id.
namestringoptional
What the workspace calls it.
descriptionstringoptional
The author's one-line description, when written.
nullable
true
statusstringoptional
draft, active, paused or archived. Only active flows answer customers.
enum
["draft","active","paused","archived"]
priorityintegeroptional
Trigger order: lower runs first when two live flows claim the same words.
channelsarray<string>optional
The channels it runs on. WhatsApp today.
versionintegeroptional
The DRAFT's optimistic-lock counter, incremented on every save. Send it back as expected_version when publishing.
has_unpublished_changesbooleanoptional
Whether the draft differs from what is live.
published_versionintegeroptional
The number of the version customers get right now, or null when nothing is live.
nullable
true
published_atstringoptional
When the live version went live.
format
date-time
nullable
true
node_countintegeroptional
How many steps the draft has.
triggersarray<object>optional
What starts it.
Show child properties
typestringoptional
keyword, cold_start, no_session_fallback, choice_id, manual…
valuestringoptional
The trigger's single value, when it has one.
nullable
true
valuesarray<string>optional
The trigger's list of values, when it has several.
keywordsarray<string>optional
Every keyword the triggers claim, lower-cased.
created_atstringoptional
When the flow was created.
format
date-time
updated_atstringoptional
When the draft last changed.
format
date-time
definitionobjectoptional
The draft graph (nodes, entryNodeId, defaults) with secrets redacted. Only with include=definition.
additionalProperties
true
metaobjectrequired
Page numbers.
Show child properties
current_pageintegeroptional
This page.
per_pageintegeroptional
Rows per page.
totalintegeroptional
Flows matching.
last_pageintegeroptional
The last page number.
{
    "data": [
        {
            "id": 17,
            "name": "Oda ya chakula",
            "description": "Takes a food order and collects payment.",
            "status": "active",
            "priority": 10,
            "channels": [
                "whatsapp"
            ],
            "version": 42,
            "has_unpublished_changes": false,
            "published_version": 3,
            "published_at": "2026-09-12T09:14:02+03:00",
            "node_count": 18,
            "triggers": [
                {
                    "type": "keyword",
                    "value": "oda",
                    "values": [
                        "oda",
                        "order"
                    ]
                }
            ],
            "keywords": [
                "oda",
                "order"
            ],
            "created_at": "2026-08-30T10:00:00+03:00",
            "updated_at": "2026-09-12T09:14:02+03:00"
        }
    ],
    "meta": {
        "current_page": 1,
        "per_page": 25,
        "total": 1,
        "last_page": 1
    }
}
default
{
    "data": [
        {
            "id": 17,
            "name": "Oda ya chakula",
            "description": "Takes a food order and collects payment.",
            "status": "active",
            "priority": 10,
            "channels": [
                "whatsapp"
            ],
            "version": 42,
            "has_unpublished_changes": false,
            "published_version": 3,
            "published_at": "2026-09-12T09:14:02+03:00",
            "node_count": 18,
            "triggers": [
                {
                    "type": "keyword",
                    "value": "oda",
                    "values": [
                        "oda",
                        "order"
                    ]
                }
            ],
            "keywords": [
                "oda",
                "order"
            ],
            "created_at": "2026-08-30T10:00:00+03:00",
            "updated_at": "2026-09-12T09:14:02+03:00"
        }
    ],
    "meta": {
        "current_page": 1,
        "per_page": 25,
        "total": 1,
        "last_page": 1
    }
}
422An unknown status.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Unknown status \"live\". One of: draft, active, paused, archived."
}
default
{
    "status": "error",
    "message": "Unknown status \"live\". One of: draft, active, paused, archived."
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The key's issuer does not hold flows.view, the key has no issuer on record, or the key was issued without the scope this call needs.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This API key was issued by a user without the \"flows.view\" permission."
}
default
{
    "status": "error",
    "message": "This API key was issued by a user without the \"flows.view\" permission."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Flows

Read one flow

GET/api/v3/flows/{flow}

One flow's card; include=definition adds the draft graph with secrets redacted. The version on the card is what a publish must send back as expected_version.

AuthenticationTenant API token

Required permission: flows.view

Path parameters

flowintegerrequired
The flow id.

Example: 17

Query parameters

includestringoptional
Comma-separated extras: definition.

Example: definition

Responses

200The flow.
dataobjectrequired
The flow.
Show child properties
idintegeroptional
The flow id.
namestringoptional
What the workspace calls it.
descriptionstringoptional
The author's one-line description, when written.
nullable
true
statusstringoptional
draft, active, paused or archived. Only active flows answer customers.
enum
["draft","active","paused","archived"]
priorityintegeroptional
Trigger order: lower runs first when two live flows claim the same words.
channelsarray<string>optional
The channels it runs on. WhatsApp today.
versionintegeroptional
The DRAFT's optimistic-lock counter, incremented on every save. Send it back as expected_version when publishing.
has_unpublished_changesbooleanoptional
Whether the draft differs from what is live.
published_versionintegeroptional
The number of the version customers get right now, or null when nothing is live.
nullable
true
published_atstringoptional
When the live version went live.
format
date-time
nullable
true
node_countintegeroptional
How many steps the draft has.
triggersarray<object>optional
What starts it.
Show child properties
typestringoptional
keyword, cold_start, no_session_fallback, choice_id, manual…
valuestringoptional
The trigger's single value, when it has one.
nullable
true
valuesarray<string>optional
The trigger's list of values, when it has several.
keywordsarray<string>optional
Every keyword the triggers claim, lower-cased.
created_atstringoptional
When the flow was created.
format
date-time
updated_atstringoptional
When the draft last changed.
format
date-time
definitionobjectoptional
The draft graph (nodes, entryNodeId, defaults) with secrets redacted. Only with include=definition.
additionalProperties
true
{
    "data": {
        "id": 17,
        "name": "Oda ya chakula",
        "description": "Takes a food order and collects payment.",
        "status": "active",
        "priority": 10,
        "channels": [
            "whatsapp"
        ],
        "version": 42,
        "has_unpublished_changes": false,
        "published_version": 3,
        "published_at": "2026-09-12T09:14:02+03:00",
        "node_count": 18,
        "triggers": [
            {
                "type": "keyword",
                "value": "oda",
                "values": [
                    "oda",
                    "order"
                ]
            }
        ],
        "keywords": [
            "oda",
            "order"
        ],
        "created_at": "2026-08-30T10:00:00+03:00",
        "updated_at": "2026-09-12T09:14:02+03:00"
    }
}
default
{
    "data": {
        "id": 17,
        "name": "Oda ya chakula",
        "description": "Takes a food order and collects payment.",
        "status": "active",
        "priority": 10,
        "channels": [
            "whatsapp"
        ],
        "version": 42,
        "has_unpublished_changes": false,
        "published_version": 3,
        "published_at": "2026-09-12T09:14:02+03:00",
        "node_count": 18,
        "triggers": [
            {
                "type": "keyword",
                "value": "oda",
                "values": [
                    "oda",
                    "order"
                ]
            }
        ],
        "keywords": [
            "oda",
            "order"
        ],
        "created_at": "2026-08-30T10:00:00+03:00",
        "updated_at": "2026-09-12T09:14:02+03:00"
    }
}
404No such flow or session in this workspace. Another workspace's answers 404, never 403.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}
default
{
    "status": "error",
    "message": "Resource not found."
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The key's issuer does not hold flows.view, the key has no issuer on record, or the key was issued without the scope this call needs.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This API key was issued by a user without the \"flows.view\" permission."
}
default
{
    "status": "error",
    "message": "This API key was issued by a user without the \"flows.view\" permission."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Flows

Publish the draft

POST/api/v3/flows/{flow}/publish

Makes the draft live for real customers, after the same validation, scenario tests and go-live readiness checks the builder runs. A draft that equals what is live answers unchanged:true and mints nothing. Needs the flows:publish scope on a scoped key.

AuthenticationTenant API token

Required permission: flows.publish

Path parameters

flowintegerrequired
The flow id.

Example: 17

Request body

application/json · required

expected_versionintegerrequired
The draft version you read on the flow card. A draft somebody changed since answers 409 rather than publishing a graph nobody checked.
Complete request schema
{
    "type": "object",
    "required": [
        "expected_version"
    ],
    "properties": {
        "expected_version": {
            "type": "integer",
            "description": "The draft version you read on the flow card. A draft somebody changed since answers 409 rather than publishing a graph nobody checked."
        }
    }
}

Responses

200Published, or nothing to publish.
dataobjectrequired
What happened.
Show child properties
publishedbooleanoptional
A new version went live.
unchangedbooleanoptional
The draft equalled the live version; nothing was minted.
version_numberintegeroptional
The version now live.
nullable
true
statusstringoptional
The flow's status after the publish.
issuesarray<object>optional
Warnings that did not block.
Show child properties
levelstringoptional
error (refuses a publish) or warning.
enum
["error","warning"]
node_idstringoptional
The step the finding is about, when it is about one.
nullable
true
fieldstringoptional
The step field, when the finding anchors to one.
nullable
true
codestringoptional
A stable code; readiness:* names an account setting rather than the graph.
nullable
true
messagestringoptional
The finding in plain words.
fix_hrefstringoptional
The page in the app that fixes a readiness finding.
nullable
true
{
    "data": {
        "published": true,
        "unchanged": false,
        "version_number": 4,
        "status": "active",
        "issues": []
    }
}
default
{
    "data": {
        "published": true,
        "unchanged": false,
        "version_number": 4,
        "status": "active",
        "issues": []
    }
}
404No such flow or session in this workspace. Another workspace's answers 404, never 403.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}
default
{
    "status": "error",
    "message": "Resource not found."
}
409The draft moved: expected_version is not the draft's version.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "The draft is at version 43, not 42. Read the flow again and publish with the version it answers."
}
default
{
    "status": "error",
    "message": "The draft is at version 43, not 42. Read the flow again and publish with the version it answers."
}
422Refused: graph errors, failed scenario tests or readiness blockers. `issues` names each.
statusstringoptional
Always "error".
messagestringoptional
What to do.
issuesarray<object>optional
Every finding, errors and warnings.
Show child properties
levelstringoptional
error (refuses a publish) or warning.
enum
["error","warning"]
node_idstringoptional
The step the finding is about, when it is about one.
nullable
true
fieldstringoptional
The step field, when the finding anchors to one.
nullable
true
codestringoptional
A stable code; readiness:* names an account setting rather than the graph.
nullable
true
messagestringoptional
The finding in plain words.
fix_hrefstringoptional
The page in the app that fixes a readiness finding.
nullable
true
{
    "status": "error",
    "message": "Nothing went live. Fix the issues and publish again; a `readiness:*` issue is an account setting fixed at its fix_href.",
    "issues": [
        {
            "level": "error",
            "node_id": null,
            "field": null,
            "code": "readiness:whatsapp_channel",
            "message": "No WhatsApp number is connected to this workspace, so the flow has nothing to speak from.",
            "fix_href": "/app/accounts"
        }
    ]
}
default
{
    "status": "error",
    "message": "Nothing went live. Fix the issues and publish again; a `readiness:*` issue is an account setting fixed at its fix_href.",
    "issues": [
        {
            "level": "error",
            "node_id": null,
            "field": null,
            "code": "readiness:whatsapp_channel",
            "message": "No WhatsApp number is connected to this workspace, so the flow has nothing to speak from.",
            "fix_href": "/app/accounts"
        }
    ]
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The key's issuer does not hold flows.publish, the key has no issuer on record, or the key was issued without the scope this call needs.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This API key was issued by a user without the \"flows.publish\" permission."
}
default
{
    "status": "error",
    "message": "This API key was issued by a user without the \"flows.publish\" permission."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Flows

Export a flow as a bundle

GET/api/v3/flows/{flow}/export

The flow as a marketplace bundle — the same file the builder's Export menu downloads and its Import reads. Workspace-specific references (tables, profiles, templates) come out as placeholders the importer fills.

AuthenticationTenant API token

Required permission: flows.view

Path parameters

flowintegerrequired
The flow id.

Example: 17

Responses

200The bundle.
{
    "format": "momo.marketplace.bundle",
    "version": 1,
    "exported_at": "2026-09-12T09:14:02+03:00",
    "artifacts": [
        {
            "kind": "flow",
            "name": "Oda ya chakula",
            "definition": {
                "nodes": []
            }
        }
    ],
    "requires": []
}
default
{
    "format": "momo.marketplace.bundle",
    "version": 1,
    "exported_at": "2026-09-12T09:14:02+03:00",
    "artifacts": [
        {
            "kind": "flow",
            "name": "Oda ya chakula",
            "definition": {
                "nodes": []
            }
        }
    ],
    "requires": []
}
404No such flow or session in this workspace. Another workspace's answers 404, never 403.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}
default
{
    "status": "error",
    "message": "Resource not found."
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The key's issuer does not hold flows.view, the key has no issuer on record, or the key was issued without the scope this call needs.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This API key was issued by a user without the \"flows.view\" permission."
}
default
{
    "status": "error",
    "message": "This API key was issued by a user without the \"flows.view\" permission."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Flows

List a flow's sessions

GET/api/v3/flows/{flow}/sessions

This flow's sessions, newest first, with the support filters. Cursor-paginated: pass meta.next_cursor as cursor for the next page. No variables ride a list row.

AuthenticationTenant API token

Required permission: flows.view

Path parameters

flowintegerrequired
The flow id.

Example: 17

Query parameters

phonestringoptional
The customer's phone in any shape (0712…, 255712…, +255712…); matched exactly against the stored identifier.

Example: 255712345678

statusstringoptional
One session status.

Example: waiting

outcomestringoptional
One outcome word (in_progress, completed, abandoned, handed_over, failed, expired, cancelled, ended_by_operator).

Example: completed

nodestringoptional
Sessions on this step.

Example: ask_name

ended_reasonstringoptional
The runtime's ended reason.

Example: timeout

versionintegeroptional
Sessions pinned to this published version id.

Example: 91

fromstringoptional
Started on or after this day (YYYY-MM-DD). `GET /api/v3/whatsapp/accounts` lists the accounts and their numbers (`phone_numbers[].id`, or the number itself).

Example: 2026-09-01

tostringoptional
Started on or before this day (YYYY-MM-DD).

Example: 2026-09-12

limitintegeroptional
Rows per page, at most 100.
default
25
cursorstringoptional
The next_cursor of the previous page.

Example: eyJtZXNzYWdlX2Zsb3dfc2Vzc2lvbnMuaWQiOjUxMTksIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0

Responses

200The sessions.
dataarray<object>required
The sessions, newest first.
Show child properties
idintegeroptional
The session id.
flow_idintegeroptional
The flow it runs.
flow_namestringoptional
The flow's name at read time.
flow_versionintegeroptional
The published version number the session is pinned to.
nullable
true
conversation_idintegeroptional
The inbox conversation it lives in.
contact_idintegeroptional
The contact record, when the conversation has one.
nullable
true
contact_namestringoptional
The customer's name as the inbox knows it.
nullable
true
contact_identifierstringoptional
The customer's phone (or handle) as stored on the conversation.
triggerstringoptional
What started it: inbound, manual, api, schedule, automation…
statusstringoptional
running, waiting, completed, failed, expired, superseded_by_human or cancelled.
outcomestringoptional
The eight-word reading of the status for people: in_progress, completed, abandoned, handed_over, failed, expired, cancelled, ended_by_operator.
ended_reasonstringoptional
The runtime's own word for why it ended, when it has.
nullable
true
node_idstringoptional
The step it is on, or ended on.
nullable
true
awaitingstringoptional
What a waiting session waits for: text, choice, media, location, form, timer, payment…
nullable
true
turnsintegeroptional
How many customer turns it has taken.
resume_atstringoptional
When a timer wakes it, if one will.
format
date-time
nullable
true
expires_atstringoptional
When it lapses if the customer says nothing.
format
date-time
nullable
true
started_atstringoptional
When it started.
format
date-time
ended_atstringoptional
When it ended, or null while live.
format
date-time
nullable
true
metaobjectrequired
Cursor paging.
Show child properties
per_pageintegeroptional
Rows per page.
next_cursorstringoptional
Pass as cursor for the next page; null on the last.
nullable
true
{
    "data": [
        {
            "id": 5120,
            "flow_id": 17,
            "flow_name": "Oda ya chakula",
            "flow_version": 3,
            "conversation_id": 771,
            "contact_id": 2201,
            "contact_name": "Asha Mrisho",
            "contact_identifier": "255712345678",
            "trigger": "inbound",
            "status": "completed",
            "outcome": "completed",
            "ended_reason": "completed",
            "node_id": "done",
            "awaiting": null,
            "turns": 6,
            "resume_at": null,
            "expires_at": null,
            "started_at": "2026-09-12T09:01:12+03:00",
            "ended_at": "2026-09-12T09:14:02+03:00"
        }
    ],
    "meta": {
        "per_page": 25,
        "next_cursor": null
    }
}
default
{
    "data": [
        {
            "id": 5120,
            "flow_id": 17,
            "flow_name": "Oda ya chakula",
            "flow_version": 3,
            "conversation_id": 771,
            "contact_id": 2201,
            "contact_name": "Asha Mrisho",
            "contact_identifier": "255712345678",
            "trigger": "inbound",
            "status": "completed",
            "outcome": "completed",
            "ended_reason": "completed",
            "node_id": "done",
            "awaiting": null,
            "turns": 6,
            "resume_at": null,
            "expires_at": null,
            "started_at": "2026-09-12T09:01:12+03:00",
            "ended_at": "2026-09-12T09:14:02+03:00"
        }
    ],
    "meta": {
        "per_page": 25,
        "next_cursor": null
    }
}
404No such flow or session in this workspace. Another workspace's answers 404, never 403.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}
default
{
    "status": "error",
    "message": "Resource not found."
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The key's issuer does not hold flows.view, the key has no issuer on record, or the key was issued without the scope this call needs.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This API key was issued by a user without the \"flows.view\" permission."
}
default
{
    "status": "error",
    "message": "This API key was issued by a user without the \"flows.view\" permission."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Flows

Start a flow for a customer

POST/api/v3/flows/{flow}/sessions

Queues the live flow on an existing WhatsApp conversation — by the customer's phone or the conversation id — with optional starting variables. Answers 202: the session appears once the runner takes its first step, so read the sessions list or subscribe to the flow.session.started webhook. A thread a person has silenced is respected. Needs flows:write on a scoped key.

AuthenticationTenant API token

Required permission: flows.edit

Path parameters

flowintegerrequired
The flow id.

Example: 17

Request body

application/json · required

phonestringoptional
The customer's WhatsApp number in any shape. Required unless conversation_id is given.
conversation_idintegeroptional
The inbox conversation to start on. Required unless phone is given.
variablesobjectoptional
Up to 32 starting variables (strings, numbers, booleans), readable in the flow as {{name}}. Names are letters, digits and underscores.
additionalProperties
true
Complete request schema
{
    "type": "object",
    "properties": {
        "phone": {
            "type": "string",
            "description": "The customer's WhatsApp number in any shape. Required unless conversation_id is given."
        },
        "conversation_id": {
            "type": "integer",
            "description": "The inbox conversation to start on. Required unless phone is given."
        },
        "variables": {
            "type": "object",
            "additionalProperties": true,
            "description": "Up to 32 starting variables (strings, numbers, booleans), readable in the flow as {{name}}. Names are letters, digits and underscores."
        }
    }
}

Responses

202Queued.
dataobjectrequired
The start, queued.
Show child properties
queuedbooleanoptional
Always true.
flow_idintegeroptional
The flow.
conversation_idintegeroptional
The conversation it starts on.
contact_identifierstringoptional
The customer's stored identifier.
{
    "data": {
        "queued": true,
        "flow_id": 17,
        "conversation_id": 771,
        "contact_identifier": "255712345678"
    }
}
default
{
    "data": {
        "queued": true,
        "flow_id": 17,
        "conversation_id": 771,
        "contact_identifier": "255712345678"
    }
}
404No such flow or session in this workspace. Another workspace's answers 404, never 403.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}
default
{
    "status": "error",
    "message": "Resource not found."
}
422The flow is not live, no conversation with that customer exists, or a variable is not allowed.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This flow is not live: publish it (and resume it if paused) before starting it for a customer."
}
default
{
    "status": "error",
    "message": "This flow is not live: publish it (and resume it if paused) before starting it for a customer."
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The key's issuer does not hold flows.edit, the key has no issuer on record, or the key was issued without the scope this call needs.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This API key was issued by a user without the \"flows.edit\" permission."
}
default
{
    "status": "error",
    "message": "This API key was issued by a user without the \"flows.edit\" permission."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Flows

Find sessions by customer

GET/api/v3/flow-sessions

Sessions across every flow, found by the customer's phone (or narrowed to one flow with flow). The same filters and cursor as a flow's session list.

AuthenticationTenant API token

Required permission: flows.view

Query parameters

phonestringoptional
The customer's phone in any shape (0712…, 255712…, +255712…); matched exactly against the stored identifier.

Example: 255712345678

statusstringoptional
One session status.

Example: waiting

outcomestringoptional
One outcome word (in_progress, completed, abandoned, handed_over, failed, expired, cancelled, ended_by_operator).

Example: completed

nodestringoptional
Sessions on this step.

Example: ask_name

ended_reasonstringoptional
The runtime's ended reason.

Example: timeout

versionintegeroptional
Sessions pinned to this published version id.

Example: 91

fromstringoptional
Started on or after this day (YYYY-MM-DD). `GET /api/v3/whatsapp/accounts` lists the accounts and their numbers (`phone_numbers[].id`, or the number itself).

Example: 2026-09-01

tostringoptional
Started on or before this day (YYYY-MM-DD).

Example: 2026-09-12

limitintegeroptional
Rows per page, at most 100.
default
25
cursorstringoptional
The next_cursor of the previous page.

Example: eyJtZXNzYWdlX2Zsb3dfc2Vzc2lvbnMuaWQiOjUxMTksIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0

flowintegeroptional
Narrow to one flow id.

Example: 17

Responses

200The sessions.
dataarray<object>required
The sessions, newest first.
Show child properties
idintegeroptional
The session id.
flow_idintegeroptional
The flow it runs.
flow_namestringoptional
The flow's name at read time.
flow_versionintegeroptional
The published version number the session is pinned to.
nullable
true
conversation_idintegeroptional
The inbox conversation it lives in.
contact_idintegeroptional
The contact record, when the conversation has one.
nullable
true
contact_namestringoptional
The customer's name as the inbox knows it.
nullable
true
contact_identifierstringoptional
The customer's phone (or handle) as stored on the conversation.
triggerstringoptional
What started it: inbound, manual, api, schedule, automation…
statusstringoptional
running, waiting, completed, failed, expired, superseded_by_human or cancelled.
outcomestringoptional
The eight-word reading of the status for people: in_progress, completed, abandoned, handed_over, failed, expired, cancelled, ended_by_operator.
ended_reasonstringoptional
The runtime's own word for why it ended, when it has.
nullable
true
node_idstringoptional
The step it is on, or ended on.
nullable
true
awaitingstringoptional
What a waiting session waits for: text, choice, media, location, form, timer, payment…
nullable
true
turnsintegeroptional
How many customer turns it has taken.
resume_atstringoptional
When a timer wakes it, if one will.
format
date-time
nullable
true
expires_atstringoptional
When it lapses if the customer says nothing.
format
date-time
nullable
true
started_atstringoptional
When it started.
format
date-time
ended_atstringoptional
When it ended, or null while live.
format
date-time
nullable
true
metaobjectrequired
Cursor paging.
Show child properties
per_pageintegeroptional
Rows per page.
next_cursorstringoptional
Pass as cursor for the next page; null on the last.
nullable
true
{
    "data": [
        {
            "id": 5120,
            "flow_id": 17,
            "flow_name": "Oda ya chakula",
            "flow_version": 3,
            "conversation_id": 771,
            "contact_id": 2201,
            "contact_name": "Asha Mrisho",
            "contact_identifier": "255712345678",
            "trigger": "inbound",
            "status": "completed",
            "outcome": "completed",
            "ended_reason": "completed",
            "node_id": "done",
            "awaiting": null,
            "turns": 6,
            "resume_at": null,
            "expires_at": null,
            "started_at": "2026-09-12T09:01:12+03:00",
            "ended_at": "2026-09-12T09:14:02+03:00"
        }
    ],
    "meta": {
        "per_page": 25,
        "next_cursor": null
    }
}
default
{
    "data": [
        {
            "id": 5120,
            "flow_id": 17,
            "flow_name": "Oda ya chakula",
            "flow_version": 3,
            "conversation_id": 771,
            "contact_id": 2201,
            "contact_name": "Asha Mrisho",
            "contact_identifier": "255712345678",
            "trigger": "inbound",
            "status": "completed",
            "outcome": "completed",
            "ended_reason": "completed",
            "node_id": "done",
            "awaiting": null,
            "turns": 6,
            "resume_at": null,
            "expires_at": null,
            "started_at": "2026-09-12T09:01:12+03:00",
            "ended_at": "2026-09-12T09:14:02+03:00"
        }
    ],
    "meta": {
        "per_page": 25,
        "next_cursor": null
    }
}
422Neither a phone nor a flow was given.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Give a `phone` to find a customer's sessions, or a `flow` id."
}
default
{
    "status": "error",
    "message": "Give a `phone` to find a customer's sessions, or a `flow` id."
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The key's issuer does not hold flows.view, the key has no issuer on record, or the key was issued without the scope this call needs.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This API key was issued by a user without the \"flows.view\" permission."
}
default
{
    "status": "error",
    "message": "This API key was issued by a user without the \"flows.view\" permission."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Flows

Read one session and its trace

GET/api/v3/flow-sessions/{session}

The session card and its step-by-step trace, redacted. variables is the session's variable bag ONLY for a key whose issuer holds flows.sessions.pii (the same line the app draws), and that read is written to the audit log; otherwise it is null.

AuthenticationTenant API token

Required permission: flows.view

Path parameters

sessionintegerrequired
The session id.

Example: 5120

Responses

200The session, its trace and — when permitted — its variables.
dataobjectrequired
The session in full.
Show child properties
sessionobjectoptional
The session card.
Show child properties
idintegeroptional
The session id.
flow_idintegeroptional
The flow it runs.
flow_namestringoptional
The flow's name at read time.
flow_versionintegeroptional
The published version number the session is pinned to.
nullable
true
conversation_idintegeroptional
The inbox conversation it lives in.
contact_idintegeroptional
The contact record, when the conversation has one.
nullable
true
contact_namestringoptional
The customer's name as the inbox knows it.
nullable
true
contact_identifierstringoptional
The customer's phone (or handle) as stored on the conversation.
triggerstringoptional
What started it: inbound, manual, api, schedule, automation…
statusstringoptional
running, waiting, completed, failed, expired, superseded_by_human or cancelled.
outcomestringoptional
The eight-word reading of the status for people: in_progress, completed, abandoned, handed_over, failed, expired, cancelled, ended_by_operator.
ended_reasonstringoptional
The runtime's own word for why it ended, when it has.
nullable
true
node_idstringoptional
The step it is on, or ended on.
nullable
true
awaitingstringoptional
What a waiting session waits for: text, choice, media, location, form, timer, payment…
nullable
true
turnsintegeroptional
How many customer turns it has taken.
resume_atstringoptional
When a timer wakes it, if one will.
format
date-time
nullable
true
expires_atstringoptional
When it lapses if the customer says nothing.
format
date-time
nullable
true
started_atstringoptional
When it started.
format
date-time
ended_atstringoptional
When it ended, or null while live.
format
date-time
nullable
true
stepsarray<object>optional
The trace, oldest first, at most 1000 rows.
Show child properties
seqintegeroptional
The step's sequence number within the session.
node_idstringoptional
The step of the flow.
nullable
true
node_kindstringoptional
The step's kind (send_text, ask_text, collect_payment…).
nullable
true
kindstringoptional
entered, emitted, awaited, resumed, branched, error, skipped or ended.
payloadobjectoptional
What the step recorded, with customer-typed values redacted.
additionalProperties
true
nullable
true
duration_msintegeroptional
How long the step took.
nullable
true
atstringoptional
When it happened.
format
date-time
nullable
true
variablesobjectoptional
The variable bag, redacted, or null when the key may not read it.
additionalProperties
true
nullable
true
exported_atstringoptional
When this read happened.
format
date-time
{
    "data": {
        "session": {
            "id": 5120,
            "flow_id": 17,
            "flow_name": "Oda ya chakula",
            "flow_version": 3,
            "conversation_id": 771,
            "contact_id": 2201,
            "contact_name": "Asha Mrisho",
            "contact_identifier": "255712345678",
            "trigger": "inbound",
            "status": "completed",
            "outcome": "completed",
            "ended_reason": "completed",
            "node_id": "done",
            "awaiting": null,
            "turns": 6,
            "resume_at": null,
            "expires_at": null,
            "started_at": "2026-09-12T09:01:12+03:00",
            "ended_at": "2026-09-12T09:14:02+03:00"
        },
        "steps": [
            {
                "seq": 3,
                "node_id": "ask_name",
                "node_kind": "ask_text",
                "kind": "awaited",
                "payload": {
                    "kind": "text"
                },
                "duration_ms": 12,
                "at": "2026-09-12T09:14:02+03:00"
            }
        ],
        "variables": null,
        "exported_at": "2026-09-12T09:14:02+03:00"
    }
}
default
{
    "data": {
        "session": {
            "id": 5120,
            "flow_id": 17,
            "flow_name": "Oda ya chakula",
            "flow_version": 3,
            "conversation_id": 771,
            "contact_id": 2201,
            "contact_name": "Asha Mrisho",
            "contact_identifier": "255712345678",
            "trigger": "inbound",
            "status": "completed",
            "outcome": "completed",
            "ended_reason": "completed",
            "node_id": "done",
            "awaiting": null,
            "turns": 6,
            "resume_at": null,
            "expires_at": null,
            "started_at": "2026-09-12T09:01:12+03:00",
            "ended_at": "2026-09-12T09:14:02+03:00"
        },
        "steps": [
            {
                "seq": 3,
                "node_id": "ask_name",
                "node_kind": "ask_text",
                "kind": "awaited",
                "payload": {
                    "kind": "text"
                },
                "duration_ms": 12,
                "at": "2026-09-12T09:14:02+03:00"
            }
        ],
        "variables": null,
        "exported_at": "2026-09-12T09:14:02+03:00"
    }
}
404No such flow or session in this workspace. Another workspace's answers 404, never 403.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}
default
{
    "status": "error",
    "message": "Resource not found."
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The key's issuer does not hold flows.view, the key has no issuer on record, or the key was issued without the scope this call needs.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This API key was issued by a user without the \"flows.view\" permission."
}
default
{
    "status": "error",
    "message": "This API key was issued by a user without the \"flows.view\" permission."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Flows

Download a session's trace as CSV

GET/api/v3/flow-sessions/{session}/export

One row per step: seq, at, node_id, node_kind, kind, detail (JSON), duration_ms. Never carries variables.

AuthenticationTenant API token

Required permission: flows.view

Path parameters

sessionintegerrequired
The session id.

Example: 5120

Responses

200The CSV.
"seq,at,node_id,node_kind,kind,detail,duration_ms\n1,2026-09-12T09:01:12+03:00,hi,send_text,entered,{},3\n"
404No such flow or session in this workspace. Another workspace's answers 404, never 403.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}
default
{
    "status": "error",
    "message": "Resource not found."
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The key's issuer does not hold flows.view, the key has no issuer on record, or the key was issued without the scope this call needs.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This API key was issued by a user without the \"flows.view\" permission."
}
default
{
    "status": "error",
    "message": "This API key was issued by a user without the \"flows.view\" permission."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Flows

End a live session

POST/api/v3/flow-sessions/{session}/end

Ends a running or waiting session as an operator would; the conversation goes back to the inbox. notify_customer sends the customer one line saying a person will continue. A session already over answers 409. Needs flows:write on a scoped key.

AuthenticationTenant API token

Required permission: flows.edit

Path parameters

sessionintegerrequired
The session id.

Example: 5120

Request body

application/json

notify_customerbooleanoptional
Tell the customer a person will continue. Default false.
Complete request schema
{
    "type": "object",
    "properties": {
        "notify_customer": {
            "type": "boolean",
            "description": "Tell the customer a person will continue. Default false."
        }
    }
}

Responses

200Ended.
dataobjectrequired
The result.
Show child properties
endedbooleanoptional
True: it was live and is now cancelled.
messagestringoptional
One sentence.
sessionobjectoptional
The session after the end.
Show child properties
idintegeroptional
The session id.
flow_idintegeroptional
The flow it runs.
flow_namestringoptional
The flow's name at read time.
flow_versionintegeroptional
The published version number the session is pinned to.
nullable
true
conversation_idintegeroptional
The inbox conversation it lives in.
contact_idintegeroptional
The contact record, when the conversation has one.
nullable
true
contact_namestringoptional
The customer's name as the inbox knows it.
nullable
true
contact_identifierstringoptional
The customer's phone (or handle) as stored on the conversation.
triggerstringoptional
What started it: inbound, manual, api, schedule, automation…
statusstringoptional
running, waiting, completed, failed, expired, superseded_by_human or cancelled.
outcomestringoptional
The eight-word reading of the status for people: in_progress, completed, abandoned, handed_over, failed, expired, cancelled, ended_by_operator.
ended_reasonstringoptional
The runtime's own word for why it ended, when it has.
nullable
true
node_idstringoptional
The step it is on, or ended on.
nullable
true
awaitingstringoptional
What a waiting session waits for: text, choice, media, location, form, timer, payment…
nullable
true
turnsintegeroptional
How many customer turns it has taken.
resume_atstringoptional
When a timer wakes it, if one will.
format
date-time
nullable
true
expires_atstringoptional
When it lapses if the customer says nothing.
format
date-time
nullable
true
started_atstringoptional
When it started.
format
date-time
ended_atstringoptional
When it ended, or null while live.
format
date-time
nullable
true
{
    "data": {
        "ended": true,
        "message": "The session was ended.",
        "session": {
            "status": "cancelled",
            "outcome": "ended_by_operator",
            "id": 5120,
            "flow_id": 17,
            "flow_name": "Oda ya chakula",
            "flow_version": 3,
            "conversation_id": 771,
            "contact_id": 2201,
            "contact_name": "Asha Mrisho",
            "contact_identifier": "255712345678",
            "trigger": "inbound",
            "ended_reason": "completed",
            "node_id": "done",
            "awaiting": null,
            "turns": 6,
            "resume_at": null,
            "expires_at": null,
            "started_at": "2026-09-12T09:01:12+03:00",
            "ended_at": "2026-09-12T09:14:02+03:00"
        }
    }
}
default
{
    "data": {
        "ended": true,
        "message": "The session was ended.",
        "session": {
            "status": "cancelled",
            "outcome": "ended_by_operator",
            "id": 5120,
            "flow_id": 17,
            "flow_name": "Oda ya chakula",
            "flow_version": 3,
            "conversation_id": 771,
            "contact_id": 2201,
            "contact_name": "Asha Mrisho",
            "contact_identifier": "255712345678",
            "trigger": "inbound",
            "ended_reason": "completed",
            "node_id": "done",
            "awaiting": null,
            "turns": 6,
            "resume_at": null,
            "expires_at": null,
            "started_at": "2026-09-12T09:01:12+03:00",
            "ended_at": "2026-09-12T09:14:02+03:00"
        }
    }
}
404No such flow or session in this workspace. Another workspace's answers 404, never 403.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}
default
{
    "status": "error",
    "message": "Resource not found."
}
409The session had already ended.
dataobjectoptional
The same shape as 200 with ended:false.
additionalProperties
true
{
    "data": {
        "ended": false,
        "message": "That session had already ended.",
        "session": {
            "id": 5120,
            "flow_id": 17,
            "flow_name": "Oda ya chakula",
            "flow_version": 3,
            "conversation_id": 771,
            "contact_id": 2201,
            "contact_name": "Asha Mrisho",
            "contact_identifier": "255712345678",
            "trigger": "inbound",
            "status": "completed",
            "outcome": "completed",
            "ended_reason": "completed",
            "node_id": "done",
            "awaiting": null,
            "turns": 6,
            "resume_at": null,
            "expires_at": null,
            "started_at": "2026-09-12T09:01:12+03:00",
            "ended_at": "2026-09-12T09:14:02+03:00"
        }
    }
}
default
{
    "data": {
        "ended": false,
        "message": "That session had already ended.",
        "session": {
            "id": 5120,
            "flow_id": 17,
            "flow_name": "Oda ya chakula",
            "flow_version": 3,
            "conversation_id": 771,
            "contact_id": 2201,
            "contact_name": "Asha Mrisho",
            "contact_identifier": "255712345678",
            "trigger": "inbound",
            "status": "completed",
            "outcome": "completed",
            "ended_reason": "completed",
            "node_id": "done",
            "awaiting": null,
            "turns": 6,
            "resume_at": null,
            "expires_at": null,
            "started_at": "2026-09-12T09:01:12+03:00",
            "ended_at": "2026-09-12T09:14:02+03:00"
        }
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The key's issuer does not hold flows.edit, the key has no issuer on record, or the key was issued without the scope this call needs.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This API key was issued by a user without the \"flows.edit\" permission."
}
default
{
    "status": "error",
    "message": "This API key was issued by a user without the \"flows.edit\" permission."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / WhatsApp templates

List templates

GET/api/v3/whatsapp/templates

WhatsApp templates in this workspace, newest first, with their Meta review status. Archived rows are left out unless status=archived or status=all. updated_since makes an incremental mirror cheap: it returns rows whose local record changed after that time, which includes every status move.

AuthenticationTenant API token

Required permission: communications.templates.view

Query parameters

waba_idstringoptional
Only templates on this WhatsApp Business Account. `GET /api/v3/whatsapp/accounts` lists them (`waba_id`).

Example: 102290129340398

statusstringoptional
Local state. Omit for everything but archived; `all` includes archived.
enum
["draft","active","archived","all"]

Example: active

whatsapp_statusstringoptional
Meta's review status.
enum
["pending","in_review","approved","rejected","disabled","paused"]

Example: approved

categorystringoptional
Meta category.
enum
["marketing","utility","authentication"]

Example: utility

languagestringoptional
Language code, exact.

Example: sw

namestringoptional
Template name, exact — every language of it.

Example: order_shipped

qstringoptional
Free text over name, display name and body.
maxLength
120

Example: oda

updated_sincestringoptional
Only rows changed at or after this ISO-8601 time.
format
date-time

Example: 2026-09-12T00:00:00Z

limitintegeroptional
Rows per page, 1–100. Defaults to 20 (25 for catalogue endpoints). Values above 100 are clamped.
minimum
1
maximum
100
default
20

Example: 20

pageintegeroptional
Page number, 1-based. Read `data.pagination.has_more_pages` to know when to stop.
minimum
1
default
1

Example: 1

Responses

200A page of templates.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
A page of templates.
Show child properties
itemsarray<object>optional
The page of templates.
Show child properties
idintegerrequired
Local template id — what every other template endpoint takes in its path.
namestringrequired
The machine handle Meta knows it by: lowercase letters, digits and underscores, e.g. `order_shipped`. This is `template.name` on `POST /api/v3/whatsapp/send`.
pattern
^[a-z0-9_]{1,512}$
display_namestringoptional
What a person on the dashboard calls it.
languagestringrequired
Language code of the copy, e.g. `sw`, `en`, `en_US`. One template per language.
categorystringrequired
Meta's category. `utility` for transactional notices, `marketing` for promotions, `authentication` for one-time codes. Meta may reclassify a template during review.
enum
["marketing","utility","authentication"]
statusstringrequired
Its state on this platform, before Meta gets a say. Only an `active` one can be sent; `archived` rows are hidden from the default list.
enum
["draft","active","archived"]
whatsapp_statusstringrequired
Where Meta's review got to. `pending` = not yet submitted (or a queued submission); `in_review` = Meta has it; `approved` = usable; `rejected` = see `rejection_reason`; `paused` / `disabled` = Meta stopped it for quality.
enum
["pending","in_review","approved","rejected","disabled","paused"]
approvedbooleanoptional
True when `whatsapp_status` is `approved`.
sendablebooleanoptional
True when it can be sent right now: `status` active AND approved by Meta.
rejection_reasonstring | nulloptional
Meta's reason, when `whatsapp_status` is `rejected` — a review verdict such as `INVALID_FORMAT`, or the refusal it gave the submission itself (a name clash, a body that starts with a variable). Null otherwise.
whatsapp_business_account_idstring | nulloptional
The WhatsApp Business Account (WABA) id it lives on. `GET /api/v3/whatsapp/accounts` lists them.
whatsapp_template_idstring | nulloptional
Meta's own id for the template on that account. Null until a submission has been accepted.
quality_ratingstring | nulloptional
Meta's quality score on this account — `GREEN`, `YELLOW`, `RED` or `UNKNOWN` — once approved and in use. Null before that.
parameter_formatstringoptional
How variables are written: `POSITIONAL` (`{{1}}`, `{{2}}`) or `NAMED` (`{{order_id}}`).
enum
["POSITIONAL","NAMED"]
variablesarray<string>optional
The placeholders the header and body carry, in order — what a send must supply one value each for.
bodystringoptional
The body copy with its placeholders. The full component tree is on the single-template read.
last_synced_atstring | nulloptional
When Meta last told us anything about this template (submission, poll, webhook or refresh).
format
date-time
created_atstring | nulloptional
When the row was created here.
format
date-time
updated_atstring | nulloptional
When the row last changed here.
format
date-time
paginationobjectoptional
Page state.
Show child properties
current_pageintegerrequired
1-based current page index.
per_pageintegerrequired
Number of items per page.
last_pageintegerrequired
1-based index of the last page.
totalintegerrequired
Total number of items across all pages.
has_more_pagesbooleanrequired
True if more pages exist after the current page.
{
    "status": "success",
    "data": {
        "items": [
            {
                "id": 418,
                "name": "order_shipped",
                "display_name": "Order shipped",
                "language": "sw",
                "category": "utility",
                "status": "active",
                "whatsapp_status": "in_review",
                "approved": false,
                "sendable": false,
                "rejection_reason": null,
                "whatsapp_business_account_id": "102290129340398",
                "whatsapp_template_id": "1189456212345678",
                "quality_rating": null,
                "parameter_format": "POSITIONAL",
                "variables": [
                    "1",
                    "2"
                ],
                "body": "Habari {{1}}, oda yako {{2}} imetumwa leo.",
                "last_synced_at": "2026-09-13T09:14:02+00:00",
                "created_at": "2026-09-13T09:14:02+00:00",
                "updated_at": "2026-09-13T09:14:02+00:00"
            },
            {
                "id": 417,
                "name": "karibu",
                "display_name": "Order shipped",
                "language": "en",
                "category": "marketing",
                "status": "active",
                "whatsapp_status": "approved",
                "approved": true,
                "sendable": true,
                "rejection_reason": null,
                "whatsapp_business_account_id": "102290129340398",
                "whatsapp_template_id": "1189456200000417",
                "quality_rating": "GREEN",
                "parameter_format": "POSITIONAL",
                "variables": [
                    "1"
                ],
                "body": "Welcome to Amina, {{1}}!",
                "last_synced_at": "2026-09-13T09:14:02+00:00",
                "created_at": "2026-09-13T09:14:02+00:00",
                "updated_at": "2026-09-13T09:14:02+00:00"
            }
        ],
        "pagination": {
            "current_page": 1,
            "per_page": 25,
            "last_page": 1,
            "total": 2,
            "has_more_pages": false
        }
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}
422The payload failed validation. `errors` maps each rejected field to its messages.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Validation failed.",
    "errors": {
        "recipient": [
            "Provide recipient or recipients."
        ]
    }
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / WhatsApp templates

Create a template and submit it for review

POST/api/v3/whatsapp/templates

Creates the template on the chosen WhatsApp Business Account and — unless submit is false — submits it to Meta for review inside this request. The answer is always 201 with the template; read whatsapp_status for Meta's verdict: in_review (or approved outright) with a whatsapp_template_id, or rejected with Meta's own words in rejection_reason (a bad body, a name it will not take, a token without the permission). Only when Meta cannot be reached is the submission queued to retry; submission.queued says so and the row stays pending.

Describe the content either way: components in WhatsApp's own shape (everything Meta supports — media headers, carousels, limited-time offers, copy-code and flow buttons, named parameters), or the flat fields body, header_text, footer, buttons, variable_samples for the common case. Meta needs a sample value for every placeholder; with the flat fields, supply them in variable_samples.

A name is one template per language: order_shipped in sw and in en are two rows, each reviewed on its own. The same name and language again answers 409.

AuthenticationTenant API token

Required permission: communications.templates.manage

Request body

application/json · required

The template.

namestringoptional
The machine handle: lowercase letters, digits and underscores, at most 512. Meta refuses anything else, so this API does too rather than silently changing it. Cannot change once created.
pattern
^[a-z0-9_]{1,512}$
maxLength
512
languagestringoptional
Language code, e.g. `sw`, `en`, `en_US`. Cannot change once created — another language is another template.
maxLength
16
categorystringoptional
Meta category.
enum
["marketing","utility","authentication"]
display_namestringoptional
A human name for the dashboard. Defaults to the name, title-cased.
maxLength
255
waba_idstringoptional
Which WhatsApp Business Account to create it on. Required only when the workspace has more than one; `GET /api/v3/whatsapp/accounts` lists them (`waba_id`). Cannot change once created — use deployments.
maxLength
64
statusstringoptional
Local state. Defaults to `active`. `archived` hides it; `draft` keeps it out of the send picker.
enum
["draft","active","archived"]
parameter_formatstringoptional
How placeholders are written in the copy. Defaults to `POSITIONAL`.
enum
["POSITIONAL","NAMED"]
submitbooleanoptional
Whether to submit to Meta in this request. Defaults to true. On PATCH, only a content change submits.
default
true
componentsarray<object>optional
The content in WhatsApp's own `components[]` shape — one entry per HEADER, BODY, FOOTER, BUTTONS, CAROUSEL or LIMITED_TIME_OFFER, with Meta's `example` values. Covers everything Meta supports. Wins over the flat fields when both are sent; on PATCH, replaces the whole content.
minItems
1
maxItems
12
items.additionalProperties
true
Show child properties
typestringrequired
Component type.
enum
["HEADER","BODY","FOOTER","BUTTONS","CAROUSEL","LIMITED_TIME_OFFER"]
formatstringoptional
HEADER only: `TEXT`, `IMAGE`, `VIDEO`, `DOCUMENT` or `LOCATION`.
textstringoptional
HEADER (TEXT), BODY and FOOTER: the copy, with `{{1}}`-style or named placeholders.
exampleobjectoptional
Sample values for every placeholder, in the shape Meta wants: `{"body_text": [["Asha", "ORD-1042"]]}`, `{"header_text": ["Oda"]}`, `{"header_handle": ["<upload handle>"]}`. Required by Meta whenever there is a placeholder or a media header.
additionalProperties
true
buttonsarray<object>optional
BUTTONS only: up to ten buttons — `QUICK_REPLY`, `URL`, `PHONE_NUMBER`, `COPY_CODE`, `FLOW`, `OTP`, `CATALOG`, `MPM`, `VOICE_CALL` — each in Meta's shape.
items.additionalProperties
true
bodystringoptional
Flat description: the body copy, up to 1,024 characters, with `{{1}}`-style placeholders. Required unless `components` is given. On PATCH, sending `body` re-describes the whole content with the flat fields.
maxLength
1024
header_typestringoptional
Flat description: header kind — `none` or `text` (with `header_text`). A media header (image, video, document) needs the sample handle Meta issues on upload, which only `components` can carry (`{"type":"HEADER","format":"IMAGE","example":{"header_handle":["…"]}}`).
enum
["none","text"]
header_textstringoptional
Flat description: a one-line text header, up to 60 characters, one placeholder at most. Implies `header_type: text`.
maxLength
60
footerstringoptional
Flat description: footer copy, up to 60 characters, no placeholders.
maxLength
60
buttonsarray<object>optional
Flat description: up to ten buttons of type `quick_reply`, `url` (with `url`; a `{{1}}` at its end makes it dynamic and then `example` is required) or `phone_number` (with `phone_number`). For copy-code, flow or catalogue buttons use `components`.
maxItems
10
Show child properties
typestringoptional
Button type.
enum
["quick_reply","url","phone_number"]
textstringoptional
Button label, up to 25 characters.
maxLength
25
urlstringoptional
For `url`: the link, optionally ending in `{{1}}`.
phone_numberstringoptional
For `phone_number`: the number to dial, in E.164.
examplestringoptional
For a dynamic `url`: a sample value for its `{{1}}`.
variable_samplesobjectoptional
Flat description: a sample value per placeholder, keyed by the placeholder (`"1"`, `"2"` or a name). Meta requires a sample for every placeholder; missing ones get a generic sample, which Meta may reject for marketing copy.
additionalProperties
{"type":"string","description":"The sample value."}
Complete request schema
{
    "type": "object",
    "description": "What `POST /api/v3/whatsapp/templates` takes, and what `PATCH` accepts a subset of.",
    "properties": {
        "name": {
            "type": "string",
            "description": "The machine handle: lowercase letters, digits and underscores, at most 512. Meta refuses anything else, so this API does too rather than silently changing it. Cannot change once created.",
            "pattern": "^[a-z0-9_]{1,512}$",
            "maxLength": 512
        },
        "language": {
            "type": "string",
            "description": "Language code, e.g. `sw`, `en`, `en_US`. Cannot change once created \u2014 another language is another template.",
            "maxLength": 16
        },
        "category": {
            "type": "string",
            "description": "Meta category.",
            "enum": [
                "marketing",
                "utility",
                "authentication"
            ]
        },
        "display_name": {
            "type": "string",
            "description": "A human name for the dashboard. Defaults to the name, title-cased.",
            "maxLength": 255
        },
        "waba_id": {
            "type": "string",
            "description": "Which WhatsApp Business Account to create it on. Required only when the workspace has more than one; `GET /api/v3/whatsapp/accounts` lists them (`waba_id`). Cannot change once created \u2014 use deployments.",
            "maxLength": 64
        },
        "status": {
            "type": "string",
            "description": "Local state. Defaults to `active`. `archived` hides it; `draft` keeps it out of the send picker.",
            "enum": [
                "draft",
                "active",
                "archived"
            ]
        },
        "parameter_format": {
            "type": "string",
            "description": "How placeholders are written in the copy. Defaults to `POSITIONAL`.",
            "enum": [
                "POSITIONAL",
                "NAMED"
            ]
        },
        "submit": {
            "type": "boolean",
            "description": "Whether to submit to Meta in this request. Defaults to true. On PATCH, only a content change submits.",
            "default": true
        },
        "components": {
            "type": "array",
            "description": "The content in WhatsApp's own `components[]` shape \u2014 one entry per HEADER, BODY, FOOTER, BUTTONS, CAROUSEL or LIMITED_TIME_OFFER, with Meta's `example` values. Covers everything Meta supports. Wins over the flat fields when both are sent; on PATCH, replaces the whole content.",
            "items": {
                "$ref": "#/components/schemas/MetaTemplateComponent"
            },
            "minItems": 1,
            "maxItems": 12
        },
        "body": {
            "type": "string",
            "description": "Flat description: the body copy, up to 1,024 characters, with `{{1}}`-style placeholders. Required unless `components` is given. On PATCH, sending `body` re-describes the whole content with the flat fields.",
            "maxLength": 1024
        },
        "header_type": {
            "type": "string",
            "description": "Flat description: header kind \u2014 `none` or `text` (with `header_text`). A media header (image, video, document) needs the sample handle Meta issues on upload, which only `components` can carry (`{\"type\":\"HEADER\",\"format\":\"IMAGE\",\"example\":{\"header_handle\":[\"\u2026\"]}}`).",
            "enum": [
                "none",
                "text"
            ]
        },
        "header_text": {
            "type": "string",
            "description": "Flat description: a one-line text header, up to 60 characters, one placeholder at most. Implies `header_type: text`.",
            "maxLength": 60
        },
        "footer": {
            "type": "string",
            "description": "Flat description: footer copy, up to 60 characters, no placeholders.",
            "maxLength": 60
        },
        "buttons": {
            "type": "array",
            "description": "Flat description: up to ten buttons of type `quick_reply`, `url` (with `url`; a `{{1}}` at its end makes it dynamic and then `example` is required) or `phone_number` (with `phone_number`). For copy-code, flow or catalogue buttons use `components`.",
            "items": {
                "type": "object",
                "description": "One button.",
                "properties": {
                    "type": {
                        "type": "string",
                        "description": "Button type.",
                        "enum": [
                            "quick_reply",
                            "url",
                            "phone_number"
                        ]
                    },
                    "text": {
                        "type": "string",
                        "description": "Button label, up to 25 characters.",
                        "maxLength": 25
                    },
                    "url": {
                        "type": "string",
                        "description": "For `url`: the link, optionally ending in `{{1}}`."
                    },
                    "phone_number": {
                        "type": "string",
                        "description": "For `phone_number`: the number to dial, in E.164."
                    },
                    "example": {
                        "type": "string",
                        "description": "For a dynamic `url`: a sample value for its `{{1}}`."
                    }
                }
            },
            "maxItems": 10
        },
        "variable_samples": {
            "type": "object",
            "description": "Flat description: a sample value per placeholder, keyed by the placeholder (`\"1\"`, `\"2\"` or a name). Meta requires a sample for every placeholder; missing ones get a generic sample, which Meta may reject for marketing copy.",
            "additionalProperties": {
                "type": "string",
                "description": "The sample value."
            }
        }
    }
}
Flat fields — the common case
{
    "name": "order_shipped",
    "language": "sw",
    "category": "utility",
    "display_name": "Order shipped",
    "body": "Habari {{1}}, oda yako {{2}} imetumwa leo.",
    "header_text": "Oda {{1}}",
    "footer": "Duka la Amina",
    "buttons": [
        {
            "type": "url",
            "text": "Fuatilia",
            "url": "https://amina.co.tz/track/{{1}}",
            "example": "ORD-1042"
        }
    ],
    "variable_samples": {
        "1": "Asha",
        "2": "ORD-1042"
    }
}
WhatsApp's own components — every feature
{
    "name": "ofa_ijumaa",
    "language": "sw",
    "category": "marketing",
    "waba_id": "102290129340398",
    "components": [
        {
            "type": "HEADER",
            "format": "IMAGE",
            "example": {
                "header_handle": [
                    "4::aW1hZ2UvcG5n:ARZ\u2026"
                ]
            }
        },
        {
            "type": "BODY",
            "text": "Ofa ya {{1}}: punguzo la {{2}} hadi {{3}}.",
            "example": {
                "body_text": [
                    [
                        "Ijumaa",
                        "20%",
                        "30 Sept"
                    ]
                ]
            }
        },
        {
            "type": "FOOTER",
            "text": "Jibu STOP kuacha"
        },
        {
            "type": "BUTTONS",
            "buttons": [
                {
                    "type": "QUICK_REPLY",
                    "text": "Nataka"
                },
                {
                    "type": "COPY_CODE",
                    "example": "OFA20"
                }
            ]
        }
    ]
}
Save without submitting
{
    "name": "karibu",
    "language": "en",
    "category": "marketing",
    "body": "Welcome to Amina, {{1}}!",
    "variable_samples": {
        "1": "Asha"
    },
    "submit": false
}

Responses

201The template as saved, with Meta's answer in `whatsapp_status` and `submission`.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
The template as saved, with Meta's answer in `whatsapp_status` and `submission`.
Show child properties
idintegerrequired
Local template id — what every other template endpoint takes in its path.
namestringrequired
The machine handle Meta knows it by: lowercase letters, digits and underscores, e.g. `order_shipped`. This is `template.name` on `POST /api/v3/whatsapp/send`.
pattern
^[a-z0-9_]{1,512}$
display_namestringoptional
What a person on the dashboard calls it.
languagestringrequired
Language code of the copy, e.g. `sw`, `en`, `en_US`. One template per language.
categorystringrequired
Meta's category. `utility` for transactional notices, `marketing` for promotions, `authentication` for one-time codes. Meta may reclassify a template during review.
enum
["marketing","utility","authentication"]
statusstringrequired
Its state on this platform, before Meta gets a say. Only an `active` one can be sent; `archived` rows are hidden from the default list.
enum
["draft","active","archived"]
whatsapp_statusstringrequired
Where Meta's review got to. `pending` = not yet submitted (or a queued submission); `in_review` = Meta has it; `approved` = usable; `rejected` = see `rejection_reason`; `paused` / `disabled` = Meta stopped it for quality.
enum
["pending","in_review","approved","rejected","disabled","paused"]
approvedbooleanoptional
True when `whatsapp_status` is `approved`.
sendablebooleanoptional
True when it can be sent right now: `status` active AND approved by Meta.
rejection_reasonstring | nulloptional
Meta's reason, when `whatsapp_status` is `rejected` — a review verdict such as `INVALID_FORMAT`, or the refusal it gave the submission itself (a name clash, a body that starts with a variable). Null otherwise.
whatsapp_business_account_idstring | nulloptional
The WhatsApp Business Account (WABA) id it lives on. `GET /api/v3/whatsapp/accounts` lists them.
whatsapp_template_idstring | nulloptional
Meta's own id for the template on that account. Null until a submission has been accepted.
quality_ratingstring | nulloptional
Meta's quality score on this account — `GREEN`, `YELLOW`, `RED` or `UNKNOWN` — once approved and in use. Null before that.
parameter_formatstringoptional
How variables are written: `POSITIONAL` (`{{1}}`, `{{2}}`) or `NAMED` (`{{order_id}}`).
enum
["POSITIONAL","NAMED"]
variablesarray<string>optional
The placeholders the header and body carry, in order — what a send must supply one value each for.
bodystringoptional
The body copy with its placeholders. The full component tree is on the single-template read.
last_synced_atstring | nulloptional
When Meta last told us anything about this template (submission, poll, webhook or refresh).
format
date-time
created_atstring | nulloptional
When the row was created here.
format
date-time
updated_atstring | nulloptional
When the row last changed here.
format
date-time
header_typestringoptional
The header kind: `none`, `text`, `image`, `video` or `document`.
enum
["none","text","image","video","document"]
header_textstring | nulloptional
The header copy when `header_type` is `text`.
footerstring | nulloptional
The footer copy, up to 60 characters.
buttonsarray<object>optional
The flat-field buttons, when the template was described that way. `components` is the complete picture either way.
items.additionalProperties
true
componentsarray<object>required
The template in Meta's `components[]` shape — what is (or would be) submitted for review. Post it back unchanged to create a twin, or edit and PATCH it.
items.additionalProperties
true
Show child properties
typestringrequired
Component type.
enum
["HEADER","BODY","FOOTER","BUTTONS","CAROUSEL","LIMITED_TIME_OFFER"]
formatstringoptional
HEADER only: `TEXT`, `IMAGE`, `VIDEO`, `DOCUMENT` or `LOCATION`.
textstringoptional
HEADER (TEXT), BODY and FOOTER: the copy, with `{{1}}`-style or named placeholders.
exampleobjectoptional
Sample values for every placeholder, in the shape Meta wants: `{"body_text": [["Asha", "ORD-1042"]]}`, `{"header_text": ["Oda"]}`, `{"header_handle": ["<upload handle>"]}`. Required by Meta whenever there is a placeholder or a media header.
additionalProperties
true
buttonsarray<object>optional
BUTTONS only: up to ten buttons — `QUICK_REPLY`, `URL`, `PHONE_NUMBER`, `COPY_CODE`, `FLOW`, `OTP`, `CATALOG`, `MPM`, `VOICE_CALL` — each in Meta's shape.
items.additionalProperties
true
deploymentsarray<object>required
Its review status on every OTHER WhatsApp Business Account it has been deployed to (`POST …/deployments`). Empty until deployed.
Show child properties
idintegeroptional
Deployment id.
whatsapp_business_account_idstringoptional
The WhatsApp Business Account this deployment is on.
whatsapp_template_idstring | nulloptional
Meta's id for the template on THAT account (each account gets its own).
whatsapp_statusstringoptional
Meta's review status on that account.
enum
["pending","in_review","approved","rejected","disabled","paused"]
rejection_reasonstring | nulloptional
Meta's reason when rejected on that account.
quality_ratingstring | nulloptional
Quality score on that account: `GREEN`, `YELLOW`, `RED`, `UNKNOWN` or null.
categorystring | nulloptional
The category Meta assigned on that account, when it differs from the authored one.
last_synced_atstring | nulloptional
When Meta last told us about it on that account.
format
date-time
submissionobjectoptional
Only on responses that submitted to Meta (create, PATCH with content, submit): what happened.
Show child properties
submittedbooleanoptional
True when Meta accepted the submission in this request (read `whatsapp_status` and `whatsapp_template_id`).
queuedbooleanoptional
True when Meta could not be reached and the submission was queued to retry; the template stays `pending` until it lands.
messagestring | nulloptional
Why it was not submitted, when it was not — Meta's own words for a refusal, or that it is queued.
refreshobjectoptional
Only on `POST …/refresh`: `found_on_whatsapp` says whether Meta has the template, `message` explains when it does not.
Show child properties
found_on_whatsappbooleanoptional
False when Meta has no template of this name and language on the account; the row is left as it was.
messagestring | nulloptional
Explanation when not found.
{
    "status": "success",
    "data": {
        "id": 418,
        "name": "order_shipped",
        "display_name": "Order shipped",
        "language": "sw",
        "category": "utility",
        "status": "active",
        "whatsapp_status": "in_review",
        "approved": false,
        "sendable": false,
        "rejection_reason": null,
        "whatsapp_business_account_id": "102290129340398",
        "whatsapp_template_id": "1189456212345678",
        "quality_rating": null,
        "parameter_format": "POSITIONAL",
        "variables": [
            "1",
            "2"
        ],
        "body": "Habari {{1}}, oda yako {{2}} imetumwa leo.",
        "last_synced_at": "2026-09-13T09:14:02+00:00",
        "created_at": "2026-09-13T09:14:02+00:00",
        "updated_at": "2026-09-13T09:14:02+00:00",
        "header_type": "text",
        "header_text": "Oda {{1}}",
        "footer": "Duka la Amina",
        "buttons": [
            {
                "type": "url",
                "text": "Fuatilia",
                "url": "https://amina.co.tz/track/{{1}}",
                "example": "ORD-1042"
            }
        ],
        "components": [
            {
                "type": "HEADER",
                "format": "TEXT",
                "text": "Oda {{1}}",
                "example": {
                    "header_text": [
                        "ORD-1042"
                    ]
                }
            },
            {
                "type": "BODY",
                "text": "Habari {{1}}, oda yako {{2}} imetumwa leo.",
                "example": {
                    "body_text": [
                        [
                            "Asha",
                            "ORD-1042"
                        ]
                    ]
                }
            },
            {
                "type": "FOOTER",
                "text": "Duka la Amina"
            },
            {
                "type": "BUTTONS",
                "buttons": [
                    {
                        "type": "URL",
                        "text": "Fuatilia",
                        "url": "https://amina.co.tz/track/{{1}}",
                        "example": [
                            "ORD-1042"
                        ]
                    }
                ]
            }
        ],
        "deployments": [],
        "submission": {
            "submitted": true,
            "queued": false,
            "message": null
        }
    }
}
409Refused because of the workspace's current state; the message says what to do.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "No WhatsApp business account is connected to this workspace, so there is nothing to submit a template to. Connect one under Settings \u2192 WhatsApp first."
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}
422The payload failed validation. `errors` maps each rejected field to its messages.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Validation failed.",
    "errors": {
        "recipient": [
            "Provide recipient or recipients."
        ]
    }
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / WhatsApp templates

Pull templates from WhatsApp

POST/api/v3/whatsapp/templates/sync

Makes this workspace a mirror of what Meta has: every APPROVED template on the account(s) that is not here yet is imported as a local row (name, language, category and full components), ones already here are refreshed from Meta's copy, and the review status of every template already submitted is re-read. Templates still in review on Meta's side that were not submitted through this platform appear once approved. Runs inside the request against Meta's paginated list, so on an account with hundreds of templates allow a few seconds. Omit waba_id to do every account.

AuthenticationTenant API token

Required permission: communications.templates.manage

Request body

application/json

Which account to pull from; omit for all.

waba_idstring | nulloptional
One WhatsApp Business Account, or omit for every connected account. `GET /api/v3/whatsapp/accounts` lists them (`waba_id`).
Complete request schema
{
    "type": "object",
    "properties": {
        "waba_id": {
            "type": [
                "string",
                "null"
            ],
            "description": "One WhatsApp Business Account, or omit for every connected account. `GET /api/v3/whatsapp/accounts` lists them (`waba_id`)."
        }
    }
}

Responses

200What the pull did.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
What the pull did.
Show child properties
importedintegeroptional
Approved templates that were new here and are now rows.
updatedintegeroptional
Approved templates already here whose content was refreshed from Meta.
statuses_updatedintegeroptional
Rows whose review status changed as a result.
errorsarray<string>optional
Per-template problems, when any — the rest of the pull still happened.
{
    "status": "success",
    "data": {
        "imported": 3,
        "updated": 9,
        "statuses_updated": 1,
        "errors": []
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}
422The payload failed validation. `errors` maps each rejected field to its messages.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Validation failed.",
    "errors": {
        "recipient": [
            "Provide recipient or recipients."
        ]
    }
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / WhatsApp templates

Read a template

GET/api/v3/whatsapp/templates/{template}

One template in full: the summary fields, its content in both the flat fields and Meta's components shape, and its review status on every account it has been deployed to. components is exactly what was (or would be) submitted to Meta — post it back to create a twin, or edit and PATCH it.

AuthenticationTenant API token

Required permission: communications.templates.view

Path parameters

templateintegerrequired
Local template id, from the list or the create response.

Example: 418

Responses

200The template.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
The template.
Show child properties
idintegerrequired
Local template id — what every other template endpoint takes in its path.
namestringrequired
The machine handle Meta knows it by: lowercase letters, digits and underscores, e.g. `order_shipped`. This is `template.name` on `POST /api/v3/whatsapp/send`.
pattern
^[a-z0-9_]{1,512}$
display_namestringoptional
What a person on the dashboard calls it.
languagestringrequired
Language code of the copy, e.g. `sw`, `en`, `en_US`. One template per language.
categorystringrequired
Meta's category. `utility` for transactional notices, `marketing` for promotions, `authentication` for one-time codes. Meta may reclassify a template during review.
enum
["marketing","utility","authentication"]
statusstringrequired
Its state on this platform, before Meta gets a say. Only an `active` one can be sent; `archived` rows are hidden from the default list.
enum
["draft","active","archived"]
whatsapp_statusstringrequired
Where Meta's review got to. `pending` = not yet submitted (or a queued submission); `in_review` = Meta has it; `approved` = usable; `rejected` = see `rejection_reason`; `paused` / `disabled` = Meta stopped it for quality.
enum
["pending","in_review","approved","rejected","disabled","paused"]
approvedbooleanoptional
True when `whatsapp_status` is `approved`.
sendablebooleanoptional
True when it can be sent right now: `status` active AND approved by Meta.
rejection_reasonstring | nulloptional
Meta's reason, when `whatsapp_status` is `rejected` — a review verdict such as `INVALID_FORMAT`, or the refusal it gave the submission itself (a name clash, a body that starts with a variable). Null otherwise.
whatsapp_business_account_idstring | nulloptional
The WhatsApp Business Account (WABA) id it lives on. `GET /api/v3/whatsapp/accounts` lists them.
whatsapp_template_idstring | nulloptional
Meta's own id for the template on that account. Null until a submission has been accepted.
quality_ratingstring | nulloptional
Meta's quality score on this account — `GREEN`, `YELLOW`, `RED` or `UNKNOWN` — once approved and in use. Null before that.
parameter_formatstringoptional
How variables are written: `POSITIONAL` (`{{1}}`, `{{2}}`) or `NAMED` (`{{order_id}}`).
enum
["POSITIONAL","NAMED"]
variablesarray<string>optional
The placeholders the header and body carry, in order — what a send must supply one value each for.
bodystringoptional
The body copy with its placeholders. The full component tree is on the single-template read.
last_synced_atstring | nulloptional
When Meta last told us anything about this template (submission, poll, webhook or refresh).
format
date-time
created_atstring | nulloptional
When the row was created here.
format
date-time
updated_atstring | nulloptional
When the row last changed here.
format
date-time
header_typestringoptional
The header kind: `none`, `text`, `image`, `video` or `document`.
enum
["none","text","image","video","document"]
header_textstring | nulloptional
The header copy when `header_type` is `text`.
footerstring | nulloptional
The footer copy, up to 60 characters.
buttonsarray<object>optional
The flat-field buttons, when the template was described that way. `components` is the complete picture either way.
items.additionalProperties
true
componentsarray<object>required
The template in Meta's `components[]` shape — what is (or would be) submitted for review. Post it back unchanged to create a twin, or edit and PATCH it.
items.additionalProperties
true
Show child properties
typestringrequired
Component type.
enum
["HEADER","BODY","FOOTER","BUTTONS","CAROUSEL","LIMITED_TIME_OFFER"]
formatstringoptional
HEADER only: `TEXT`, `IMAGE`, `VIDEO`, `DOCUMENT` or `LOCATION`.
textstringoptional
HEADER (TEXT), BODY and FOOTER: the copy, with `{{1}}`-style or named placeholders.
exampleobjectoptional
Sample values for every placeholder, in the shape Meta wants: `{"body_text": [["Asha", "ORD-1042"]]}`, `{"header_text": ["Oda"]}`, `{"header_handle": ["<upload handle>"]}`. Required by Meta whenever there is a placeholder or a media header.
additionalProperties
true
buttonsarray<object>optional
BUTTONS only: up to ten buttons — `QUICK_REPLY`, `URL`, `PHONE_NUMBER`, `COPY_CODE`, `FLOW`, `OTP`, `CATALOG`, `MPM`, `VOICE_CALL` — each in Meta's shape.
items.additionalProperties
true
deploymentsarray<object>required
Its review status on every OTHER WhatsApp Business Account it has been deployed to (`POST …/deployments`). Empty until deployed.
Show child properties
idintegeroptional
Deployment id.
whatsapp_business_account_idstringoptional
The WhatsApp Business Account this deployment is on.
whatsapp_template_idstring | nulloptional
Meta's id for the template on THAT account (each account gets its own).
whatsapp_statusstringoptional
Meta's review status on that account.
enum
["pending","in_review","approved","rejected","disabled","paused"]
rejection_reasonstring | nulloptional
Meta's reason when rejected on that account.
quality_ratingstring | nulloptional
Quality score on that account: `GREEN`, `YELLOW`, `RED`, `UNKNOWN` or null.
categorystring | nulloptional
The category Meta assigned on that account, when it differs from the authored one.
last_synced_atstring | nulloptional
When Meta last told us about it on that account.
format
date-time
submissionobjectoptional
Only on responses that submitted to Meta (create, PATCH with content, submit): what happened.
Show child properties
submittedbooleanoptional
True when Meta accepted the submission in this request (read `whatsapp_status` and `whatsapp_template_id`).
queuedbooleanoptional
True when Meta could not be reached and the submission was queued to retry; the template stays `pending` until it lands.
messagestring | nulloptional
Why it was not submitted, when it was not — Meta's own words for a refusal, or that it is queued.
refreshobjectoptional
Only on `POST …/refresh`: `found_on_whatsapp` says whether Meta has the template, `message` explains when it does not.
Show child properties
found_on_whatsappbooleanoptional
False when Meta has no template of this name and language on the account; the row is left as it was.
messagestring | nulloptional
Explanation when not found.
{
    "status": "success",
    "data": {
        "id": 418,
        "name": "order_shipped",
        "display_name": "Order shipped",
        "language": "sw",
        "category": "utility",
        "status": "active",
        "whatsapp_status": "in_review",
        "approved": false,
        "sendable": false,
        "rejection_reason": null,
        "whatsapp_business_account_id": "102290129340398",
        "whatsapp_template_id": "1189456212345678",
        "quality_rating": null,
        "parameter_format": "POSITIONAL",
        "variables": [
            "1",
            "2"
        ],
        "body": "Habari {{1}}, oda yako {{2}} imetumwa leo.",
        "last_synced_at": "2026-09-13T09:14:02+00:00",
        "created_at": "2026-09-13T09:14:02+00:00",
        "updated_at": "2026-09-13T09:14:02+00:00",
        "header_type": "text",
        "header_text": "Oda {{1}}",
        "footer": "Duka la Amina",
        "buttons": [
            {
                "type": "url",
                "text": "Fuatilia",
                "url": "https://amina.co.tz/track/{{1}}",
                "example": "ORD-1042"
            }
        ],
        "components": [
            {
                "type": "HEADER",
                "format": "TEXT",
                "text": "Oda {{1}}",
                "example": {
                    "header_text": [
                        "ORD-1042"
                    ]
                }
            },
            {
                "type": "BODY",
                "text": "Habari {{1}}, oda yako {{2}} imetumwa leo.",
                "example": {
                    "body_text": [
                        [
                            "Asha",
                            "ORD-1042"
                        ]
                    ]
                }
            },
            {
                "type": "FOOTER",
                "text": "Duka la Amina"
            },
            {
                "type": "BUTTONS",
                "buttons": [
                    {
                        "type": "URL",
                        "text": "Fuatilia",
                        "url": "https://amina.co.tz/track/{{1}}",
                        "example": [
                            "ORD-1042"
                        ]
                    }
                ]
            }
        ],
        "deployments": []
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}
404No such record for this tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / WhatsApp templates

Change a template

PATCH/api/v3/whatsapp/templates/{template}

Any subset of the create fields. Changing content — body, header_text, footer, buttons, components or category — re-submits to Meta as an EDIT of the template it already holds (Meta then puts it back in review); changing only display_name or status does not touch Meta. submit: false saves a content change without re-submitting. A template in review cannot be edited on Meta's side, so the change is saved here and submitted the next time you call …/submit once the review is over.

name, language and waba_id cannot change: Meta knows the template by those. A template described by components (or imported from WhatsApp) may hold parts the flat fields cannot express, so to change part of it send the edited components; a flat body re-describes the whole content.

AuthenticationTenant API token

Required permission: communications.templates.manage

Path parameters

templateintegerrequired
Local template id, from the list or the create response.

Example: 418

Request body

application/json · required

The fields to change.

namestringoptional
The machine handle: lowercase letters, digits and underscores, at most 512. Meta refuses anything else, so this API does too rather than silently changing it. Cannot change once created.
pattern
^[a-z0-9_]{1,512}$
maxLength
512
languagestringoptional
Language code, e.g. `sw`, `en`, `en_US`. Cannot change once created — another language is another template.
maxLength
16
categorystringoptional
Meta category.
enum
["marketing","utility","authentication"]
display_namestringoptional
A human name for the dashboard. Defaults to the name, title-cased.
maxLength
255
waba_idstringoptional
Which WhatsApp Business Account to create it on. Required only when the workspace has more than one; `GET /api/v3/whatsapp/accounts` lists them (`waba_id`). Cannot change once created — use deployments.
maxLength
64
statusstringoptional
Local state. Defaults to `active`. `archived` hides it; `draft` keeps it out of the send picker.
enum
["draft","active","archived"]
parameter_formatstringoptional
How placeholders are written in the copy. Defaults to `POSITIONAL`.
enum
["POSITIONAL","NAMED"]
submitbooleanoptional
Whether to submit to Meta in this request. Defaults to true. On PATCH, only a content change submits.
default
true
componentsarray<object>optional
The content in WhatsApp's own `components[]` shape — one entry per HEADER, BODY, FOOTER, BUTTONS, CAROUSEL or LIMITED_TIME_OFFER, with Meta's `example` values. Covers everything Meta supports. Wins over the flat fields when both are sent; on PATCH, replaces the whole content.
minItems
1
maxItems
12
items.additionalProperties
true
Show child properties
typestringrequired
Component type.
enum
["HEADER","BODY","FOOTER","BUTTONS","CAROUSEL","LIMITED_TIME_OFFER"]
formatstringoptional
HEADER only: `TEXT`, `IMAGE`, `VIDEO`, `DOCUMENT` or `LOCATION`.
textstringoptional
HEADER (TEXT), BODY and FOOTER: the copy, with `{{1}}`-style or named placeholders.
exampleobjectoptional
Sample values for every placeholder, in the shape Meta wants: `{"body_text": [["Asha", "ORD-1042"]]}`, `{"header_text": ["Oda"]}`, `{"header_handle": ["<upload handle>"]}`. Required by Meta whenever there is a placeholder or a media header.
additionalProperties
true
buttonsarray<object>optional
BUTTONS only: up to ten buttons — `QUICK_REPLY`, `URL`, `PHONE_NUMBER`, `COPY_CODE`, `FLOW`, `OTP`, `CATALOG`, `MPM`, `VOICE_CALL` — each in Meta's shape.
items.additionalProperties
true
bodystringoptional
Flat description: the body copy, up to 1,024 characters, with `{{1}}`-style placeholders. Required unless `components` is given. On PATCH, sending `body` re-describes the whole content with the flat fields.
maxLength
1024
header_typestringoptional
Flat description: header kind — `none` or `text` (with `header_text`). A media header (image, video, document) needs the sample handle Meta issues on upload, which only `components` can carry (`{"type":"HEADER","format":"IMAGE","example":{"header_handle":["…"]}}`).
enum
["none","text"]
header_textstringoptional
Flat description: a one-line text header, up to 60 characters, one placeholder at most. Implies `header_type: text`.
maxLength
60
footerstringoptional
Flat description: footer copy, up to 60 characters, no placeholders.
maxLength
60
buttonsarray<object>optional
Flat description: up to ten buttons of type `quick_reply`, `url` (with `url`; a `{{1}}` at its end makes it dynamic and then `example` is required) or `phone_number` (with `phone_number`). For copy-code, flow or catalogue buttons use `components`.
maxItems
10
Show child properties
typestringoptional
Button type.
enum
["quick_reply","url","phone_number"]
textstringoptional
Button label, up to 25 characters.
maxLength
25
urlstringoptional
For `url`: the link, optionally ending in `{{1}}`.
phone_numberstringoptional
For `phone_number`: the number to dial, in E.164.
examplestringoptional
For a dynamic `url`: a sample value for its `{{1}}`.
variable_samplesobjectoptional
Flat description: a sample value per placeholder, keyed by the placeholder (`"1"`, `"2"` or a name). Meta requires a sample for every placeholder; missing ones get a generic sample, which Meta may reject for marketing copy.
additionalProperties
{"type":"string","description":"The sample value."}
Complete request schema
{
    "type": "object",
    "description": "What `POST /api/v3/whatsapp/templates` takes, and what `PATCH` accepts a subset of.",
    "properties": {
        "name": {
            "type": "string",
            "description": "The machine handle: lowercase letters, digits and underscores, at most 512. Meta refuses anything else, so this API does too rather than silently changing it. Cannot change once created.",
            "pattern": "^[a-z0-9_]{1,512}$",
            "maxLength": 512
        },
        "language": {
            "type": "string",
            "description": "Language code, e.g. `sw`, `en`, `en_US`. Cannot change once created \u2014 another language is another template.",
            "maxLength": 16
        },
        "category": {
            "type": "string",
            "description": "Meta category.",
            "enum": [
                "marketing",
                "utility",
                "authentication"
            ]
        },
        "display_name": {
            "type": "string",
            "description": "A human name for the dashboard. Defaults to the name, title-cased.",
            "maxLength": 255
        },
        "waba_id": {
            "type": "string",
            "description": "Which WhatsApp Business Account to create it on. Required only when the workspace has more than one; `GET /api/v3/whatsapp/accounts` lists them (`waba_id`). Cannot change once created \u2014 use deployments.",
            "maxLength": 64
        },
        "status": {
            "type": "string",
            "description": "Local state. Defaults to `active`. `archived` hides it; `draft` keeps it out of the send picker.",
            "enum": [
                "draft",
                "active",
                "archived"
            ]
        },
        "parameter_format": {
            "type": "string",
            "description": "How placeholders are written in the copy. Defaults to `POSITIONAL`.",
            "enum": [
                "POSITIONAL",
                "NAMED"
            ]
        },
        "submit": {
            "type": "boolean",
            "description": "Whether to submit to Meta in this request. Defaults to true. On PATCH, only a content change submits.",
            "default": true
        },
        "components": {
            "type": "array",
            "description": "The content in WhatsApp's own `components[]` shape \u2014 one entry per HEADER, BODY, FOOTER, BUTTONS, CAROUSEL or LIMITED_TIME_OFFER, with Meta's `example` values. Covers everything Meta supports. Wins over the flat fields when both are sent; on PATCH, replaces the whole content.",
            "items": {
                "$ref": "#/components/schemas/MetaTemplateComponent"
            },
            "minItems": 1,
            "maxItems": 12
        },
        "body": {
            "type": "string",
            "description": "Flat description: the body copy, up to 1,024 characters, with `{{1}}`-style placeholders. Required unless `components` is given. On PATCH, sending `body` re-describes the whole content with the flat fields.",
            "maxLength": 1024
        },
        "header_type": {
            "type": "string",
            "description": "Flat description: header kind \u2014 `none` or `text` (with `header_text`). A media header (image, video, document) needs the sample handle Meta issues on upload, which only `components` can carry (`{\"type\":\"HEADER\",\"format\":\"IMAGE\",\"example\":{\"header_handle\":[\"\u2026\"]}}`).",
            "enum": [
                "none",
                "text"
            ]
        },
        "header_text": {
            "type": "string",
            "description": "Flat description: a one-line text header, up to 60 characters, one placeholder at most. Implies `header_type: text`.",
            "maxLength": 60
        },
        "footer": {
            "type": "string",
            "description": "Flat description: footer copy, up to 60 characters, no placeholders.",
            "maxLength": 60
        },
        "buttons": {
            "type": "array",
            "description": "Flat description: up to ten buttons of type `quick_reply`, `url` (with `url`; a `{{1}}` at its end makes it dynamic and then `example` is required) or `phone_number` (with `phone_number`). For copy-code, flow or catalogue buttons use `components`.",
            "items": {
                "type": "object",
                "description": "One button.",
                "properties": {
                    "type": {
                        "type": "string",
                        "description": "Button type.",
                        "enum": [
                            "quick_reply",
                            "url",
                            "phone_number"
                        ]
                    },
                    "text": {
                        "type": "string",
                        "description": "Button label, up to 25 characters.",
                        "maxLength": 25
                    },
                    "url": {
                        "type": "string",
                        "description": "For `url`: the link, optionally ending in `{{1}}`."
                    },
                    "phone_number": {
                        "type": "string",
                        "description": "For `phone_number`: the number to dial, in E.164."
                    },
                    "example": {
                        "type": "string",
                        "description": "For a dynamic `url`: a sample value for its `{{1}}`."
                    }
                }
            },
            "maxItems": 10
        },
        "variable_samples": {
            "type": "object",
            "description": "Flat description: a sample value per placeholder, keyed by the placeholder (`\"1\"`, `\"2\"` or a name). Meta requires a sample for every placeholder; missing ones get a generic sample, which Meta may reject for marketing copy.",
            "additionalProperties": {
                "type": "string",
                "description": "The sample value."
            }
        }
    }
}
New body copy — re-submits
{
    "body": "Habari {{1}}, oda yako {{2}} iko njiani.",
    "variable_samples": {
        "1": "Asha",
        "2": "ORD-1042"
    }
}
Hide it — no submission
{
    "status": "archived"
}

Responses

200The template as saved, with `submission` when a content change was submitted.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
The template as saved, with `submission` when a content change was submitted.
Show child properties
idintegerrequired
Local template id — what every other template endpoint takes in its path.
namestringrequired
The machine handle Meta knows it by: lowercase letters, digits and underscores, e.g. `order_shipped`. This is `template.name` on `POST /api/v3/whatsapp/send`.
pattern
^[a-z0-9_]{1,512}$
display_namestringoptional
What a person on the dashboard calls it.
languagestringrequired
Language code of the copy, e.g. `sw`, `en`, `en_US`. One template per language.
categorystringrequired
Meta's category. `utility` for transactional notices, `marketing` for promotions, `authentication` for one-time codes. Meta may reclassify a template during review.
enum
["marketing","utility","authentication"]
statusstringrequired
Its state on this platform, before Meta gets a say. Only an `active` one can be sent; `archived` rows are hidden from the default list.
enum
["draft","active","archived"]
whatsapp_statusstringrequired
Where Meta's review got to. `pending` = not yet submitted (or a queued submission); `in_review` = Meta has it; `approved` = usable; `rejected` = see `rejection_reason`; `paused` / `disabled` = Meta stopped it for quality.
enum
["pending","in_review","approved","rejected","disabled","paused"]
approvedbooleanoptional
True when `whatsapp_status` is `approved`.
sendablebooleanoptional
True when it can be sent right now: `status` active AND approved by Meta.
rejection_reasonstring | nulloptional
Meta's reason, when `whatsapp_status` is `rejected` — a review verdict such as `INVALID_FORMAT`, or the refusal it gave the submission itself (a name clash, a body that starts with a variable). Null otherwise.
whatsapp_business_account_idstring | nulloptional
The WhatsApp Business Account (WABA) id it lives on. `GET /api/v3/whatsapp/accounts` lists them.
whatsapp_template_idstring | nulloptional
Meta's own id for the template on that account. Null until a submission has been accepted.
quality_ratingstring | nulloptional
Meta's quality score on this account — `GREEN`, `YELLOW`, `RED` or `UNKNOWN` — once approved and in use. Null before that.
parameter_formatstringoptional
How variables are written: `POSITIONAL` (`{{1}}`, `{{2}}`) or `NAMED` (`{{order_id}}`).
enum
["POSITIONAL","NAMED"]
variablesarray<string>optional
The placeholders the header and body carry, in order — what a send must supply one value each for.
bodystringoptional
The body copy with its placeholders. The full component tree is on the single-template read.
last_synced_atstring | nulloptional
When Meta last told us anything about this template (submission, poll, webhook or refresh).
format
date-time
created_atstring | nulloptional
When the row was created here.
format
date-time
updated_atstring | nulloptional
When the row last changed here.
format
date-time
header_typestringoptional
The header kind: `none`, `text`, `image`, `video` or `document`.
enum
["none","text","image","video","document"]
header_textstring | nulloptional
The header copy when `header_type` is `text`.
footerstring | nulloptional
The footer copy, up to 60 characters.
buttonsarray<object>optional
The flat-field buttons, when the template was described that way. `components` is the complete picture either way.
items.additionalProperties
true
componentsarray<object>required
The template in Meta's `components[]` shape — what is (or would be) submitted for review. Post it back unchanged to create a twin, or edit and PATCH it.
items.additionalProperties
true
Show child properties
typestringrequired
Component type.
enum
["HEADER","BODY","FOOTER","BUTTONS","CAROUSEL","LIMITED_TIME_OFFER"]
formatstringoptional
HEADER only: `TEXT`, `IMAGE`, `VIDEO`, `DOCUMENT` or `LOCATION`.
textstringoptional
HEADER (TEXT), BODY and FOOTER: the copy, with `{{1}}`-style or named placeholders.
exampleobjectoptional
Sample values for every placeholder, in the shape Meta wants: `{"body_text": [["Asha", "ORD-1042"]]}`, `{"header_text": ["Oda"]}`, `{"header_handle": ["<upload handle>"]}`. Required by Meta whenever there is a placeholder or a media header.
additionalProperties
true
buttonsarray<object>optional
BUTTONS only: up to ten buttons — `QUICK_REPLY`, `URL`, `PHONE_NUMBER`, `COPY_CODE`, `FLOW`, `OTP`, `CATALOG`, `MPM`, `VOICE_CALL` — each in Meta's shape.
items.additionalProperties
true
deploymentsarray<object>required
Its review status on every OTHER WhatsApp Business Account it has been deployed to (`POST …/deployments`). Empty until deployed.
Show child properties
idintegeroptional
Deployment id.
whatsapp_business_account_idstringoptional
The WhatsApp Business Account this deployment is on.
whatsapp_template_idstring | nulloptional
Meta's id for the template on THAT account (each account gets its own).
whatsapp_statusstringoptional
Meta's review status on that account.
enum
["pending","in_review","approved","rejected","disabled","paused"]
rejection_reasonstring | nulloptional
Meta's reason when rejected on that account.
quality_ratingstring | nulloptional
Quality score on that account: `GREEN`, `YELLOW`, `RED`, `UNKNOWN` or null.
categorystring | nulloptional
The category Meta assigned on that account, when it differs from the authored one.
last_synced_atstring | nulloptional
When Meta last told us about it on that account.
format
date-time
submissionobjectoptional
Only on responses that submitted to Meta (create, PATCH with content, submit): what happened.
Show child properties
submittedbooleanoptional
True when Meta accepted the submission in this request (read `whatsapp_status` and `whatsapp_template_id`).
queuedbooleanoptional
True when Meta could not be reached and the submission was queued to retry; the template stays `pending` until it lands.
messagestring | nulloptional
Why it was not submitted, when it was not — Meta's own words for a refusal, or that it is queued.
refreshobjectoptional
Only on `POST …/refresh`: `found_on_whatsapp` says whether Meta has the template, `message` explains when it does not.
Show child properties
found_on_whatsappbooleanoptional
False when Meta has no template of this name and language on the account; the row is left as it was.
messagestring | nulloptional
Explanation when not found.
{
    "status": "success",
    "data": {
        "id": 418,
        "name": "order_shipped",
        "display_name": "Order shipped",
        "language": "sw",
        "category": "utility",
        "status": "active",
        "whatsapp_status": "in_review",
        "approved": false,
        "sendable": false,
        "rejection_reason": null,
        "whatsapp_business_account_id": "102290129340398",
        "whatsapp_template_id": "1189456212345678",
        "quality_rating": null,
        "parameter_format": "POSITIONAL",
        "variables": [
            "1",
            "2"
        ],
        "body": "Habari {{1}}, oda yako {{2}} iko njiani.",
        "last_synced_at": "2026-09-13T09:14:02+00:00",
        "created_at": "2026-09-13T09:14:02+00:00",
        "updated_at": "2026-09-13T09:14:02+00:00",
        "header_type": "text",
        "header_text": "Oda {{1}}",
        "footer": "Duka la Amina",
        "buttons": [
            {
                "type": "url",
                "text": "Fuatilia",
                "url": "https://amina.co.tz/track/{{1}}",
                "example": "ORD-1042"
            }
        ],
        "components": [
            {
                "type": "HEADER",
                "format": "TEXT",
                "text": "Oda {{1}}",
                "example": {
                    "header_text": [
                        "ORD-1042"
                    ]
                }
            },
            {
                "type": "BODY",
                "text": "Habari {{1}}, oda yako {{2}} imetumwa leo.",
                "example": {
                    "body_text": [
                        [
                            "Asha",
                            "ORD-1042"
                        ]
                    ]
                }
            },
            {
                "type": "FOOTER",
                "text": "Duka la Amina"
            },
            {
                "type": "BUTTONS",
                "buttons": [
                    {
                        "type": "URL",
                        "text": "Fuatilia",
                        "url": "https://amina.co.tz/track/{{1}}",
                        "example": [
                            "ORD-1042"
                        ]
                    }
                ]
            }
        ],
        "deployments": [],
        "submission": {
            "submitted": true,
            "queued": false,
            "message": null
        }
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}
404No such record for this tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}
422The payload failed validation. `errors` maps each rejected field to its messages.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Validation failed.",
    "errors": {
        "recipient": [
            "Provide recipient or recipients."
        ]
    }
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / WhatsApp templates

Delete a template

DELETE/api/v3/whatsapp/templates/{template}

Removes THIS language of the template from the WhatsApp Business Account (Meta keeps other languages of the same name) and archives the row here, so campaign history that pointed at it still reads. permanent=true deletes the row as well. A template never submitted has nothing to remove from Meta, and removed_from_whatsapp is false.

AuthenticationTenant API token

Required permission: communications.templates.manage

Path parameters

templateintegerrequired
Local template id, from the list or the create response.

Example: 418

Query parameters

permanentbooleanoptional
Delete the local row too, not just archive it.
default
false

Example:

Responses

200What was done.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
What was done.
Show child properties
idintegeroptional
The template id.
archivedbooleanoptional
True when the row was archived (the default).
deletedbooleanoptional
True when the row was deleted (`permanent=true`).
removed_from_whatsappbooleanoptional
True when Meta confirmed the removal of this language from the account.
{
    "status": "success",
    "data": {
        "id": 418,
        "archived": true,
        "deleted": false,
        "removed_from_whatsapp": true
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}
404No such record for this tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / WhatsApp templates

Submit to Meta for review

POST/api/v3/whatsapp/templates/{template}/submit

Submits (or re-submits) the template to Meta now, inside the request, and answers with the template as Meta left it. Use it after fixing a rejected template, or for one created with submit: false. A template Meta already holds is edited rather than created again; one Meta is still reviewing is left alone and its current status recorded. Meta allows an approved template about ten edits a month.

AuthenticationTenant API token

Required permission: communications.templates.manage

Path parameters

templateintegerrequired
Local template id, from the list or the create response.

Example: 418

Responses

200The template, with `submission` saying what happened.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
The template, with `submission` saying what happened.
Show child properties
idintegerrequired
Local template id — what every other template endpoint takes in its path.
namestringrequired
The machine handle Meta knows it by: lowercase letters, digits and underscores, e.g. `order_shipped`. This is `template.name` on `POST /api/v3/whatsapp/send`.
pattern
^[a-z0-9_]{1,512}$
display_namestringoptional
What a person on the dashboard calls it.
languagestringrequired
Language code of the copy, e.g. `sw`, `en`, `en_US`. One template per language.
categorystringrequired
Meta's category. `utility` for transactional notices, `marketing` for promotions, `authentication` for one-time codes. Meta may reclassify a template during review.
enum
["marketing","utility","authentication"]
statusstringrequired
Its state on this platform, before Meta gets a say. Only an `active` one can be sent; `archived` rows are hidden from the default list.
enum
["draft","active","archived"]
whatsapp_statusstringrequired
Where Meta's review got to. `pending` = not yet submitted (or a queued submission); `in_review` = Meta has it; `approved` = usable; `rejected` = see `rejection_reason`; `paused` / `disabled` = Meta stopped it for quality.
enum
["pending","in_review","approved","rejected","disabled","paused"]
approvedbooleanoptional
True when `whatsapp_status` is `approved`.
sendablebooleanoptional
True when it can be sent right now: `status` active AND approved by Meta.
rejection_reasonstring | nulloptional
Meta's reason, when `whatsapp_status` is `rejected` — a review verdict such as `INVALID_FORMAT`, or the refusal it gave the submission itself (a name clash, a body that starts with a variable). Null otherwise.
whatsapp_business_account_idstring | nulloptional
The WhatsApp Business Account (WABA) id it lives on. `GET /api/v3/whatsapp/accounts` lists them.
whatsapp_template_idstring | nulloptional
Meta's own id for the template on that account. Null until a submission has been accepted.
quality_ratingstring | nulloptional
Meta's quality score on this account — `GREEN`, `YELLOW`, `RED` or `UNKNOWN` — once approved and in use. Null before that.
parameter_formatstringoptional
How variables are written: `POSITIONAL` (`{{1}}`, `{{2}}`) or `NAMED` (`{{order_id}}`).
enum
["POSITIONAL","NAMED"]
variablesarray<string>optional
The placeholders the header and body carry, in order — what a send must supply one value each for.
bodystringoptional
The body copy with its placeholders. The full component tree is on the single-template read.
last_synced_atstring | nulloptional
When Meta last told us anything about this template (submission, poll, webhook or refresh).
format
date-time
created_atstring | nulloptional
When the row was created here.
format
date-time
updated_atstring | nulloptional
When the row last changed here.
format
date-time
header_typestringoptional
The header kind: `none`, `text`, `image`, `video` or `document`.
enum
["none","text","image","video","document"]
header_textstring | nulloptional
The header copy when `header_type` is `text`.
footerstring | nulloptional
The footer copy, up to 60 characters.
buttonsarray<object>optional
The flat-field buttons, when the template was described that way. `components` is the complete picture either way.
items.additionalProperties
true
componentsarray<object>required
The template in Meta's `components[]` shape — what is (or would be) submitted for review. Post it back unchanged to create a twin, or edit and PATCH it.
items.additionalProperties
true
Show child properties
typestringrequired
Component type.
enum
["HEADER","BODY","FOOTER","BUTTONS","CAROUSEL","LIMITED_TIME_OFFER"]
formatstringoptional
HEADER only: `TEXT`, `IMAGE`, `VIDEO`, `DOCUMENT` or `LOCATION`.
textstringoptional
HEADER (TEXT), BODY and FOOTER: the copy, with `{{1}}`-style or named placeholders.
exampleobjectoptional
Sample values for every placeholder, in the shape Meta wants: `{"body_text": [["Asha", "ORD-1042"]]}`, `{"header_text": ["Oda"]}`, `{"header_handle": ["<upload handle>"]}`. Required by Meta whenever there is a placeholder or a media header.
additionalProperties
true
buttonsarray<object>optional
BUTTONS only: up to ten buttons — `QUICK_REPLY`, `URL`, `PHONE_NUMBER`, `COPY_CODE`, `FLOW`, `OTP`, `CATALOG`, `MPM`, `VOICE_CALL` — each in Meta's shape.
items.additionalProperties
true
deploymentsarray<object>required
Its review status on every OTHER WhatsApp Business Account it has been deployed to (`POST …/deployments`). Empty until deployed.
Show child properties
idintegeroptional
Deployment id.
whatsapp_business_account_idstringoptional
The WhatsApp Business Account this deployment is on.
whatsapp_template_idstring | nulloptional
Meta's id for the template on THAT account (each account gets its own).
whatsapp_statusstringoptional
Meta's review status on that account.
enum
["pending","in_review","approved","rejected","disabled","paused"]
rejection_reasonstring | nulloptional
Meta's reason when rejected on that account.
quality_ratingstring | nulloptional
Quality score on that account: `GREEN`, `YELLOW`, `RED`, `UNKNOWN` or null.
categorystring | nulloptional
The category Meta assigned on that account, when it differs from the authored one.
last_synced_atstring | nulloptional
When Meta last told us about it on that account.
format
date-time
submissionobjectoptional
Only on responses that submitted to Meta (create, PATCH with content, submit): what happened.
Show child properties
submittedbooleanoptional
True when Meta accepted the submission in this request (read `whatsapp_status` and `whatsapp_template_id`).
queuedbooleanoptional
True when Meta could not be reached and the submission was queued to retry; the template stays `pending` until it lands.
messagestring | nulloptional
Why it was not submitted, when it was not — Meta's own words for a refusal, or that it is queued.
refreshobjectoptional
Only on `POST …/refresh`: `found_on_whatsapp` says whether Meta has the template, `message` explains when it does not.
Show child properties
found_on_whatsappbooleanoptional
False when Meta has no template of this name and language on the account; the row is left as it was.
messagestring | nulloptional
Explanation when not found.
{
    "status": "success",
    "data": {
        "id": 418,
        "name": "order_shipped",
        "display_name": "Order shipped",
        "language": "sw",
        "category": "utility",
        "status": "active",
        "whatsapp_status": "in_review",
        "approved": false,
        "sendable": false,
        "rejection_reason": null,
        "whatsapp_business_account_id": "102290129340398",
        "whatsapp_template_id": "1189456212345678",
        "quality_rating": null,
        "parameter_format": "POSITIONAL",
        "variables": [
            "1",
            "2"
        ],
        "body": "Habari {{1}}, oda yako {{2}} imetumwa leo.",
        "last_synced_at": "2026-09-13T09:14:02+00:00",
        "created_at": "2026-09-13T09:14:02+00:00",
        "updated_at": "2026-09-13T09:14:02+00:00",
        "header_type": "text",
        "header_text": "Oda {{1}}",
        "footer": "Duka la Amina",
        "buttons": [
            {
                "type": "url",
                "text": "Fuatilia",
                "url": "https://amina.co.tz/track/{{1}}",
                "example": "ORD-1042"
            }
        ],
        "components": [
            {
                "type": "HEADER",
                "format": "TEXT",
                "text": "Oda {{1}}",
                "example": {
                    "header_text": [
                        "ORD-1042"
                    ]
                }
            },
            {
                "type": "BODY",
                "text": "Habari {{1}}, oda yako {{2}} imetumwa leo.",
                "example": {
                    "body_text": [
                        [
                            "Asha",
                            "ORD-1042"
                        ]
                    ]
                }
            },
            {
                "type": "FOOTER",
                "text": "Duka la Amina"
            },
            {
                "type": "BUTTONS",
                "buttons": [
                    {
                        "type": "URL",
                        "text": "Fuatilia",
                        "url": "https://amina.co.tz/track/{{1}}",
                        "example": [
                            "ORD-1042"
                        ]
                    }
                ]
            }
        ],
        "deployments": [],
        "submission": {
            "submitted": true,
            "queued": false,
            "message": null
        }
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}
404No such record for this tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / WhatsApp templates

Ask Meta for the current status

POST/api/v3/whatsapp/templates/{template}/refresh

Reads the template from Meta right now — review status, rejection reason, quality score — and writes it on the row. The background poll and Meta's status webhook keep rows current on their own; this is for the moment you need the answer, such as right after …/submit. By Meta id when the row has one, by name and language otherwise. refresh.found_on_whatsapp is false when Meta has no such template on the account, and the row is left as it was.

AuthenticationTenant API token

Required permission: communications.templates.view

Path parameters

templateintegerrequired
Local template id, from the list or the create response.

Example: 418

Responses

200The template as Meta reports it.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
The template as Meta reports it.
Show child properties
idintegerrequired
Local template id — what every other template endpoint takes in its path.
namestringrequired
The machine handle Meta knows it by: lowercase letters, digits and underscores, e.g. `order_shipped`. This is `template.name` on `POST /api/v3/whatsapp/send`.
pattern
^[a-z0-9_]{1,512}$
display_namestringoptional
What a person on the dashboard calls it.
languagestringrequired
Language code of the copy, e.g. `sw`, `en`, `en_US`. One template per language.
categorystringrequired
Meta's category. `utility` for transactional notices, `marketing` for promotions, `authentication` for one-time codes. Meta may reclassify a template during review.
enum
["marketing","utility","authentication"]
statusstringrequired
Its state on this platform, before Meta gets a say. Only an `active` one can be sent; `archived` rows are hidden from the default list.
enum
["draft","active","archived"]
whatsapp_statusstringrequired
Where Meta's review got to. `pending` = not yet submitted (or a queued submission); `in_review` = Meta has it; `approved` = usable; `rejected` = see `rejection_reason`; `paused` / `disabled` = Meta stopped it for quality.
enum
["pending","in_review","approved","rejected","disabled","paused"]
approvedbooleanoptional
True when `whatsapp_status` is `approved`.
sendablebooleanoptional
True when it can be sent right now: `status` active AND approved by Meta.
rejection_reasonstring | nulloptional
Meta's reason, when `whatsapp_status` is `rejected` — a review verdict such as `INVALID_FORMAT`, or the refusal it gave the submission itself (a name clash, a body that starts with a variable). Null otherwise.
whatsapp_business_account_idstring | nulloptional
The WhatsApp Business Account (WABA) id it lives on. `GET /api/v3/whatsapp/accounts` lists them.
whatsapp_template_idstring | nulloptional
Meta's own id for the template on that account. Null until a submission has been accepted.
quality_ratingstring | nulloptional
Meta's quality score on this account — `GREEN`, `YELLOW`, `RED` or `UNKNOWN` — once approved and in use. Null before that.
parameter_formatstringoptional
How variables are written: `POSITIONAL` (`{{1}}`, `{{2}}`) or `NAMED` (`{{order_id}}`).
enum
["POSITIONAL","NAMED"]
variablesarray<string>optional
The placeholders the header and body carry, in order — what a send must supply one value each for.
bodystringoptional
The body copy with its placeholders. The full component tree is on the single-template read.
last_synced_atstring | nulloptional
When Meta last told us anything about this template (submission, poll, webhook or refresh).
format
date-time
created_atstring | nulloptional
When the row was created here.
format
date-time
updated_atstring | nulloptional
When the row last changed here.
format
date-time
header_typestringoptional
The header kind: `none`, `text`, `image`, `video` or `document`.
enum
["none","text","image","video","document"]
header_textstring | nulloptional
The header copy when `header_type` is `text`.
footerstring | nulloptional
The footer copy, up to 60 characters.
buttonsarray<object>optional
The flat-field buttons, when the template was described that way. `components` is the complete picture either way.
items.additionalProperties
true
componentsarray<object>required
The template in Meta's `components[]` shape — what is (or would be) submitted for review. Post it back unchanged to create a twin, or edit and PATCH it.
items.additionalProperties
true
Show child properties
typestringrequired
Component type.
enum
["HEADER","BODY","FOOTER","BUTTONS","CAROUSEL","LIMITED_TIME_OFFER"]
formatstringoptional
HEADER only: `TEXT`, `IMAGE`, `VIDEO`, `DOCUMENT` or `LOCATION`.
textstringoptional
HEADER (TEXT), BODY and FOOTER: the copy, with `{{1}}`-style or named placeholders.
exampleobjectoptional
Sample values for every placeholder, in the shape Meta wants: `{"body_text": [["Asha", "ORD-1042"]]}`, `{"header_text": ["Oda"]}`, `{"header_handle": ["<upload handle>"]}`. Required by Meta whenever there is a placeholder or a media header.
additionalProperties
true
buttonsarray<object>optional
BUTTONS only: up to ten buttons — `QUICK_REPLY`, `URL`, `PHONE_NUMBER`, `COPY_CODE`, `FLOW`, `OTP`, `CATALOG`, `MPM`, `VOICE_CALL` — each in Meta's shape.
items.additionalProperties
true
deploymentsarray<object>required
Its review status on every OTHER WhatsApp Business Account it has been deployed to (`POST …/deployments`). Empty until deployed.
Show child properties
idintegeroptional
Deployment id.
whatsapp_business_account_idstringoptional
The WhatsApp Business Account this deployment is on.
whatsapp_template_idstring | nulloptional
Meta's id for the template on THAT account (each account gets its own).
whatsapp_statusstringoptional
Meta's review status on that account.
enum
["pending","in_review","approved","rejected","disabled","paused"]
rejection_reasonstring | nulloptional
Meta's reason when rejected on that account.
quality_ratingstring | nulloptional
Quality score on that account: `GREEN`, `YELLOW`, `RED`, `UNKNOWN` or null.
categorystring | nulloptional
The category Meta assigned on that account, when it differs from the authored one.
last_synced_atstring | nulloptional
When Meta last told us about it on that account.
format
date-time
submissionobjectoptional
Only on responses that submitted to Meta (create, PATCH with content, submit): what happened.
Show child properties
submittedbooleanoptional
True when Meta accepted the submission in this request (read `whatsapp_status` and `whatsapp_template_id`).
queuedbooleanoptional
True when Meta could not be reached and the submission was queued to retry; the template stays `pending` until it lands.
messagestring | nulloptional
Why it was not submitted, when it was not — Meta's own words for a refusal, or that it is queued.
refreshobjectoptional
Only on `POST …/refresh`: `found_on_whatsapp` says whether Meta has the template, `message` explains when it does not.
Show child properties
found_on_whatsappbooleanoptional
False when Meta has no template of this name and language on the account; the row is left as it was.
messagestring | nulloptional
Explanation when not found.
{
    "status": "success",
    "data": {
        "id": 418,
        "name": "order_shipped",
        "display_name": "Order shipped",
        "language": "sw",
        "category": "utility",
        "status": "active",
        "whatsapp_status": "approved",
        "approved": true,
        "sendable": true,
        "rejection_reason": null,
        "whatsapp_business_account_id": "102290129340398",
        "whatsapp_template_id": "1189456212345678",
        "quality_rating": "GREEN",
        "parameter_format": "POSITIONAL",
        "variables": [
            "1",
            "2"
        ],
        "body": "Habari {{1}}, oda yako {{2}} imetumwa leo.",
        "last_synced_at": "2026-09-13T09:14:02+00:00",
        "created_at": "2026-09-13T09:14:02+00:00",
        "updated_at": "2026-09-13T09:14:02+00:00",
        "header_type": "text",
        "header_text": "Oda {{1}}",
        "footer": "Duka la Amina",
        "buttons": [
            {
                "type": "url",
                "text": "Fuatilia",
                "url": "https://amina.co.tz/track/{{1}}",
                "example": "ORD-1042"
            }
        ],
        "components": [
            {
                "type": "HEADER",
                "format": "TEXT",
                "text": "Oda {{1}}",
                "example": {
                    "header_text": [
                        "ORD-1042"
                    ]
                }
            },
            {
                "type": "BODY",
                "text": "Habari {{1}}, oda yako {{2}} imetumwa leo.",
                "example": {
                    "body_text": [
                        [
                            "Asha",
                            "ORD-1042"
                        ]
                    ]
                }
            },
            {
                "type": "FOOTER",
                "text": "Duka la Amina"
            },
            {
                "type": "BUTTONS",
                "buttons": [
                    {
                        "type": "URL",
                        "text": "Fuatilia",
                        "url": "https://amina.co.tz/track/{{1}}",
                        "example": [
                            "ORD-1042"
                        ]
                    }
                ]
            }
        ],
        "deployments": [],
        "refresh": {
            "found_on_whatsapp": true,
            "message": null
        }
    }
}
409Refused because of the workspace's current state; the message says what to do.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "No WhatsApp business account is connected for this template."
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}
404No such record for this tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}
502WhatsApp/Meta refused the call. The message repeats what they said; the record was not changed.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "(#100) Invalid parameter: product image_url is not reachable."
}

API REFERENCE / WhatsApp templates

Deploy to more business accounts

POST/api/v3/whatsapp/templates/{template}/deployments

Puts an authored template on other WhatsApp Business Accounts of this workspace. Each account reviews it independently, so each gets its own deployment with its own Meta id and status — reported on the template's deployments and announced by template.status_changed with that account's id. Submission to each account is queued (202); read the template, or subscribe to the webhook, for the outcome. Deploying to an account it is already on re-submits it there.

AuthenticationTenant API token

Required permission: communications.templates.manage

Path parameters

templateintegerrequired
Local template id, from the list or the create response.

Example: 418

Request body

application/json · required

The accounts to deploy to.

waba_idsarray<string>required
WhatsApp Business Account ids from `GET /api/v3/whatsapp/accounts`. One to twenty. `GET /api/v3/whatsapp/accounts` lists them (`waba_id`).
minItems
1
maxItems
20
Complete request schema
{
    "type": "object",
    "required": [
        "waba_ids"
    ],
    "properties": {
        "waba_ids": {
            "type": "array",
            "description": "WhatsApp Business Account ids from `GET /api/v3/whatsapp/accounts`. One to twenty. `GET /api/v3/whatsapp/accounts` lists them (`waba_id`).",
            "items": {
                "type": "string",
                "description": "A WABA id."
            },
            "minItems": 1,
            "maxItems": 20
        }
    }
}

Responses

202The deployments, queued for submission.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
The deployments, queued for submission.
Show child properties
template_idintegeroptional
The template.
deploymentsarray<object>optional
One per account, each now `pending` until its submission lands.
Show child properties
idintegeroptional
Deployment id.
whatsapp_business_account_idstringoptional
The WhatsApp Business Account this deployment is on.
whatsapp_template_idstring | nulloptional
Meta's id for the template on THAT account (each account gets its own).
whatsapp_statusstringoptional
Meta's review status on that account.
enum
["pending","in_review","approved","rejected","disabled","paused"]
rejection_reasonstring | nulloptional
Meta's reason when rejected on that account.
quality_ratingstring | nulloptional
Quality score on that account: `GREEN`, `YELLOW`, `RED`, `UNKNOWN` or null.
categorystring | nulloptional
The category Meta assigned on that account, when it differs from the authored one.
last_synced_atstring | nulloptional
When Meta last told us about it on that account.
format
date-time
{
    "status": "success",
    "data": {
        "template_id": 418,
        "deployments": [
            {
                "id": 91,
                "whatsapp_business_account_id": "102290129340777",
                "whatsapp_template_id": null,
                "whatsapp_status": "pending",
                "rejection_reason": null,
                "quality_rating": null,
                "category": null,
                "last_synced_at": null
            }
        ]
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}
404No such record for this tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}
422The payload failed validation. `errors` maps each rejected field to its messages.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Validation failed.",
    "errors": {
        "recipient": [
            "Provide recipient or recipients."
        ]
    }
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / WhatsApp

List the WhatsApp business accounts and their numbers

GET/api/v3/whatsapp/accounts

Where the identifiers come from. Every WhatsApp Business Account connected to this workspace with its phone numbers: phone_numbers[].id is the value sender_id takes on /whatsapp/send and the group endpoints and from takes on the catalogue sends and flow sessions (the number itself is accepted there too); waba_id is what the templates endpoints take. default_phone_number_id — and the is_default flags — say what a send without sender_id goes out from; null means there is no usable default and every send must name one. Readable by a key whose issuer may read the workspace's accounts or holds any permission that uses these ids. Credentials are never included.

AuthenticationTenant API token

Required permission: communications.accounts.view (or any permission that uses these ids)

Responses

200The connected accounts.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
The connected accounts.
Show child properties
default_phone_number_idstring | nulloptional
The phone_number_id a send without `sender_id` goes out from; null when no default is configured.
itemsarray<object>optional
The accounts, active first.
Show child properties
waba_idstringrequired
The WhatsApp Business Account id — `waba_id` on the templates endpoints.
namestring | nulloptional
The business name Meta shows for the account.
is_activebooleanrequired
False once the account has been disconnected; kept so its templates and catalogues still resolve.
is_defaultbooleanoptional
True for the account a send without `sender_id` goes out from.
phone_numbersarray<object>required
The business phone numbers on the account.
Show child properties
idstringoptional
Meta's phone_number_id — what `sender_id` and `from` take. The number itself (`phone_number` or `display`) is accepted there too.
phone_numberstring | nulloptional
The number in E.164, when the connection recorded it.
displaystringoptional
The number as displayed.
is_defaultbooleanoptional
True for the number a send without `sender_id` goes out from.
quality_ratingstring | nulloptional
The account's quality rating from Meta, when known.
messaging_tierstring | nulloptional
The account's messaging limit tier from Meta, when known.
templates_countintegeroptional
How many templates live on this account here.
catalogues_countintegeroptional
How many catalogues are connected to this account.
last_synced_atstring | nulloptional
When the account details were last refreshed from the connection.
format
date-time
{
    "status": "success",
    "data": {
        "default_phone_number_id": "104512345678901",
        "items": [
            {
                "waba_id": "102290129340398",
                "name": "Duka la Amina",
                "is_active": true,
                "is_default": true,
                "phone_numbers": [
                    {
                        "id": "104512345678901",
                        "phone_number": "+255700000001",
                        "display": "+255 700 000 001",
                        "is_default": true
                    }
                ],
                "quality_rating": "GREEN",
                "messaging_tier": "TIER_1K",
                "templates_count": 12,
                "catalogues_count": 1,
                "last_synced_at": "2026-09-13T09:14:02+00:00"
            }
        ]
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / SMS

List the identities an SMS can be sent as

GET/api/v3/sms/senders

Where sender_id comes from. Every identity this workspace may send SMS as — approved alphanumeric sender IDs, its own SMS-capable phone numbers, and its active short codes — with value being exactly what sender_id takes on /sms/send and /sms/campaign. Only usable identities are listed (a sender ID still under review is not). Readable by a key whose issuer may read the workspace's accounts or holds any permission that uses these ids.

AuthenticationTenant API token

Required permission: communications.accounts.view (or any permission that uses these ids)

Responses

200The sending identities.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
The sending identities.
Show child properties
itemsarray<object>optional
The identities, sender IDs first, then numbers, then short codes.
Show child properties
typestringrequired
What kind of identity: an approved alphanumeric sender ID, one of the workspace's own SMS-capable numbers, or an active short code.
enum
["sender_id","phone_number","short_code"]
valuestringrequired
Pass this as `sender_id`.
labelstringrequired
A human name for it: the sender ID itself, the number's label, or the short code's description.
statusstringrequired
`approved` for a sender ID, `active` for a number or short code — only usable identities are listed.
enum
["approved","active"]
{
    "status": "success",
    "data": {
        "items": [
            {
                "type": "sender_id",
                "value": "AMINA",
                "label": "AMINA",
                "status": "approved"
            },
            {
                "type": "phone_number",
                "value": "+255700000001",
                "label": "Main line",
                "status": "active"
            },
            {
                "type": "short_code",
                "value": "15551",
                "label": "Promotions",
                "status": "active"
            }
        ]
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Contacts

List contact groups

GET/api/v3/contact-groups

Where {group_id} comes from. Every contact path takes a group, and until now an integrator had to read its id off the dashboard — this is the list, with how many people each group holds, how many still accept messages, and the extra fields its contacts may carry. Either the numeric id or the uid works anywhere a group is named.

AuthenticationTenant API token

Required permission: contacts.view

Query parameters

statusstringoptional
Narrow to groups in this state; `all` includes retired ones (the default returns every group).
enum
["active","inactive","all"]

Example: active

qstringoptional
Free text over the group name.
maxLength
120

Example: Dar

limitintegeroptional
Rows per page, 1–100. Defaults to 20 (25 for catalogue endpoints). Values above 100 are clamped.
minimum
1
maximum
100
default
20

Example: 20

pageintegeroptional
Page number, 1-based. Read `data.pagination.has_more_pages` to know when to stop.
minimum
1
default
1

Example: 1

Responses

200The groups, most recently changed first.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
The groups, most recently changed first.
Show child properties
itemsarray<object>optional
The groups, most recently changed first.
Show child properties
idintegerrequired
Numeric id. Accepted anywhere a group is named.
uidstringrequired
Stable UUID. Accepted anywhere the numeric id is, and the one to store.
format
uuid
namestringrequired
What the group is called.
statusstringrequired
`active`, or `inactive` for a group kept for its history but no longer in use.
enum
["active","inactive"]
contacts_countintegerrequired
How many people are in it, subscribed or not.
subscribed_countintegerrequired
How many of those still accept messages — the number a campaign would actually reach.
custom_fieldsarray<object>required
The extra fields this group declares.
Show child properties
keystringrequired
The key a contact carries it under, and what a campaign addresses as `{{cf:key}}`. Letters, digits, `_`, `.` and `-`.
maxLength
64
pattern
^[A-Za-z0-9_.-]+$
labelstringoptional
What a person reading the dashboard sees. Defaults to the key.
maxLength
120
typestringoptional
How the dashboard renders and validates it. Defaults to `text`.
enum
["text","number","email","date","select"]
created_atstring | nulloptional
When the group was created.
format
date-time
updated_atstring | nulloptional
When it last changed.
format
date-time
paginationobjectoptional
Page state.
Show child properties
current_pageintegerrequired
1-based current page index.
per_pageintegerrequired
Number of items per page.
last_pageintegerrequired
1-based index of the last page.
totalintegerrequired
Total number of items across all pages.
has_more_pagesbooleanrequired
True if more pages exist after the current page.
{
    "status": "success",
    "data": {
        "items": [
            {
                "id": 42,
                "uid": "9f3a1c20-5d6e-4a71-9c3b-2f1a0e7b8c44",
                "name": "Wateja wa Dar",
                "status": "active",
                "contacts_count": 1180,
                "subscribed_count": 1094,
                "custom_fields": [
                    {
                        "key": "order_ref",
                        "label": "Order reference",
                        "type": "text"
                    },
                    {
                        "key": "branch",
                        "label": "Branch",
                        "type": "select"
                    }
                ],
                "created_at": "2026-09-14T09:14:02+00:00",
                "updated_at": "2026-09-14T09:14:02+00:00"
            }
        ],
        "pagination": {
            "current_page": 1,
            "per_page": 25,
            "last_page": 1,
            "total": 1,
            "has_more_pages": false
        }
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}
422The payload failed validation. `errors` maps each rejected field to its messages.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Validation failed.",
    "errors": {
        "recipient": [
            "Provide recipient or recipients."
        ]
    }
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Contacts

Create a contact group

POST/api/v3/contact-groups

Creates a group to put contacts in. custom_fields declares the extra fields its contacts may carry — a contact can still be given an undeclared key, but a declared one is what the dashboard shows a column for and what a campaign addresses as {{cf:key}}. Duplicate keys are refused.

AuthenticationTenant API token

Required permission: contacts.create

Request body

application/json · required

The group.

namestringoptional
What to call the group.
maxLength
160
statusstringoptional
Defaults to `active` on create.
enum
["active","inactive"]
custom_fieldsarray<object>optional
The extra fields contacts in this group may carry. On PATCH this REPLACES the declaration — a schema is not a thing to merge — and removing a field does not touch values already stored on contacts, they are simply no longer declared.
maxItems
50
Show child properties
keystringrequired
The key a contact carries it under, and what a campaign addresses as `{{cf:key}}`. Letters, digits, `_`, `.` and `-`.
maxLength
64
pattern
^[A-Za-z0-9_.-]+$
labelstringoptional
What a person reading the dashboard sees. Defaults to the key.
maxLength
120
typestringoptional
How the dashboard renders and validates it. Defaults to `text`.
enum
["text","number","email","date","select"]
Complete request schema
{
    "type": "object",
    "description": "What `POST /api/v3/contact-groups` takes, and what `PATCH` accepts a subset of.",
    "properties": {
        "name": {
            "type": "string",
            "description": "What to call the group.",
            "maxLength": 160
        },
        "status": {
            "type": "string",
            "description": "Defaults to `active` on create.",
            "enum": [
                "active",
                "inactive"
            ]
        },
        "custom_fields": {
            "type": "array",
            "description": "The extra fields contacts in this group may carry. On PATCH this REPLACES the declaration \u2014 a schema is not a thing to merge \u2014 and removing a field does not touch values already stored on contacts, they are simply no longer declared.",
            "items": {
                "$ref": "#/components/schemas/ContactGroupField"
            },
            "maxItems": 50
        }
    }
}

Responses

201The group as created.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
The group as created.
Show child properties
idintegerrequired
Numeric id. Accepted anywhere a group is named.
uidstringrequired
Stable UUID. Accepted anywhere the numeric id is, and the one to store.
format
uuid
namestringrequired
What the group is called.
statusstringrequired
`active`, or `inactive` for a group kept for its history but no longer in use.
enum
["active","inactive"]
contacts_countintegerrequired
How many people are in it, subscribed or not.
subscribed_countintegerrequired
How many of those still accept messages — the number a campaign would actually reach.
custom_fieldsarray<object>required
The extra fields this group declares.
Show child properties
keystringrequired
The key a contact carries it under, and what a campaign addresses as `{{cf:key}}`. Letters, digits, `_`, `.` and `-`.
maxLength
64
pattern
^[A-Za-z0-9_.-]+$
labelstringoptional
What a person reading the dashboard sees. Defaults to the key.
maxLength
120
typestringoptional
How the dashboard renders and validates it. Defaults to `text`.
enum
["text","number","email","date","select"]
created_atstring | nulloptional
When the group was created.
format
date-time
updated_atstring | nulloptional
When it last changed.
format
date-time
{
    "status": "success",
    "data": {
        "id": 42,
        "uid": "9f3a1c20-5d6e-4a71-9c3b-2f1a0e7b8c44",
        "name": "Wateja wa Dar",
        "status": "active",
        "contacts_count": 1180,
        "subscribed_count": 1094,
        "custom_fields": [
            {
                "key": "order_ref",
                "label": "Order reference",
                "type": "text"
            },
            {
                "key": "branch",
                "label": "Branch",
                "type": "select"
            }
        ],
        "created_at": "2026-09-14T09:14:02+00:00",
        "updated_at": "2026-09-14T09:14:02+00:00"
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}
422The payload failed validation. `errors` maps each rejected field to its messages.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Validation failed.",
    "errors": {
        "recipient": [
            "Provide recipient or recipients."
        ]
    }
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Contacts

Read a contact group

GET/api/v3/contact-groups/{group}

One group with its counts and the extra fields it declares — what a caller needs before writing a contact into it.

AuthenticationTenant API token

Required permission: contacts.view

Path parameters

groupstringrequired
The group, by numeric id or UUID.

Example: 42

Responses

200The group.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
The group.
Show child properties
idintegerrequired
Numeric id. Accepted anywhere a group is named.
uidstringrequired
Stable UUID. Accepted anywhere the numeric id is, and the one to store.
format
uuid
namestringrequired
What the group is called.
statusstringrequired
`active`, or `inactive` for a group kept for its history but no longer in use.
enum
["active","inactive"]
contacts_countintegerrequired
How many people are in it, subscribed or not.
subscribed_countintegerrequired
How many of those still accept messages — the number a campaign would actually reach.
custom_fieldsarray<object>required
The extra fields this group declares.
Show child properties
keystringrequired
The key a contact carries it under, and what a campaign addresses as `{{cf:key}}`. Letters, digits, `_`, `.` and `-`.
maxLength
64
pattern
^[A-Za-z0-9_.-]+$
labelstringoptional
What a person reading the dashboard sees. Defaults to the key.
maxLength
120
typestringoptional
How the dashboard renders and validates it. Defaults to `text`.
enum
["text","number","email","date","select"]
created_atstring | nulloptional
When the group was created.
format
date-time
updated_atstring | nulloptional
When it last changed.
format
date-time
{
    "status": "success",
    "data": {
        "id": 42,
        "uid": "9f3a1c20-5d6e-4a71-9c3b-2f1a0e7b8c44",
        "name": "Wateja wa Dar",
        "status": "active",
        "contacts_count": 1180,
        "subscribed_count": 1094,
        "custom_fields": [
            {
                "key": "order_ref",
                "label": "Order reference",
                "type": "text"
            },
            {
                "key": "branch",
                "label": "Branch",
                "type": "select"
            }
        ],
        "created_at": "2026-09-14T09:14:02+00:00",
        "updated_at": "2026-09-14T09:14:02+00:00"
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}
404No such record for this tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Contacts

Change a contact group

PATCH/api/v3/contact-groups/{group}

Rename a group, retire it with status: inactive, or change the fields it declares. custom_fields replaces the declaration rather than merging into it; the values already stored on contacts are untouched either way.

AuthenticationTenant API token

Required permission: contacts.edit

Path parameters

groupstringrequired
The group, by numeric id or UUID.

Example: 42

Request body

application/json · required

The fields to change.

namestringoptional
What to call the group.
maxLength
160
statusstringoptional
Defaults to `active` on create.
enum
["active","inactive"]
custom_fieldsarray<object>optional
The extra fields contacts in this group may carry. On PATCH this REPLACES the declaration — a schema is not a thing to merge — and removing a field does not touch values already stored on contacts, they are simply no longer declared.
maxItems
50
Show child properties
keystringrequired
The key a contact carries it under, and what a campaign addresses as `{{cf:key}}`. Letters, digits, `_`, `.` and `-`.
maxLength
64
pattern
^[A-Za-z0-9_.-]+$
labelstringoptional
What a person reading the dashboard sees. Defaults to the key.
maxLength
120
typestringoptional
How the dashboard renders and validates it. Defaults to `text`.
enum
["text","number","email","date","select"]
Complete request schema
{
    "type": "object",
    "description": "What `POST /api/v3/contact-groups` takes, and what `PATCH` accepts a subset of.",
    "properties": {
        "name": {
            "type": "string",
            "description": "What to call the group.",
            "maxLength": 160
        },
        "status": {
            "type": "string",
            "description": "Defaults to `active` on create.",
            "enum": [
                "active",
                "inactive"
            ]
        },
        "custom_fields": {
            "type": "array",
            "description": "The extra fields contacts in this group may carry. On PATCH this REPLACES the declaration \u2014 a schema is not a thing to merge \u2014 and removing a field does not touch values already stored on contacts, they are simply no longer declared.",
            "items": {
                "$ref": "#/components/schemas/ContactGroupField"
            },
            "maxItems": 50
        }
    }
}

Responses

200The group as saved.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
The group as saved.
Show child properties
idintegerrequired
Numeric id. Accepted anywhere a group is named.
uidstringrequired
Stable UUID. Accepted anywhere the numeric id is, and the one to store.
format
uuid
namestringrequired
What the group is called.
statusstringrequired
`active`, or `inactive` for a group kept for its history but no longer in use.
enum
["active","inactive"]
contacts_countintegerrequired
How many people are in it, subscribed or not.
subscribed_countintegerrequired
How many of those still accept messages — the number a campaign would actually reach.
custom_fieldsarray<object>required
The extra fields this group declares.
Show child properties
keystringrequired
The key a contact carries it under, and what a campaign addresses as `{{cf:key}}`. Letters, digits, `_`, `.` and `-`.
maxLength
64
pattern
^[A-Za-z0-9_.-]+$
labelstringoptional
What a person reading the dashboard sees. Defaults to the key.
maxLength
120
typestringoptional
How the dashboard renders and validates it. Defaults to `text`.
enum
["text","number","email","date","select"]
created_atstring | nulloptional
When the group was created.
format
date-time
updated_atstring | nulloptional
When it last changed.
format
date-time
{
    "status": "success",
    "data": {
        "id": 42,
        "uid": "9f3a1c20-5d6e-4a71-9c3b-2f1a0e7b8c44",
        "name": "Wateja wa Dar",
        "status": "active",
        "contacts_count": 1180,
        "subscribed_count": 1094,
        "custom_fields": [
            {
                "key": "order_ref",
                "label": "Order reference",
                "type": "text"
            },
            {
                "key": "branch",
                "label": "Branch",
                "type": "select"
            }
        ],
        "created_at": "2026-09-14T09:14:02+00:00",
        "updated_at": "2026-09-14T09:14:02+00:00"
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}
404No such record for this tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}
422The payload failed validation. `errors` maps each rejected field to its messages.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Validation failed.",
    "errors": {
        "recipient": [
            "Provide recipient or recipients."
        ]
    }
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Contacts

Delete a contact group

DELETE/api/v3/contact-groups/{group}

Deleting a group deletes every contact in it. A group that still holds people is refused with 409 and the count, unless you repeat the call with force=true — there is no undo, and the contacts do not move anywhere.

AuthenticationTenant API token

Required permission: contacts.delete

Path parameters

groupstringrequired
The group, by numeric id or UUID.

Example: 42

Query parameters

forcebooleanoptional
Delete the group even though it still holds contacts, taking them with it.
default
false

Example: 1

Responses

200What was deleted.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
What was deleted.
Show child properties
idintegeroptional
The group that was deleted.
uidstringoptional
Its UUID.
deletedbooleanoptional
Always true on a 200 here.
contacts_deletedintegeroptional
How many contacts went with it.
{
    "status": "success",
    "data": {
        "id": 42,
        "uid": "9f3a1c20-5d6e-4a71-9c3b-2f1a0e7b8c44",
        "deleted": true,
        "contacts_deleted": 1180
    }
}
409Refused because of the state the data is in; the message says what to do.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This group still holds 1180 contact(s), and deleting it deletes them too. Move them first, or repeat with ?force=true."
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}
404No such record for this tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Contacts

List the contacts in a group

GET/api/v3/contact-groups/{group}/contacts

The people in one group, most recently changed first. search matches the name, the national number, and the full number with punctuation stripped, so a number pasted as +255 712 345 678 finds them.

AuthenticationTenant API token

Required permission: contacts.view

Path parameters

groupstringrequired
The group, by numeric id or UUID.

Example: 42

Query parameters

searchstringoptional
Free text over name and number.

Example: Asha

subscribedbooleanoptional
Only those who do (or do not) still accept messages.

Example: 1

limitintegeroptional
Rows per page, 1–100. Defaults to 20 (25 for catalogue endpoints). Values above 100 are clamped.
minimum
1
maximum
100
default
20

Example: 20

pageintegeroptional
Page number, 1-based. Read `data.pagination.has_more_pages` to know when to stop.
minimum
1
default
1

Example: 1

Responses

200The contacts in this group.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
The contacts in this group.
Show child properties
itemsarray<object>optional
The contacts in this group.
Show child properties
idintegerrequired
Internal numeric id.
uidstringrequired
Public unique identifier.
group_idintegerrequired
Contact group internal id.
group_uidstringrequired
Contact group public uid.
namestringrequired
Contact display name.
country_codestringrequired
Country code (e.g. 255).
phone_numberstringrequired
National number without country code.
full_phone_numberstringrequired
E.164 or full number for sending.
is_subscribedbooleanrequired
Stored subscription flag; not automatically applied by the current SMS campaign dispatch loop.
custom_field_valuesobjectoptional
Every non-reserved field you sent when creating or updating the contact, echoed back. Always an object — `{}` when there are none.
additionalProperties
true
created_atstring | nulloptional
ISO 8601 timestamp of when the contact was created.
updated_atstring | nulloptional
ISO 8601 timestamp of the last change to it.
paginationobjectoptional
Page state.
Show child properties
current_pageintegerrequired
1-based current page index.
per_pageintegerrequired
Number of items per page.
last_pageintegerrequired
1-based index of the last page.
totalintegerrequired
Total number of items across all pages.
has_more_pagesbooleanrequired
True if more pages exist after the current page.
{
    "status": "success",
    "data": {
        "items": [
            {
                "id": 9201,
                "uid": "ctc_7f2ab1c93de04a6b8f10",
                "group_id": 42,
                "group_uid": "9f3a1c20-5d6e-4a71-9c3b-2f1a0e7b8c44",
                "name": "Asha Mwinyi",
                "country_code": "255",
                "phone_number": "712345678",
                "full_phone_number": "255712345678",
                "is_subscribed": true,
                "custom_field_values": {
                    "order_ref": "ORD-1042",
                    "branch": "Mlimani"
                },
                "created_at": "2026-09-14T09:14:02+00:00",
                "updated_at": "2026-09-14T09:14:02+00:00"
            }
        ],
        "pagination": {
            "current_page": 1,
            "per_page": 25,
            "last_page": 1,
            "total": 1,
            "has_more_pages": false
        }
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}
404No such record for this tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}
422The payload failed validation. `errors` maps each rejected field to its messages.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Validation failed.",
    "errors": {
        "recipient": [
            "Provide recipient or recipients."
        ]
    }
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Contacts

Add a contact to a group

POST/api/v3/contact-groups/{group}/contacts

Creates one contact. The REST twin of POST /api/v3/contacts/{group_id}/store, which still works and does the same thing.

AuthenticationTenant API token

Required permission: contacts.create

Path parameters

groupstringrequired
The group, by numeric id or UUID.

Example: 42

Request body

application/json · required

The contact.

PHONEstringoptional
The number. Required on create; on PATCH send it only when the number itself is changing. `phone_number` is an alias and `PHONE` wins if both are sent.
maxLength
64
country_codestringoptional
The country code to split off, e.g. `255`. Send it explicitly: a `+255…` prefix alone is not inferred.
maxLength
8
namestringoptional
The person's name. `NAME`, or `FIRST_NAME` + `LAST_NAME`, are accepted instead. On create, a contact with no name given is named after their number; on PATCH, a name that is not sent is left alone.
maxLength
160
is_subscribedbooleanoptional
Whether they accept messages. Defaults true on create; left alone on PATCH when not sent.
Complete request schema
{
    "type": "object",
    "description": "A contact. Every field that is not one of the reserved names below is stored as a custom field, at the TOP level of the object \u2014 do not wrap them in `custom_field_values`, which would store that wrapper as a key.",
    "additionalProperties": true,
    "properties": {
        "PHONE": {
            "type": "string",
            "description": "The number. Required on create; on PATCH send it only when the number itself is changing. `phone_number` is an alias and `PHONE` wins if both are sent.",
            "maxLength": 64
        },
        "country_code": {
            "type": "string",
            "description": "The country code to split off, e.g. `255`. Send it explicitly: a `+255\u2026` prefix alone is not inferred.",
            "maxLength": 8
        },
        "name": {
            "type": "string",
            "description": "The person's name. `NAME`, or `FIRST_NAME` + `LAST_NAME`, are accepted instead. On create, a contact with no name given is named after their number; on PATCH, a name that is not sent is left alone.",
            "maxLength": 160
        },
        "is_subscribed": {
            "type": "boolean",
            "description": "Whether they accept messages. Defaults true on create; left alone on PATCH when not sent."
        }
    }
}

Responses

201The contact as created.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
The contact as created.
Show child properties
idintegerrequired
Internal numeric id.
uidstringrequired
Public unique identifier.
group_idintegerrequired
Contact group internal id.
group_uidstringrequired
Contact group public uid.
namestringrequired
Contact display name.
country_codestringrequired
Country code (e.g. 255).
phone_numberstringrequired
National number without country code.
full_phone_numberstringrequired
E.164 or full number for sending.
is_subscribedbooleanrequired
Stored subscription flag; not automatically applied by the current SMS campaign dispatch loop.
custom_field_valuesobjectoptional
Every non-reserved field you sent when creating or updating the contact, echoed back. Always an object — `{}` when there are none.
additionalProperties
true
created_atstring | nulloptional
ISO 8601 timestamp of when the contact was created.
updated_atstring | nulloptional
ISO 8601 timestamp of the last change to it.
{
    "status": "success",
    "data": {
        "id": 9201,
        "uid": "ctc_7f2ab1c93de04a6b8f10",
        "group_id": 42,
        "group_uid": "9f3a1c20-5d6e-4a71-9c3b-2f1a0e7b8c44",
        "name": "Asha Mwinyi",
        "country_code": "255",
        "phone_number": "712345678",
        "full_phone_number": "255712345678",
        "is_subscribed": true,
        "custom_field_values": {
            "order_ref": "ORD-1042",
            "branch": "Mlimani"
        },
        "created_at": "2026-09-14T09:14:02+00:00",
        "updated_at": "2026-09-14T09:14:02+00:00"
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}
404No such record for this tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}
422The payload failed validation. `errors` maps each rejected field to its messages.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Validation failed.",
    "errors": {
        "recipient": [
            "Provide recipient or recipients."
        ]
    }
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Contacts

Import many contacts at once

POST/api/v3/contact-groups/{group}/contacts/batch

Up to 500 contacts in one call — the import an integration actually has, instead of one POST per person. A number already in the group is UPDATED (its custom fields merge, exactly as PATCH does, and a row with no name does not rename anybody); a new one is created. skip_existing leaves the ones already there completely untouched.

Every row is judged on its own: a row that fails validation is reported in problems with its index and the rest still apply, and the answer is 207 when any row was rejected, 200 when none was. results names the outcome of every row that landed, so a caller knows what it changed without diffing anything.

AuthenticationTenant API token

Required permission: contacts.import

Path parameters

groupstringrequired
The group, by numeric id or UUID.

Example: 42

Request body

application/json · required

The contacts to import.

contactsarray<object>required
The rows, 1 to 500 of them, each shaped like a single create.
minItems
1
maxItems
500
items.additionalProperties
true
Show child properties
PHONEstringoptional
The number. Required on create; on PATCH send it only when the number itself is changing. `phone_number` is an alias and `PHONE` wins if both are sent.
maxLength
64
country_codestringoptional
The country code to split off, e.g. `255`. Send it explicitly: a `+255…` prefix alone is not inferred.
maxLength
8
namestringoptional
The person's name. `NAME`, or `FIRST_NAME` + `LAST_NAME`, are accepted instead. On create, a contact with no name given is named after their number; on PATCH, a name that is not sent is left alone.
maxLength
160
is_subscribedbooleanoptional
Whether they accept messages. Defaults true on create; left alone on PATCH when not sent.
skip_existingbooleanoptional
Leave a number that is already in the group exactly as it is, rather than updating it. Defaults false.
Complete request schema
{
    "type": "object",
    "required": [
        "contacts"
    ],
    "properties": {
        "contacts": {
            "type": "array",
            "description": "The rows, 1 to 500 of them, each shaped like a single create.",
            "items": {
                "$ref": "#/components/schemas/ContactWriteRequest"
            },
            "minItems": 1,
            "maxItems": 500
        },
        "skip_existing": {
            "type": "boolean",
            "description": "Leave a number that is already in the group exactly as it is, rather than updating it. Defaults false."
        }
    }
}

Responses

200Every row applied.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
Every row applied.
Show child properties
group_idintegeroptional
The group they went into.
receivedintegeroptional
How many rows you sent.
createdintegeroptional
How many were new.
updatedintegeroptional
How many matched a number already in the group.
skippedintegeroptional
How many were left alone because of `skip_existing`.
rejectedintegeroptional
How many failed validation and did nothing.
resultsarray<object>optional
One entry per row that landed.
Show child properties
indexintegeroptional
The row's position in the `contacts` array you sent.
uidstringoptional
The contact it created or matched.
outcomestringoptional
`created`, `updated`, or `skipped` when `skip_existing` was set and the number was already there.
enum
["created","updated","skipped"]
problemsarray<object>optional
One entry per rejected row.
Show child properties
indexintegeroptional
Its position in the array you sent.
errorsobjectoptional
Field-by-field validation messages.
additionalProperties
{"type":"array","description":"The messages for that field.","items":{"type":"string","description":"One message."}}
{
    "status": "success",
    "data": {
        "group_id": 42,
        "received": 2,
        "created": 1,
        "updated": 1,
        "skipped": 0,
        "rejected": 0,
        "results": [
            {
                "index": 0,
                "uid": "ctc_7f2ab1c93de04a6b8f10",
                "outcome": "updated"
            },
            {
                "index": 1,
                "uid": "ctc_88c1de40ab7392f5c001",
                "outcome": "created"
            }
        ],
        "problems": []
    }
}
207Some rows were rejected; the rest applied.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
Some rows were rejected; the rest applied.
Show child properties
receivedintegeroptional
How many rows you sent.
createdintegeroptional
How many were new.
updatedintegeroptional
How many matched an existing number.
skippedintegeroptional
How many `skip_existing` left alone.
rejectedintegeroptional
How many failed validation.
resultsarray<object>optional
The rows that landed.
Show child properties
indexintegeroptional
The row's position in the `contacts` array you sent.
uidstringoptional
The contact it created or matched.
outcomestringoptional
`created`, `updated`, or `skipped` when `skip_existing` was set and the number was already there.
enum
["created","updated","skipped"]
problemsarray<object>optional
The rows that did not, with why.
items.additionalProperties
true
Show child properties
indexintegeroptional
Its position in the array you sent.
errorsobjectoptional
Field-by-field validation messages.
additionalProperties
true
{
    "status": "success",
    "data": {
        "received": 3,
        "created": 1,
        "updated": 1,
        "skipped": 0,
        "rejected": 1,
        "results": [
            {
                "index": 0,
                "uid": "ctc_7f2ab1c93de04a6b8f10",
                "outcome": "updated"
            }
        ],
        "problems": [
            {
                "index": 2,
                "errors": {
                    "PHONE": [
                        "The PHONE field is required."
                    ]
                }
            }
        ]
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}
404No such record for this tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}
422The payload failed validation. `errors` maps each rejected field to its messages.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Validation failed.",
    "errors": {
        "recipient": [
            "Provide recipient or recipients."
        ]
    }
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Contacts

Read a contact

GET/api/v3/contact-groups/{group}/contacts/{contact}

One contact. The REST twin of POST /api/v3/contacts/{group_id}/search/{uid}.

AuthenticationTenant API token

Required permission: contacts.view

Path parameters

groupstringrequired
The group, by numeric id or UUID.

Example: 42

contactstringrequired
The contact, by its UID or numeric id.

Example: ctc_7f2ab1c93de04a6b8f10

Responses

200The contact.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
The contact.
Show child properties
idintegerrequired
Internal numeric id.
uidstringrequired
Public unique identifier.
group_idintegerrequired
Contact group internal id.
group_uidstringrequired
Contact group public uid.
namestringrequired
Contact display name.
country_codestringrequired
Country code (e.g. 255).
phone_numberstringrequired
National number without country code.
full_phone_numberstringrequired
E.164 or full number for sending.
is_subscribedbooleanrequired
Stored subscription flag; not automatically applied by the current SMS campaign dispatch loop.
custom_field_valuesobjectoptional
Every non-reserved field you sent when creating or updating the contact, echoed back. Always an object — `{}` when there are none.
additionalProperties
true
created_atstring | nulloptional
ISO 8601 timestamp of when the contact was created.
updated_atstring | nulloptional
ISO 8601 timestamp of the last change to it.
{
    "status": "success",
    "data": {
        "id": 9201,
        "uid": "ctc_7f2ab1c93de04a6b8f10",
        "group_id": 42,
        "group_uid": "9f3a1c20-5d6e-4a71-9c3b-2f1a0e7b8c44",
        "name": "Asha Mwinyi",
        "country_code": "255",
        "phone_number": "712345678",
        "full_phone_number": "255712345678",
        "is_subscribed": true,
        "custom_field_values": {
            "order_ref": "ORD-1042",
            "branch": "Mlimani"
        },
        "created_at": "2026-09-14T09:14:02+00:00",
        "updated_at": "2026-09-14T09:14:02+00:00"
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}
404No such record for this tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Contacts

Change a contact

PATCH/api/v3/contact-groups/{group}/contacts/{contact}

Changes exactly what you send and nothing else. Custom fields MERGE into what is stored — send a key as null to remove it — and the name, the number and the subscription each keep their current value when the payload is silent about them. PHONE is not required unless the number itself is changing.

This is a change of behaviour: until 2026-09-14 this endpoint replaced the whole custom-field map and renamed the contact after their phone number whenever name was absent, so a call meant to flip is_subscribed quietly destroyed data. A caller that followed the old advice — read, merge, send everything — gets the same result as before.

AuthenticationTenant API token

Required permission: contacts.edit

Path parameters

groupstringrequired
The group, by numeric id or UUID.

Example: 42

contactstringrequired
The contact, by its UID or numeric id.

Example: ctc_7f2ab1c93de04a6b8f10

Request body

application/json · required

The fields to change.

PHONEstringoptional
The number. Required on create; on PATCH send it only when the number itself is changing. `phone_number` is an alias and `PHONE` wins if both are sent.
maxLength
64
country_codestringoptional
The country code to split off, e.g. `255`. Send it explicitly: a `+255…` prefix alone is not inferred.
maxLength
8
namestringoptional
The person's name. `NAME`, or `FIRST_NAME` + `LAST_NAME`, are accepted instead. On create, a contact with no name given is named after their number; on PATCH, a name that is not sent is left alone.
maxLength
160
is_subscribedbooleanoptional
Whether they accept messages. Defaults true on create; left alone on PATCH when not sent.
Complete request schema
{
    "type": "object",
    "description": "A contact. Every field that is not one of the reserved names below is stored as a custom field, at the TOP level of the object \u2014 do not wrap them in `custom_field_values`, which would store that wrapper as a key.",
    "additionalProperties": true,
    "properties": {
        "PHONE": {
            "type": "string",
            "description": "The number. Required on create; on PATCH send it only when the number itself is changing. `phone_number` is an alias and `PHONE` wins if both are sent.",
            "maxLength": 64
        },
        "country_code": {
            "type": "string",
            "description": "The country code to split off, e.g. `255`. Send it explicitly: a `+255\u2026` prefix alone is not inferred.",
            "maxLength": 8
        },
        "name": {
            "type": "string",
            "description": "The person's name. `NAME`, or `FIRST_NAME` + `LAST_NAME`, are accepted instead. On create, a contact with no name given is named after their number; on PATCH, a name that is not sent is left alone.",
            "maxLength": 160
        },
        "is_subscribed": {
            "type": "boolean",
            "description": "Whether they accept messages. Defaults true on create; left alone on PATCH when not sent."
        }
    }
}

Responses

200The contact as saved.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
The contact as saved.
Show child properties
idintegerrequired
Internal numeric id.
uidstringrequired
Public unique identifier.
group_idintegerrequired
Contact group internal id.
group_uidstringrequired
Contact group public uid.
namestringrequired
Contact display name.
country_codestringrequired
Country code (e.g. 255).
phone_numberstringrequired
National number without country code.
full_phone_numberstringrequired
E.164 or full number for sending.
is_subscribedbooleanrequired
Stored subscription flag; not automatically applied by the current SMS campaign dispatch loop.
custom_field_valuesobjectoptional
Every non-reserved field you sent when creating or updating the contact, echoed back. Always an object — `{}` when there are none.
additionalProperties
true
created_atstring | nulloptional
ISO 8601 timestamp of when the contact was created.
updated_atstring | nulloptional
ISO 8601 timestamp of the last change to it.
{
    "status": "success",
    "data": {
        "id": 9201,
        "uid": "ctc_7f2ab1c93de04a6b8f10",
        "group_id": 42,
        "group_uid": "9f3a1c20-5d6e-4a71-9c3b-2f1a0e7b8c44",
        "name": "Asha Mwinyi",
        "country_code": "255",
        "phone_number": "712345678",
        "full_phone_number": "255712345678",
        "is_subscribed": true,
        "custom_field_values": {
            "order_ref": "ORD-1042",
            "branch": "Mlimani"
        },
        "created_at": "2026-09-14T09:14:02+00:00",
        "updated_at": "2026-09-14T09:14:02+00:00"
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}
404No such record for this tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}
422The payload failed validation. `errors` maps each rejected field to its messages.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Validation failed.",
    "errors": {
        "recipient": [
            "Provide recipient or recipients."
        ]
    }
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Contacts

Delete a contact

DELETE/api/v3/contact-groups/{group}/contacts/{contact}

Removes the contact from the group permanently. The REST twin of DELETE /api/v3/contacts/{group_id}/delete/{uid}.

AuthenticationTenant API token

Required permission: contacts.delete

Path parameters

groupstringrequired
The group, by numeric id or UUID.

Example: 42

contactstringrequired
The contact, by its UID or numeric id.

Example: ctc_7f2ab1c93de04a6b8f10

Responses

200What was deleted.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
What was deleted.
Show child properties
deletedbooleanoptional
Always true on a 200 here.
uidstringoptional
The contact that was deleted.
{
    "status": "success",
    "data": {
        "deleted": true,
        "uid": "ctc_7f2ab1c93de04a6b8f10"
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}
404No such record for this tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Contacts

Find a contact across every group

GET/api/v3/contacts

Every contact in the workspace, whichever group holds them — the answer to "who is this number?" when the caller does not know. q matches the name, the national number and the full number with punctuation stripped. group_id narrows it back to one group.

AuthenticationTenant API token

Required permission: contacts.view

Query parameters

qstringoptional
Free text over name and number. `search` is an alias.

Example: 712345678

group_idstringoptional
Only this group, by numeric id or UUID.

Example: 42

subscribedbooleanoptional
Only those who do (or do not) still accept messages.

Example: 1

limitintegeroptional
Rows per page, 1–100. Defaults to 20 (25 for catalogue endpoints). Values above 100 are clamped.
minimum
1
maximum
100
default
20

Example: 20

pageintegeroptional
Page number, 1-based. Read `data.pagination.has_more_pages` to know when to stop.
minimum
1
default
1

Example: 1

Responses

200The matching contacts, most recently changed first.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
The matching contacts, most recently changed first.
Show child properties
itemsarray<object>optional
The matching contacts, most recently changed first.
Show child properties
idintegerrequired
Internal numeric id.
uidstringrequired
Public unique identifier.
group_idintegerrequired
Contact group internal id.
group_uidstringrequired
Contact group public uid.
namestringrequired
Contact display name.
country_codestringrequired
Country code (e.g. 255).
phone_numberstringrequired
National number without country code.
full_phone_numberstringrequired
E.164 or full number for sending.
is_subscribedbooleanrequired
Stored subscription flag; not automatically applied by the current SMS campaign dispatch loop.
custom_field_valuesobjectoptional
Every non-reserved field you sent when creating or updating the contact, echoed back. Always an object — `{}` when there are none.
additionalProperties
true
created_atstring | nulloptional
ISO 8601 timestamp of when the contact was created.
updated_atstring | nulloptional
ISO 8601 timestamp of the last change to it.
paginationobjectoptional
Page state.
Show child properties
current_pageintegerrequired
1-based current page index.
per_pageintegerrequired
Number of items per page.
last_pageintegerrequired
1-based index of the last page.
totalintegerrequired
Total number of items across all pages.
has_more_pagesbooleanrequired
True if more pages exist after the current page.
{
    "status": "success",
    "data": {
        "items": [
            {
                "id": 9201,
                "uid": "ctc_7f2ab1c93de04a6b8f10",
                "group_id": 42,
                "group_uid": "9f3a1c20-5d6e-4a71-9c3b-2f1a0e7b8c44",
                "name": "Asha Mwinyi",
                "country_code": "255",
                "phone_number": "712345678",
                "full_phone_number": "255712345678",
                "is_subscribed": true,
                "custom_field_values": {
                    "order_ref": "ORD-1042",
                    "branch": "Mlimani"
                },
                "created_at": "2026-09-14T09:14:02+00:00",
                "updated_at": "2026-09-14T09:14:02+00:00"
            }
        ],
        "pagination": {
            "current_page": 1,
            "per_page": 25,
            "last_page": 1,
            "total": 1,
            "has_more_pages": false
        }
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}
404No such record for this tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}
422The payload failed validation. `errors` maps each rejected field to its messages.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Validation failed.",
    "errors": {
        "recipient": [
            "Provide recipient or recipients."
        ]
    }
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Posts

List posts

GET/api/v3/posts

Posts in this workspace: scheduled ones first (soonest first), then newest. status=sent is published or partially published; failed includes posts with a target waiting on a reconnect.

AuthenticationTenant API token

Required permission: posts.view

Query parameters

statusstringoptional
Only posts in this state.
enum
["draft","scheduled","publishing","published","partially_published","failed","cancelled","sent"]

Example: scheduled

platformstringoptional
Only posts with a target on this platform.
enum
["facebook","instagram","tiktok","youtube","linkedin"]

Example: facebook

qstringoptional
Free text over the post body.
maxLength
120

Example: chapati

limitintegeroptional
Rows per page, 1–100. Defaults to 20 (25 for catalogue endpoints). Values above 100 are clamped.
minimum
1
maximum
100
default
20

Example: 20

pageintegeroptional
Page number, 1-based. Read `data.pagination.has_more_pages` to know when to stop.
minimum
1
default
1

Example: 1

Responses

200A page of posts.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
A page of posts.
Show child properties
itemsarray<object>required
The posts, scheduled first then newest.
Show child properties
uidstringrequired
The post id (pub_…).
statusstringrequired
draft, scheduled, publishing, published, partially_published, failed or cancelled.
schedule_statusstringoptional
The scheduler's own state.
scheduled_atstring | nulloptional
When it goes out, ISO-8601.
scheduled_tzstring | nulloptional
Timezone of the schedule.
published_atstring | nulloptional
When it went out, ISO-8601.
created_atstringoptional
When it was created, ISO-8601.
body_excerptstringoptional
The first 120 characters of the text.
thumbnailobject | nulloptional
A thumbnail of the first media item ({url, kind}), or null.
media_countintegeroptional
How many media items are attached.
media_kindstring | nulloptional
image, video or mixed, or null for text only.
platformsarray<string>required
The platforms it goes to.
targets_summaryobjectoptional
Counts of targets by state.
targetsarray<object>optional
Per-target state, abbreviated.
comments_hrefstring | nulloptional
A link into the comments desk for this post, once it has one.
metricsobject | nulloptional
Views, likes, comments, shares, reach from the nightly insights sync.
comment_countintegeroptional
Comments received across targets.
unanswered_countintegeroptional
Comments still needing an answer.
created_byobjectoptional
Who created it: {id, name}.
paginationobjectrequired
Page state.
Show child properties
current_pageintegerrequired
1-based current page index.
per_pageintegerrequired
Number of items per page.
last_pageintegerrequired
1-based index of the last page.
totalintegerrequired
Total number of items across all pages.
has_more_pagesbooleanrequired
True if more pages exist after the current page.
{
    "status": "success",
    "data": {
        "items": [
            {
                "uid": "pub_9k2f3m1x",
                "status": "scheduled",
                "schedule_status": "pending",
                "scheduled_at": "2026-09-20T09:00:00+03:00",
                "scheduled_tz": "Africa/Dar_es_Salaam",
                "published_at": null,
                "created_at": "2026-09-18T10:12:00+03:00",
                "body_excerpt": "Chapati za asubuhi zimeanza! Karibu Duka la Amina.",
                "thumbnail": {
                    "url": "https://business.momo.tz/storage/posts/ast_7h3k2p.jpg",
                    "kind": "image"
                },
                "media_count": 1,
                "media_kind": "image",
                "platforms": [
                    "facebook"
                ],
                "targets_summary": {
                    "pending": 1
                },
                "targets": [
                    {
                        "id": 31,
                        "platform": "facebook",
                        "status": "pending"
                    }
                ],
                "comments_href": null,
                "metrics": null,
                "comment_count": 0,
                "unanswered_count": 0,
                "created_by": {
                    "id": 12,
                    "name": "Amina Juma"
                }
            }
        ],
        "pagination": {
            "current_page": 1,
            "per_page": 20,
            "last_page": 1,
            "total": 1,
            "has_more_pages": false
        }
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}
422The payload failed validation. `errors` maps each rejected field to its messages.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Validation failed.",
    "errors": {
        "recipient": [
            "Provide recipient or recipients."
        ]
    }
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Posts

Create a post

POST/api/v3/posts

Creates a post through the same rules as the composer. schedule.mode decides what happens next: draft (default) saves it; now publishes at once; later schedules it at at. Every target must be a connected account (GET /api/v3/posts/accounts) and, for now/later, able to publish. Per-platform options are validated against that platform (YouTube needs title and made_for_kids, TikTok privacy_level); pre-flight warnings block now/later until listed in accept_warnings. The same idempotency_key again returns the existing post with 200.

AuthenticationTenant API token

Required permission: posts.manage

Request body

application/json · required

The post.

targetsarray<integer>required
Social account ids the post goes to (GET /api/v3/posts/accounts).
minItems
1
maxItems
50
contentobjectrequired
What to post: body, media, first comment.
Show child properties
bodystringrequired
The text.
mediaarray<object>required
Attached media, in order.
Show child properties
assetstringrequired
The media asset uid, from POST /api/v3/posts/media.
alt_textstring | nulloptional
Alt text for the picture, where the platform supports it.
first_commentstring | nulloptional
A comment posted under the post right after it goes up, where the platform allows it.
overridesobjectoptional
Per-account content overrides keyed by social account id: {body, media, first_comment}.
optionsobjectoptional
Per-platform options keyed by platform, validated against that platform's schema (YouTube: title, privacy, made_for_kids…; TikTok: privacy_level…). Omitted keys take the account's saved defaults.
options_overridesobjectoptional
Per-account option overrides keyed by social account id.
scheduleobjectoptional
What to do once saved.
Show child properties
modestringoptional
draft (default) saves only; now publishes at once; later schedules at `at`.
enum
["draft","now","later"]
atstringoptional
When to publish, ISO-8601, for later.
tzstringoptional
IANA timezone for `at`.
accept_warningsarray<string>optional
Pre-flight warning codes the caller accepts, for now/later.
idempotency_keystringoptional
Any string of yours; the same key again returns the same post (200) instead of creating another.
notesstringoptional
An internal note on the post.
Complete request schema
{
    "type": "object",
    "description": "The post to write.",
    "required": [
        "targets",
        "content"
    ],
    "properties": {
        "targets": {
            "type": "array",
            "description": "Social account ids the post goes to (GET /api/v3/posts/accounts).",
            "items": {
                "type": "integer",
                "description": "A social account id."
            },
            "minItems": 1,
            "maxItems": 50
        },
        "content": {
            "$ref": "#/components/schemas/PostContent",
            "description": "What to post: body, media, first comment."
        },
        "overrides": {
            "type": "object",
            "description": "Per-account content overrides keyed by social account id: {body, media, first_comment}."
        },
        "options": {
            "type": "object",
            "description": "Per-platform options keyed by platform, validated against that platform's schema (YouTube: title, privacy, made_for_kids\u2026; TikTok: privacy_level\u2026). Omitted keys take the account's saved defaults."
        },
        "options_overrides": {
            "type": "object",
            "description": "Per-account option overrides keyed by social account id."
        },
        "schedule": {
            "type": "object",
            "description": "What to do once saved.",
            "properties": {
                "mode": {
                    "type": "string",
                    "description": "draft (default) saves only; now publishes at once; later schedules at `at`.",
                    "enum": [
                        "draft",
                        "now",
                        "later"
                    ]
                },
                "at": {
                    "type": "string",
                    "description": "When to publish, ISO-8601, for later."
                },
                "tz": {
                    "type": "string",
                    "description": "IANA timezone for `at`."
                }
            }
        },
        "accept_warnings": {
            "type": "array",
            "description": "Pre-flight warning codes the caller accepts, for now/later.",
            "items": {
                "type": "string",
                "description": "A warning code from verification."
            }
        },
        "idempotency_key": {
            "type": "string",
            "description": "Any string of yours; the same key again returns the same post (200) instead of creating another."
        },
        "notes": {
            "type": "string",
            "description": "An internal note on the post."
        }
    }
}
Example
{
    "targets": [
        4,
        7
    ],
    "content": {
        "body": "Chapati za asubuhi zimeanza! Karibu Duka la Amina.",
        "media": [
            {
                "asset": "ast_7h3k2p",
                "alt_text": "Chapati on a plate"
            }
        ],
        "first_comment": "#chapati #dar"
    },
    "options": {
        "tiktok": {
            "privacy_level": "PUBLIC_TO_EVERYONE"
        }
    },
    "schedule": {
        "mode": "later",
        "at": "2026-09-20T09:00:00",
        "tz": "Africa/Dar_es_Salaam"
    },
    "idempotency_key": "order-42"
}

Responses

201The post was created.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
The post was created.
Show child properties
postobjectrequired
The post in full.
Show child properties
uidstringrequired
The post id (pub_…).
statusstringrequired
draft, scheduled, publishing, published, partially_published, failed or cancelled.
enum
["draft","scheduled","publishing","published","partially_published","failed","cancelled"]
schedule_statusstringoptional
The scheduler's own state: none, pending, claimed, dispatched, processed.
scheduled_atstring | nulloptional
When it goes (or went) out, ISO-8601.
scheduled_tzstring | nulloptional
The timezone the schedule was given in.
published_atstring | nulloptional
When the first target went up.
completed_atstring | nulloptional
When every target settled.
created_atstringoptional
When the post was created here, ISO-8601.
updated_atstringoptional
Last change, ISO-8601.
created_byobject | nulloptional
Who created it: {id, name}.
approved_byobject | nulloptional
Who approved sending it: {id, name}.
approved_atstring | nulloptional
When it was approved for sending.
contentobjectrequired
What is posted: body, media, first comment.
Show child properties
bodystringrequired
The text.
mediaarray<object>required
Attached media, in order.
Show child properties
assetstringrequired
The media asset uid, from POST /api/v3/posts/media.
alt_textstring | nulloptional
Alt text for the picture, where the platform supports it.
first_commentstring | nulloptional
A comment posted under the post right after it goes up, where the platform allows it.
overridesobjectoptional
Per-account content overrides, keyed by social account id.
optionsobjectoptional
Per-platform options, keyed by platform (YouTube title/privacy, TikTok privacy_level…).
options_overridesobjectoptional
Per-account option overrides, keyed by social account id.
targetsarray<object>required
One row per account the post goes to.
Show child properties
idintegerrequired
The target row id, for retry/delete-on-platform in the app.
social_account_idintegerrequired
The connected social account.
platformstringrequired
facebook, instagram, tiktok, youtube or linkedin.
statusstringrequired
pending, publishing, published, failed, needs_reconnect or cancelled.
enum
["pending","publishing","published","failed","needs_reconnect","cancelled"]
provider_post_idstring | nulloptional
The platform's own id for the post once published.
permalinkstring | nulloptional
The public link once published.
error_messagestring | nulloptional
Why this target failed, in the platform's words.
published_atstring | nulloptional
When it went up on this platform, ISO-8601.
mediaarray<object>optional
The attached assets as stored (uid, kind, url, status, probe).
verificationobjectoptional
The pre-flight: errors that block sending and warnings a person may accept by code.
notesstring | nulloptional
Internal notes.
idempotency_keystring | nulloptional
The key given on create, if any.
approvalobject | nulloptional
A pending approval hand-off (id, state, mode, at, requested_by, requested_at, expires_at, href), or null.
noticestring | nulloptional
Anything the scheduler wants you to know.
existingbooleanoptional
False on a fresh create.
{
    "status": "success",
    "data": {
        "post": {
            "uid": "pub_9k2f3m1x",
            "status": "scheduled",
            "schedule_status": "pending",
            "scheduled_at": "2026-09-20T09:00:00+03:00",
            "scheduled_tz": "Africa/Dar_es_Salaam",
            "published_at": null,
            "completed_at": null,
            "created_at": "2026-09-18T10:12:00+03:00",
            "updated_at": "2026-09-18T10:15:00+03:00",
            "created_by": {
                "id": 12,
                "name": "Amina Juma"
            },
            "approved_by": {
                "id": 12,
                "name": "Amina Juma"
            },
            "approved_at": "2026-09-18T10:15:00+03:00",
            "content": {
                "body": "Chapati za asubuhi zimeanza! Karibu Duka la Amina.",
                "media": [
                    {
                        "asset": "ast_7h3k2p",
                        "alt_text": "Chapati on a plate"
                    }
                ],
                "first_comment": "#chapati #dar"
            },
            "overrides": [],
            "options": {
                "tiktok": {
                    "privacy_level": "PUBLIC_TO_EVERYONE"
                }
            },
            "options_overrides": [],
            "targets": [
                {
                    "id": 31,
                    "social_account_id": 4,
                    "platform": "facebook",
                    "status": "pending",
                    "provider_post_id": null,
                    "permalink": null,
                    "error_message": null,
                    "published_at": null
                }
            ],
            "media": [
                {
                    "uid": "ast_7h3k2p",
                    "kind": "image",
                    "url": "https://business.momo.tz/storage/posts/ast_7h3k2p.jpg",
                    "status": "ready"
                }
            ],
            "verification": {
                "errors": [],
                "warnings": []
            },
            "notes": null,
            "idempotency_key": "order-42",
            "approval": null
        },
        "notice": null,
        "existing": false
    }
}
200The idempotency key matched an existing post; it is returned unchanged.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
The idempotency key matched an existing post; it is returned unchanged.
Show child properties
postobjectrequired
The post in full.
Show child properties
uidstringrequired
The post id (pub_…).
statusstringrequired
draft, scheduled, publishing, published, partially_published, failed or cancelled.
enum
["draft","scheduled","publishing","published","partially_published","failed","cancelled"]
schedule_statusstringoptional
The scheduler's own state: none, pending, claimed, dispatched, processed.
scheduled_atstring | nulloptional
When it goes (or went) out, ISO-8601.
scheduled_tzstring | nulloptional
The timezone the schedule was given in.
published_atstring | nulloptional
When the first target went up.
completed_atstring | nulloptional
When every target settled.
created_atstringoptional
When the post was created here, ISO-8601.
updated_atstringoptional
Last change, ISO-8601.
created_byobject | nulloptional
Who created it: {id, name}.
approved_byobject | nulloptional
Who approved sending it: {id, name}.
approved_atstring | nulloptional
When it was approved for sending.
contentobjectrequired
What is posted: body, media, first comment.
Show child properties
bodystringrequired
The text.
mediaarray<object>required
Attached media, in order.
Show child properties
assetstringrequired
The media asset uid, from POST /api/v3/posts/media.
alt_textstring | nulloptional
Alt text for the picture, where the platform supports it.
first_commentstring | nulloptional
A comment posted under the post right after it goes up, where the platform allows it.
overridesobjectoptional
Per-account content overrides, keyed by social account id.
optionsobjectoptional
Per-platform options, keyed by platform (YouTube title/privacy, TikTok privacy_level…).
options_overridesobjectoptional
Per-account option overrides, keyed by social account id.
targetsarray<object>required
One row per account the post goes to.
Show child properties
idintegerrequired
The target row id, for retry/delete-on-platform in the app.
social_account_idintegerrequired
The connected social account.
platformstringrequired
facebook, instagram, tiktok, youtube or linkedin.
statusstringrequired
pending, publishing, published, failed, needs_reconnect or cancelled.
enum
["pending","publishing","published","failed","needs_reconnect","cancelled"]
provider_post_idstring | nulloptional
The platform's own id for the post once published.
permalinkstring | nulloptional
The public link once published.
error_messagestring | nulloptional
Why this target failed, in the platform's words.
published_atstring | nulloptional
When it went up on this platform, ISO-8601.
mediaarray<object>optional
The attached assets as stored (uid, kind, url, status, probe).
verificationobjectoptional
The pre-flight: errors that block sending and warnings a person may accept by code.
notesstring | nulloptional
Internal notes.
idempotency_keystring | nulloptional
The key given on create, if any.
approvalobject | nulloptional
A pending approval hand-off (id, state, mode, at, requested_by, requested_at, expires_at, href), or null.
noticestring | nulloptional
Always null here.
existingbooleanoptional
True.
{
    "status": "success",
    "data": {
        "post": {
            "uid": "pub_9k2f3m1x",
            "status": "scheduled",
            "schedule_status": "pending",
            "scheduled_at": "2026-09-20T09:00:00+03:00",
            "scheduled_tz": "Africa/Dar_es_Salaam",
            "published_at": null,
            "completed_at": null,
            "created_at": "2026-09-18T10:12:00+03:00",
            "updated_at": "2026-09-18T10:15:00+03:00",
            "created_by": {
                "id": 12,
                "name": "Amina Juma"
            },
            "approved_by": {
                "id": 12,
                "name": "Amina Juma"
            },
            "approved_at": "2026-09-18T10:15:00+03:00",
            "content": {
                "body": "Chapati za asubuhi zimeanza! Karibu Duka la Amina.",
                "media": [
                    {
                        "asset": "ast_7h3k2p",
                        "alt_text": "Chapati on a plate"
                    }
                ],
                "first_comment": "#chapati #dar"
            },
            "overrides": [],
            "options": {
                "tiktok": {
                    "privacy_level": "PUBLIC_TO_EVERYONE"
                }
            },
            "options_overrides": [],
            "targets": [
                {
                    "id": 31,
                    "social_account_id": 4,
                    "platform": "facebook",
                    "status": "pending",
                    "provider_post_id": null,
                    "permalink": null,
                    "error_message": null,
                    "published_at": null
                }
            ],
            "media": [
                {
                    "uid": "ast_7h3k2p",
                    "kind": "image",
                    "url": "https://business.momo.tz/storage/posts/ast_7h3k2p.jpg",
                    "status": "ready"
                }
            ],
            "verification": {
                "errors": [],
                "warnings": []
            },
            "notes": null,
            "idempotency_key": "order-42",
            "approval": null
        },
        "notice": null,
        "existing": true
    }
}
409The pre-flight found errors, or a warning was not accepted. `verification` lists them by target.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This post has warnings to accept before it can be scheduled.",
    "verification": {
        "warnings": [
            {
                "code": "caption_truncated",
                "target": 4
            }
        ]
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}
422The payload failed validation. `errors` maps each rejected field to its messages.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Validation failed.",
    "errors": {
        "recipient": [
            "Provide recipient or recipients."
        ]
    }
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Posts

List publish targets

GET/api/v3/posts/accounts

The connected accounts a post can go to, with each one's publishing state and why it cannot publish when it cannot. Bounded by what the key's owner is assigned to.

AuthenticationTenant API token

Required permission: posts.view

Responses

200The accounts.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
The accounts.
Show child properties
itemsarray<object>required
Every account this key may post to.
Show child properties
idintegerrequired
The social account id to pass as a target.
platformstringrequired
facebook, instagram, tiktok, youtube or linkedin.
namestringrequired
The account's name.
handlestring | nulloptional
Its @handle, when it has one.
avatarstring | nulloptional
Profile picture URL.
publish_enabledbooleanrequired
Whether the account holds the publishing permission.
needs_reconnectbooleanoptional
Whether the login has expired or been revoked.
needs_reconnect_reasonstring | nulloptional
Why, when it does.
reason_textstring | nulloptional
One sentence on why it cannot publish, when it cannot.
publish_tierstringrequired
ready, needs_permission, waiting (platform review pending) or blocked.
publish_statestringoptional
The raw grant state: on, available, missing, gated, off, not_applicable.
publish_gatestring | nulloptional
What the workspace is waiting on, when gated.
publish_defaultsobjectoptional
The options this account fills in by itself on a new post.
insights_statusstring | nulloptional
What the nightly metrics sync found: ok, scope_missing, unavailable, error.
author_kindstring | nulloptional
LinkedIn only: person or organization.
token_expires_atstring | nulloptional
When the login expires, if it does.
healthobjectoptional
What the last health probe recorded.
{
    "status": "success",
    "data": {
        "items": [
            {
                "id": 4,
                "platform": "facebook",
                "name": "Duka la Amina",
                "handle": null,
                "avatar": null,
                "publish_enabled": true,
                "needs_reconnect": false,
                "needs_reconnect_reason": null,
                "reason_text": null,
                "publish_tier": "ready",
                "publish_state": "on",
                "publish_gate": null,
                "publish_defaults": [],
                "insights_status": "ok",
                "author_kind": null,
                "token_expires_at": null,
                "health": {
                    "checked_at": "2026-09-18T04:05:00+03:00",
                    "quota": null
                }
            }
        ]
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Posts

Upload media for a post

POST/api/v3/posts/media

Send a multipart file (JPEG, PNG, WebP, HEIC, GIF, MP4, MOV, WebM) or an https url to fetch. The asset comes back processing; poll GET /api/v3/posts/media/{uid} until it is ready, then put its uid in content.media. Pictures over 1920px on the long side are resized; videos are probed for duration, aspect and audio.

AuthenticationTenant API token

Required permission: posts.manage

Request body

application/json · required

A file or a URL.

urlstringrequired
An https URL of a picture or video to fetch.
Complete request schema
{
    "type": "object",
    "description": "A URL to fetch.",
    "required": [
        "url"
    ],
    "properties": {
        "url": {
            "type": "string",
            "description": "An https URL of a picture or video to fetch."
        }
    }
}
From a URL
{
    "url": "https://cdn.example.co.tz/chapati.jpg"
}

Responses

201The asset, processing.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
The asset, processing.
Show child properties
mediaobjectrequired
An uploaded media asset.
Show child properties
uidstringrequired
The asset uid to put in content.media.
kindstringrequired
image or video.
mime_typestringoptional
The served MIME type.
original_namestring | nulloptional
The filename as uploaded.
sizeintegeroptional
Bytes.
statusstringrequired
processing until probed and converted, then ready; failed when the file could not be used.
urlstring | nulloptional
Where the served copy lives.
thumbnail_urlstring | nulloptional
A poster frame for a video.
probeobject | nulloptional
width, height, duration_ms, aspect, has_audio once known.
warningsarray<string>optional
Anything the pipeline noticed (a resize, a re-encode).
{
    "status": "success",
    "data": {
        "media": {
            "uid": "ast_7h3k2p",
            "kind": "image",
            "mime_type": "image/jpeg",
            "original_name": "chapati.jpg",
            "size": 412000,
            "status": "processing",
            "url": "https://business.momo.tz/storage/posts/ast_7h3k2p.jpg",
            "thumbnail_url": null,
            "probe": null,
            "warnings": []
        }
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}
422The payload failed validation. `errors` maps each rejected field to its messages.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Validation failed.",
    "errors": {
        "recipient": [
            "Provide recipient or recipients."
        ]
    }
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Posts

Read a media asset

GET/api/v3/posts/media/{uid}

One uploaded asset and its state; ready means it can go on a post.

AuthenticationTenant API token

Required permission: posts.view

Path parameters

uidstringrequired
The asset uid (ast_…).

Example: ast_7h3k2p

Responses

200The asset.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
The asset.
Show child properties
mediaobjectrequired
An uploaded media asset.
Show child properties
uidstringrequired
The asset uid to put in content.media.
kindstringrequired
image or video.
mime_typestringoptional
The served MIME type.
original_namestring | nulloptional
The filename as uploaded.
sizeintegeroptional
Bytes.
statusstringrequired
processing until probed and converted, then ready; failed when the file could not be used.
urlstring | nulloptional
Where the served copy lives.
thumbnail_urlstring | nulloptional
A poster frame for a video.
probeobject | nulloptional
width, height, duration_ms, aspect, has_audio once known.
warningsarray<string>optional
Anything the pipeline noticed (a resize, a re-encode).
{
    "status": "success",
    "data": {
        "media": {
            "uid": "ast_7h3k2p",
            "kind": "image",
            "mime_type": "image/jpeg",
            "original_name": "chapati.jpg",
            "size": 412000,
            "status": "ready",
            "url": "https://business.momo.tz/storage/posts/ast_7h3k2p.jpg",
            "thumbnail_url": null,
            "probe": {
                "width": 1080,
                "height": 1350,
                "duration_ms": null,
                "aspect": 0.8,
                "has_audio": null
            },
            "warnings": []
        }
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}
404No such record for this tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Posts

Read a post

GET/api/v3/posts/{uid}

One post in full: content, per-platform options, every target's state and public link, the pre-flight report, and any pending approval.

AuthenticationTenant API token

Required permission: posts.view

Path parameters

uidstringrequired
The post id (pub_…).

Example: pub_9k2f3m1x

Responses

200The post.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
The post.
Show child properties
uidstringrequired
The post id (pub_…).
statusstringrequired
draft, scheduled, publishing, published, partially_published, failed or cancelled.
enum
["draft","scheduled","publishing","published","partially_published","failed","cancelled"]
schedule_statusstringoptional
The scheduler's own state: none, pending, claimed, dispatched, processed.
scheduled_atstring | nulloptional
When it goes (or went) out, ISO-8601.
scheduled_tzstring | nulloptional
The timezone the schedule was given in.
published_atstring | nulloptional
When the first target went up.
completed_atstring | nulloptional
When every target settled.
created_atstringoptional
When the post was created here, ISO-8601.
updated_atstringoptional
Last change, ISO-8601.
created_byobject | nulloptional
Who created it: {id, name}.
approved_byobject | nulloptional
Who approved sending it: {id, name}.
approved_atstring | nulloptional
When it was approved for sending.
contentobjectrequired
What is posted: body, media, first comment.
Show child properties
bodystringrequired
The text.
mediaarray<object>required
Attached media, in order.
Show child properties
assetstringrequired
The media asset uid, from POST /api/v3/posts/media.
alt_textstring | nulloptional
Alt text for the picture, where the platform supports it.
first_commentstring | nulloptional
A comment posted under the post right after it goes up, where the platform allows it.
overridesobjectoptional
Per-account content overrides, keyed by social account id.
optionsobjectoptional
Per-platform options, keyed by platform (YouTube title/privacy, TikTok privacy_level…).
options_overridesobjectoptional
Per-account option overrides, keyed by social account id.
targetsarray<object>required
One row per account the post goes to.
Show child properties
idintegerrequired
The target row id, for retry/delete-on-platform in the app.
social_account_idintegerrequired
The connected social account.
platformstringrequired
facebook, instagram, tiktok, youtube or linkedin.
statusstringrequired
pending, publishing, published, failed, needs_reconnect or cancelled.
enum
["pending","publishing","published","failed","needs_reconnect","cancelled"]
provider_post_idstring | nulloptional
The platform's own id for the post once published.
permalinkstring | nulloptional
The public link once published.
error_messagestring | nulloptional
Why this target failed, in the platform's words.
published_atstring | nulloptional
When it went up on this platform, ISO-8601.
mediaarray<object>optional
The attached assets as stored (uid, kind, url, status, probe).
verificationobjectoptional
The pre-flight: errors that block sending and warnings a person may accept by code.
notesstring | nulloptional
Internal notes.
idempotency_keystring | nulloptional
The key given on create, if any.
approvalobject | nulloptional
A pending approval hand-off (id, state, mode, at, requested_by, requested_at, expires_at, href), or null.
{
    "status": "success",
    "data": {
        "uid": "pub_9k2f3m1x",
        "status": "scheduled",
        "schedule_status": "pending",
        "scheduled_at": "2026-09-20T09:00:00+03:00",
        "scheduled_tz": "Africa/Dar_es_Salaam",
        "published_at": null,
        "completed_at": null,
        "created_at": "2026-09-18T10:12:00+03:00",
        "updated_at": "2026-09-18T10:15:00+03:00",
        "created_by": {
            "id": 12,
            "name": "Amina Juma"
        },
        "approved_by": {
            "id": 12,
            "name": "Amina Juma"
        },
        "approved_at": "2026-09-18T10:15:00+03:00",
        "content": {
            "body": "Chapati za asubuhi zimeanza! Karibu Duka la Amina.",
            "media": [
                {
                    "asset": "ast_7h3k2p",
                    "alt_text": "Chapati on a plate"
                }
            ],
            "first_comment": "#chapati #dar"
        },
        "overrides": [],
        "options": {
            "tiktok": {
                "privacy_level": "PUBLIC_TO_EVERYONE"
            }
        },
        "options_overrides": [],
        "targets": [
            {
                "id": 31,
                "social_account_id": 4,
                "platform": "facebook",
                "status": "pending",
                "provider_post_id": null,
                "permalink": null,
                "error_message": null,
                "published_at": null
            }
        ],
        "media": [
            {
                "uid": "ast_7h3k2p",
                "kind": "image",
                "url": "https://business.momo.tz/storage/posts/ast_7h3k2p.jpg",
                "status": "ready"
            }
        ],
        "verification": {
            "errors": [],
            "warnings": []
        },
        "notes": null,
        "idempotency_key": "order-42",
        "approval": null
    }
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}
404No such record for this tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Posts

Replace a post's content

PATCH/api/v3/posts/{uid}

Replaces the content, targets and options of a draft or a pending scheduled post — the same body as create. A post already publishing answers 409. Editing an approved post withdraws the approval; schedule.mode re-records it.

AuthenticationTenant API token

Required permission: posts.manage

Path parameters

uidstringrequired
The post id (pub_…).

Example: pub_9k2f3m1x

Request body

application/json · required

The post.

targetsarray<integer>required
Social account ids the post goes to (GET /api/v3/posts/accounts).
minItems
1
maxItems
50
contentobjectrequired
What to post: body, media, first comment.
Show child properties
bodystringrequired
The text.
mediaarray<object>required
Attached media, in order.
Show child properties
assetstringrequired
The media asset uid, from POST /api/v3/posts/media.
alt_textstring | nulloptional
Alt text for the picture, where the platform supports it.
first_commentstring | nulloptional
A comment posted under the post right after it goes up, where the platform allows it.
overridesobjectoptional
Per-account content overrides keyed by social account id: {body, media, first_comment}.
optionsobjectoptional
Per-platform options keyed by platform, validated against that platform's schema (YouTube: title, privacy, made_for_kids…; TikTok: privacy_level…). Omitted keys take the account's saved defaults.
options_overridesobjectoptional
Per-account option overrides keyed by social account id.
scheduleobjectoptional
What to do once saved.
Show child properties
modestringoptional
draft (default) saves only; now publishes at once; later schedules at `at`.
enum
["draft","now","later"]
atstringoptional
When to publish, ISO-8601, for later.
tzstringoptional
IANA timezone for `at`.
accept_warningsarray<string>optional
Pre-flight warning codes the caller accepts, for now/later.
idempotency_keystringoptional
Any string of yours; the same key again returns the same post (200) instead of creating another.
notesstringoptional
An internal note on the post.
Complete request schema
{
    "type": "object",
    "description": "The post to write.",
    "required": [
        "targets",
        "content"
    ],
    "properties": {
        "targets": {
            "type": "array",
            "description": "Social account ids the post goes to (GET /api/v3/posts/accounts).",
            "items": {
                "type": "integer",
                "description": "A social account id."
            },
            "minItems": 1,
            "maxItems": 50
        },
        "content": {
            "$ref": "#/components/schemas/PostContent",
            "description": "What to post: body, media, first comment."
        },
        "overrides": {
            "type": "object",
            "description": "Per-account content overrides keyed by social account id: {body, media, first_comment}."
        },
        "options": {
            "type": "object",
            "description": "Per-platform options keyed by platform, validated against that platform's schema (YouTube: title, privacy, made_for_kids\u2026; TikTok: privacy_level\u2026). Omitted keys take the account's saved defaults."
        },
        "options_overrides": {
            "type": "object",
            "description": "Per-account option overrides keyed by social account id."
        },
        "schedule": {
            "type": "object",
            "description": "What to do once saved.",
            "properties": {
                "mode": {
                    "type": "string",
                    "description": "draft (default) saves only; now publishes at once; later schedules at `at`.",
                    "enum": [
                        "draft",
                        "now",
                        "later"
                    ]
                },
                "at": {
                    "type": "string",
                    "description": "When to publish, ISO-8601, for later."
                },
                "tz": {
                    "type": "string",
                    "description": "IANA timezone for `at`."
                }
            }
        },
        "accept_warnings": {
            "type": "array",
            "description": "Pre-flight warning codes the caller accepts, for now/later.",
            "items": {
                "type": "string",
                "description": "A warning code from verification."
            }
        },
        "idempotency_key": {
            "type": "string",
            "description": "Any string of yours; the same key again returns the same post (200) instead of creating another."
        },
        "notes": {
            "type": "string",
            "description": "An internal note on the post."
        }
    }
}
Example
{
    "targets": [
        4,
        7
    ],
    "content": {
        "body": "Chapati za asubuhi zimeanza! Karibu Duka la Amina.",
        "media": [
            {
                "asset": "ast_7h3k2p",
                "alt_text": "Chapati on a plate"
            }
        ],
        "first_comment": "#chapati #dar"
    },
    "options": {
        "tiktok": {
            "privacy_level": "PUBLIC_TO_EVERYONE"
        }
    },
    "schedule": {
        "mode": "later",
        "at": "2026-09-20T09:00:00",
        "tz": "Africa/Dar_es_Salaam"
    },
    "idempotency_key": "order-42"
}

Responses

200The updated post.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
The updated post.
Show child properties
postobjectrequired
The post in full.
Show child properties
uidstringrequired
The post id (pub_…).
statusstringrequired
draft, scheduled, publishing, published, partially_published, failed or cancelled.
enum
["draft","scheduled","publishing","published","partially_published","failed","cancelled"]
schedule_statusstringoptional
The scheduler's own state: none, pending, claimed, dispatched, processed.
scheduled_atstring | nulloptional
When it goes (or went) out, ISO-8601.
scheduled_tzstring | nulloptional
The timezone the schedule was given in.
published_atstring | nulloptional
When the first target went up.
completed_atstring | nulloptional
When every target settled.
created_atstringoptional
When the post was created here, ISO-8601.
updated_atstringoptional
Last change, ISO-8601.
created_byobject | nulloptional
Who created it: {id, name}.
approved_byobject | nulloptional
Who approved sending it: {id, name}.
approved_atstring | nulloptional
When it was approved for sending.
contentobjectrequired
What is posted: body, media, first comment.
Show child properties
bodystringrequired
The text.
mediaarray<object>required
Attached media, in order.
Show child properties
assetstringrequired
The media asset uid, from POST /api/v3/posts/media.
alt_textstring | nulloptional
Alt text for the picture, where the platform supports it.
first_commentstring | nulloptional
A comment posted under the post right after it goes up, where the platform allows it.
overridesobjectoptional
Per-account content overrides, keyed by social account id.
optionsobjectoptional
Per-platform options, keyed by platform (YouTube title/privacy, TikTok privacy_level…).
options_overridesobjectoptional
Per-account option overrides, keyed by social account id.
targetsarray<object>required
One row per account the post goes to.
Show child properties
idintegerrequired
The target row id, for retry/delete-on-platform in the app.
social_account_idintegerrequired
The connected social account.
platformstringrequired
facebook, instagram, tiktok, youtube or linkedin.
statusstringrequired
pending, publishing, published, failed, needs_reconnect or cancelled.
enum
["pending","publishing","published","failed","needs_reconnect","cancelled"]
provider_post_idstring | nulloptional
The platform's own id for the post once published.
permalinkstring | nulloptional
The public link once published.
error_messagestring | nulloptional
Why this target failed, in the platform's words.
published_atstring | nulloptional
When it went up on this platform, ISO-8601.
mediaarray<object>optional
The attached assets as stored (uid, kind, url, status, probe).
verificationobjectoptional
The pre-flight: errors that block sending and warnings a person may accept by code.
notesstring | nulloptional
Internal notes.
idempotency_keystring | nulloptional
The key given on create, if any.
approvalobject | nulloptional
A pending approval hand-off (id, state, mode, at, requested_by, requested_at, expires_at, href), or null.
noticestring | nulloptional
Anything the scheduler wants you to know.
{
    "status": "success",
    "data": {
        "post": {
            "uid": "pub_9k2f3m1x",
            "status": "scheduled",
            "schedule_status": "pending",
            "scheduled_at": "2026-09-20T09:00:00+03:00",
            "scheduled_tz": "Africa/Dar_es_Salaam",
            "published_at": null,
            "completed_at": null,
            "created_at": "2026-09-18T10:12:00+03:00",
            "updated_at": "2026-09-18T10:15:00+03:00",
            "created_by": {
                "id": 12,
                "name": "Amina Juma"
            },
            "approved_by": {
                "id": 12,
                "name": "Amina Juma"
            },
            "approved_at": "2026-09-18T10:15:00+03:00",
            "content": {
                "body": "Chapati za asubuhi zimeanza! Karibu Duka la Amina.",
                "media": [
                    {
                        "asset": "ast_7h3k2p",
                        "alt_text": "Chapati on a plate"
                    }
                ],
                "first_comment": "#chapati #dar"
            },
            "overrides": [],
            "options": {
                "tiktok": {
                    "privacy_level": "PUBLIC_TO_EVERYONE"
                }
            },
            "options_overrides": [],
            "targets": [
                {
                    "id": 31,
                    "social_account_id": 4,
                    "platform": "facebook",
                    "status": "pending",
                    "provider_post_id": null,
                    "permalink": null,
                    "error_message": null,
                    "published_at": null
                }
            ],
            "media": [
                {
                    "uid": "ast_7h3k2p",
                    "kind": "image",
                    "url": "https://business.momo.tz/storage/posts/ast_7h3k2p.jpg",
                    "status": "ready"
                }
            ],
            "verification": {
                "errors": [],
                "warnings": []
            },
            "notes": null,
            "idempotency_key": "order-42",
            "approval": null
        },
        "notice": null
    }
}
409The post is already publishing, or the pre-flight blocked the requested schedule.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This post is already publishing and can no longer be edited."
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}
404No such record for this tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}
422The payload failed validation. `errors` maps each rejected field to its messages.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Validation failed.",
    "errors": {
        "recipient": [
            "Provide recipient or recipients."
        ]
    }
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Posts

Delete a post

DELETE/api/v3/posts/{uid}

Removes the post here. Anything already published on a platform stays there; a post that is publishing right now answers 409.

AuthenticationTenant API token

Required permission: posts.manage

Path parameters

uidstringrequired
The post id (pub_…).

Example: pub_9k2f3m1x

Responses

200Deleted.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
Deleted.
Show child properties
deletedbooleanrequired
True.
uidstringrequired
The uid that was deleted.
{
    "status": "success",
    "data": {
        "deleted": true,
        "uid": "pub_9k2f3m1x"
    }
}
409The post is publishing right now.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This post is already publishing."
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}
404No such record for this tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Posts

Schedule a post

POST/api/v3/posts/{uid}/schedule

Schedules a draft (or moves a pending scheduled post) to at. Pre-flight warnings must be accepted by code; errors answer 409 with the report.

AuthenticationTenant API token

Required permission: posts.manage

Path parameters

uidstringrequired
The post id (pub_…).

Example: pub_9k2f3m1x

Request body

application/json · required

The time.

atstringrequired
When to publish, ISO-8601.
tzstringoptional
IANA timezone for `at`.
accept_warningsarray<string>optional
Warning codes accepted.
Complete request schema
{
    "type": "object",
    "description": "When.",
    "required": [
        "at"
    ],
    "properties": {
        "at": {
            "type": "string",
            "description": "When to publish, ISO-8601."
        },
        "tz": {
            "type": "string",
            "description": "IANA timezone for `at`."
        },
        "accept_warnings": {
            "type": "array",
            "description": "Warning codes accepted.",
            "items": {
                "type": "string",
                "description": "A warning code."
            }
        }
    }
}
Example
{
    "at": "2026-09-20T09:00:00",
    "tz": "Africa/Dar_es_Salaam",
    "accept_warnings": []
}

Responses

200Scheduled.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
Scheduled.
Show child properties
postobjectrequired
The post in full.
Show child properties
uidstringrequired
The post id (pub_…).
statusstringrequired
draft, scheduled, publishing, published, partially_published, failed or cancelled.
enum
["draft","scheduled","publishing","published","partially_published","failed","cancelled"]
schedule_statusstringoptional
The scheduler's own state: none, pending, claimed, dispatched, processed.
scheduled_atstring | nulloptional
When it goes (or went) out, ISO-8601.
scheduled_tzstring | nulloptional
The timezone the schedule was given in.
published_atstring | nulloptional
When the first target went up.
completed_atstring | nulloptional
When every target settled.
created_atstringoptional
When the post was created here, ISO-8601.
updated_atstringoptional
Last change, ISO-8601.
created_byobject | nulloptional
Who created it: {id, name}.
approved_byobject | nulloptional
Who approved sending it: {id, name}.
approved_atstring | nulloptional
When it was approved for sending.
contentobjectrequired
What is posted: body, media, first comment.
Show child properties
bodystringrequired
The text.
mediaarray<object>required
Attached media, in order.
Show child properties
assetstringrequired
The media asset uid, from POST /api/v3/posts/media.
alt_textstring | nulloptional
Alt text for the picture, where the platform supports it.
first_commentstring | nulloptional
A comment posted under the post right after it goes up, where the platform allows it.
overridesobjectoptional
Per-account content overrides, keyed by social account id.
optionsobjectoptional
Per-platform options, keyed by platform (YouTube title/privacy, TikTok privacy_level…).
options_overridesobjectoptional
Per-account option overrides, keyed by social account id.
targetsarray<object>required
One row per account the post goes to.
Show child properties
idintegerrequired
The target row id, for retry/delete-on-platform in the app.
social_account_idintegerrequired
The connected social account.
platformstringrequired
facebook, instagram, tiktok, youtube or linkedin.
statusstringrequired
pending, publishing, published, failed, needs_reconnect or cancelled.
enum
["pending","publishing","published","failed","needs_reconnect","cancelled"]
provider_post_idstring | nulloptional
The platform's own id for the post once published.
permalinkstring | nulloptional
The public link once published.
error_messagestring | nulloptional
Why this target failed, in the platform's words.
published_atstring | nulloptional
When it went up on this platform, ISO-8601.
mediaarray<object>optional
The attached assets as stored (uid, kind, url, status, probe).
verificationobjectoptional
The pre-flight: errors that block sending and warnings a person may accept by code.
notesstring | nulloptional
Internal notes.
idempotency_keystring | nulloptional
The key given on create, if any.
approvalobject | nulloptional
A pending approval hand-off (id, state, mode, at, requested_by, requested_at, expires_at, href), or null.
noticestring | nulloptional
Anything the scheduler wants you to know.
{
    "status": "success",
    "data": {
        "post": {
            "uid": "pub_9k2f3m1x",
            "status": "scheduled",
            "schedule_status": "pending",
            "scheduled_at": "2026-09-20T09:00:00+03:00",
            "scheduled_tz": "Africa/Dar_es_Salaam",
            "published_at": null,
            "completed_at": null,
            "created_at": "2026-09-18T10:12:00+03:00",
            "updated_at": "2026-09-18T10:15:00+03:00",
            "created_by": {
                "id": 12,
                "name": "Amina Juma"
            },
            "approved_by": {
                "id": 12,
                "name": "Amina Juma"
            },
            "approved_at": "2026-09-18T10:15:00+03:00",
            "content": {
                "body": "Chapati za asubuhi zimeanza! Karibu Duka la Amina.",
                "media": [
                    {
                        "asset": "ast_7h3k2p",
                        "alt_text": "Chapati on a plate"
                    }
                ],
                "first_comment": "#chapati #dar"
            },
            "overrides": [],
            "options": {
                "tiktok": {
                    "privacy_level": "PUBLIC_TO_EVERYONE"
                }
            },
            "options_overrides": [],
            "targets": [
                {
                    "id": 31,
                    "social_account_id": 4,
                    "platform": "facebook",
                    "status": "pending",
                    "provider_post_id": null,
                    "permalink": null,
                    "error_message": null,
                    "published_at": null
                }
            ],
            "media": [
                {
                    "uid": "ast_7h3k2p",
                    "kind": "image",
                    "url": "https://business.momo.tz/storage/posts/ast_7h3k2p.jpg",
                    "status": "ready"
                }
            ],
            "verification": {
                "errors": [],
                "warnings": []
            },
            "notes": null,
            "idempotency_key": "order-42",
            "approval": null
        },
        "notice": null
    }
}
409The pre-flight blocked it, or the post is already publishing.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This post has warnings to accept before it can be scheduled."
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}
404No such record for this tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}
422The payload failed validation. `errors` maps each rejected field to its messages.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Validation failed.",
    "errors": {
        "recipient": [
            "Provide recipient or recipients."
        ]
    }
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Posts

Publish a post now

POST/api/v3/posts/{uid}/publish-now

Queues the post to go out immediately on every target. Same pre-flight rule as scheduling.

AuthenticationTenant API token

Required permission: posts.manage

Path parameters

uidstringrequired
The post id (pub_…).

Example: pub_9k2f3m1x

Request body

application/json

Accepted warnings, if any.

accept_warningsarray<string>optional
Warning codes accepted.
Complete request schema
{
    "type": "object",
    "description": "Accepted warnings.",
    "properties": {
        "accept_warnings": {
            "type": "array",
            "description": "Warning codes accepted.",
            "items": {
                "type": "string",
                "description": "A warning code."
            }
        }
    }
}
Accepting one warning
{
    "accept_warnings": [
        "caption_truncated"
    ]
}

Responses

200Publishing.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
Publishing.
Show child properties
postobjectrequired
The post in full.
Show child properties
uidstringrequired
The post id (pub_…).
statusstringrequired
draft, scheduled, publishing, published, partially_published, failed or cancelled.
enum
["draft","scheduled","publishing","published","partially_published","failed","cancelled"]
schedule_statusstringoptional
The scheduler's own state: none, pending, claimed, dispatched, processed.
scheduled_atstring | nulloptional
When it goes (or went) out, ISO-8601.
scheduled_tzstring | nulloptional
The timezone the schedule was given in.
published_atstring | nulloptional
When the first target went up.
completed_atstring | nulloptional
When every target settled.
created_atstringoptional
When the post was created here, ISO-8601.
updated_atstringoptional
Last change, ISO-8601.
created_byobject | nulloptional
Who created it: {id, name}.
approved_byobject | nulloptional
Who approved sending it: {id, name}.
approved_atstring | nulloptional
When it was approved for sending.
contentobjectrequired
What is posted: body, media, first comment.
Show child properties
bodystringrequired
The text.
mediaarray<object>required
Attached media, in order.
Show child properties
assetstringrequired
The media asset uid, from POST /api/v3/posts/media.
alt_textstring | nulloptional
Alt text for the picture, where the platform supports it.
first_commentstring | nulloptional
A comment posted under the post right after it goes up, where the platform allows it.
overridesobjectoptional
Per-account content overrides, keyed by social account id.
optionsobjectoptional
Per-platform options, keyed by platform (YouTube title/privacy, TikTok privacy_level…).
options_overridesobjectoptional
Per-account option overrides, keyed by social account id.
targetsarray<object>required
One row per account the post goes to.
Show child properties
idintegerrequired
The target row id, for retry/delete-on-platform in the app.
social_account_idintegerrequired
The connected social account.
platformstringrequired
facebook, instagram, tiktok, youtube or linkedin.
statusstringrequired
pending, publishing, published, failed, needs_reconnect or cancelled.
enum
["pending","publishing","published","failed","needs_reconnect","cancelled"]
provider_post_idstring | nulloptional
The platform's own id for the post once published.
permalinkstring | nulloptional
The public link once published.
error_messagestring | nulloptional
Why this target failed, in the platform's words.
published_atstring | nulloptional
When it went up on this platform, ISO-8601.
mediaarray<object>optional
The attached assets as stored (uid, kind, url, status, probe).
verificationobjectoptional
The pre-flight: errors that block sending and warnings a person may accept by code.
notesstring | nulloptional
Internal notes.
idempotency_keystring | nulloptional
The key given on create, if any.
approvalobject | nulloptional
A pending approval hand-off (id, state, mode, at, requested_by, requested_at, expires_at, href), or null.
{
    "status": "success",
    "data": {
        "post": {
            "uid": "pub_9k2f3m1x",
            "status": "scheduled",
            "schedule_status": "pending",
            "scheduled_at": "2026-09-20T09:00:00+03:00",
            "scheduled_tz": "Africa/Dar_es_Salaam",
            "published_at": null,
            "completed_at": null,
            "created_at": "2026-09-18T10:12:00+03:00",
            "updated_at": "2026-09-18T10:15:00+03:00",
            "created_by": {
                "id": 12,
                "name": "Amina Juma"
            },
            "approved_by": {
                "id": 12,
                "name": "Amina Juma"
            },
            "approved_at": "2026-09-18T10:15:00+03:00",
            "content": {
                "body": "Chapati za asubuhi zimeanza! Karibu Duka la Amina.",
                "media": [
                    {
                        "asset": "ast_7h3k2p",
                        "alt_text": "Chapati on a plate"
                    }
                ],
                "first_comment": "#chapati #dar"
            },
            "overrides": [],
            "options": {
                "tiktok": {
                    "privacy_level": "PUBLIC_TO_EVERYONE"
                }
            },
            "options_overrides": [],
            "targets": [
                {
                    "id": 31,
                    "social_account_id": 4,
                    "platform": "facebook",
                    "status": "pending",
                    "provider_post_id": null,
                    "permalink": null,
                    "error_message": null,
                    "published_at": null
                }
            ],
            "media": [
                {
                    "uid": "ast_7h3k2p",
                    "kind": "image",
                    "url": "https://business.momo.tz/storage/posts/ast_7h3k2p.jpg",
                    "status": "ready"
                }
            ],
            "verification": {
                "errors": [],
                "warnings": []
            },
            "notes": null,
            "idempotency_key": "order-42",
            "approval": null
        }
    }
}
409The pre-flight blocked it, or the post is already publishing.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This post is already publishing."
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}
404No such record for this tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}
422The payload failed validation. `errors` maps each rejected field to its messages.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Validation failed.",
    "errors": {
        "recipient": [
            "Provide recipient or recipients."
        ]
    }
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / Posts

Cancel a scheduled post

POST/api/v3/posts/{uid}/cancel

Stops a scheduled post before its time: it is marked cancelled and will not go out. A post already publishing stops only the targets that have not started.

AuthenticationTenant API token

Required permission: posts.manage

Path parameters

uidstringrequired
The post id (pub_…).

Example: pub_9k2f3m1x

Responses

200Cancelled.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
Cancelled.
Show child properties
postobjectrequired
The post in full.
Show child properties
uidstringrequired
The post id (pub_…).
statusstringrequired
draft, scheduled, publishing, published, partially_published, failed or cancelled.
enum
["draft","scheduled","publishing","published","partially_published","failed","cancelled"]
schedule_statusstringoptional
The scheduler's own state: none, pending, claimed, dispatched, processed.
scheduled_atstring | nulloptional
When it goes (or went) out, ISO-8601.
scheduled_tzstring | nulloptional
The timezone the schedule was given in.
published_atstring | nulloptional
When the first target went up.
completed_atstring | nulloptional
When every target settled.
created_atstringoptional
When the post was created here, ISO-8601.
updated_atstringoptional
Last change, ISO-8601.
created_byobject | nulloptional
Who created it: {id, name}.
approved_byobject | nulloptional
Who approved sending it: {id, name}.
approved_atstring | nulloptional
When it was approved for sending.
contentobjectrequired
What is posted: body, media, first comment.
Show child properties
bodystringrequired
The text.
mediaarray<object>required
Attached media, in order.
Show child properties
assetstringrequired
The media asset uid, from POST /api/v3/posts/media.
alt_textstring | nulloptional
Alt text for the picture, where the platform supports it.
first_commentstring | nulloptional
A comment posted under the post right after it goes up, where the platform allows it.
overridesobjectoptional
Per-account content overrides, keyed by social account id.
optionsobjectoptional
Per-platform options, keyed by platform (YouTube title/privacy, TikTok privacy_level…).
options_overridesobjectoptional
Per-account option overrides, keyed by social account id.
targetsarray<object>required
One row per account the post goes to.
Show child properties
idintegerrequired
The target row id, for retry/delete-on-platform in the app.
social_account_idintegerrequired
The connected social account.
platformstringrequired
facebook, instagram, tiktok, youtube or linkedin.
statusstringrequired
pending, publishing, published, failed, needs_reconnect or cancelled.
enum
["pending","publishing","published","failed","needs_reconnect","cancelled"]
provider_post_idstring | nulloptional
The platform's own id for the post once published.
permalinkstring | nulloptional
The public link once published.
error_messagestring | nulloptional
Why this target failed, in the platform's words.
published_atstring | nulloptional
When it went up on this platform, ISO-8601.
mediaarray<object>optional
The attached assets as stored (uid, kind, url, status, probe).
verificationobjectoptional
The pre-flight: errors that block sending and warnings a person may accept by code.
notesstring | nulloptional
Internal notes.
idempotency_keystring | nulloptional
The key given on create, if any.
approvalobject | nulloptional
A pending approval hand-off (id, state, mode, at, requested_by, requested_at, expires_at, href), or null.
{
    "status": "success",
    "data": {
        "post": {
            "uid": "pub_9k2f3m1x",
            "status": "cancelled",
            "schedule_status": "processed",
            "scheduled_at": "2026-09-20T09:00:00+03:00",
            "scheduled_tz": "Africa/Dar_es_Salaam",
            "published_at": null,
            "completed_at": null,
            "created_at": "2026-09-18T10:12:00+03:00",
            "updated_at": "2026-09-18T10:15:00+03:00",
            "created_by": {
                "id": 12,
                "name": "Amina Juma"
            },
            "approved_by": {
                "id": 12,
                "name": "Amina Juma"
            },
            "approved_at": "2026-09-18T10:15:00+03:00",
            "content": {
                "body": "Chapati za asubuhi zimeanza! Karibu Duka la Amina.",
                "media": [
                    {
                        "asset": "ast_7h3k2p",
                        "alt_text": "Chapati on a plate"
                    }
                ],
                "first_comment": "#chapati #dar"
            },
            "overrides": [],
            "options": {
                "tiktok": {
                    "privacy_level": "PUBLIC_TO_EVERYONE"
                }
            },
            "options_overrides": [],
            "targets": [
                {
                    "id": 31,
                    "social_account_id": 4,
                    "platform": "facebook",
                    "status": "pending",
                    "provider_post_id": null,
                    "permalink": null,
                    "error_message": null,
                    "published_at": null
                }
            ],
            "media": [
                {
                    "uid": "ast_7h3k2p",
                    "kind": "image",
                    "url": "https://business.momo.tz/storage/posts/ast_7h3k2p.jpg",
                    "status": "ready"
                }
            ],
            "verification": {
                "errors": [],
                "warnings": []
            },
            "notes": null,
            "idempotency_key": "order-42",
            "approval": null
        }
    }
}
409Nothing to cancel: the post is not scheduled.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This post is not scheduled."
}
401Missing, unknown or expired bearer token.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Invalid API token."
}
403The record exists but belongs to another tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "This catalogue belongs to another tenant."
}
404No such record for this tenant.
statusstringrequired
Always "error" for failure responses.
enum
["error"]
messagestringrequired
Human-readable error message.
errorsobjectoptional
Optional field-level validation errors; keys are field names, values are arrays of messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "status": "error",
    "message": "Resource not found."
}
429More than 120 requests in a minute on this token. Wait for `Retry-After` seconds and retry.

Response headers

Retry-Afterintegeroptional
Seconds to wait before retrying.

Example: 42

X-RateLimit-Limitintegeroptional
Requests allowed per minute.

Example: 120

X-RateLimit-Remainingintegeroptional
Requests left in the current window.

Example: 0

X-RateLimit-Resetintegeroptional
Unix timestamp when the window resets.

Example: 1789012345

statusstringrequired
Always "error".
enum
["error"]
messagestringrequired
Human-readable rate limit message.
{
    "status": "error",
    "message": "Too many requests. Retry after the number of seconds in the Retry-After header."
}

API REFERENCE / MCP

Call the account MCP server

POST/mcp

The bare /mcp root, serving the account server — the cross-domain starting point, and what a person types when a client asks for a URL.

It exists because without it the whole OAuth handshake succeeds — discovery, consent, a real access token — and then the first tools/call 404s, which is the least debuggable failure there is.

Everything below applies equally to POST /mcp/v1/{server}; connect a specific server there when you know which part of the business you want, because most clients fold the entire tool list into their context and connecting everything makes an assistant worse at choosing.

AuthenticationMCP connection token or OAuth access token

Request body

application/json · required

A JSON-RPC 2.0 request. `Accept` must allow both `application/json` and `text/event-stream`.

jsonrpcstringrequired
Always the string "2.0".
const
2.0
idstring | integeroptional
Correlates the response with this request. Omit it to send a notification, which is acknowledged rather than answered.
methodstringrequired
The JSON-RPC method. `initialize` opens the session, `tools/list` returns what this server offers, `tools/call` runs one.

Example: initialize

paramsobjectoptional
Method arguments. For `tools/call` this is `{"name": "<tool>", "arguments": { … }}`, where `arguments` must satisfy that tool's `inputSchema`.
additionalProperties
true
Complete request schema
{
    "type": "object",
    "title": "JSON-RPC 2.0 request",
    "description": "The body of every MCP call. The **operation is `method`**, not the URL: one server answers `initialize`, `tools/list`, `tools/call`, `ping` and the notification methods on the same path.\n\nA notification (a request with no `id`) is answered with `202 Accepted` and an empty body.",
    "required": [
        "jsonrpc",
        "method"
    ],
    "properties": {
        "jsonrpc": {
            "type": "string",
            "const": "2.0",
            "description": "Always the string \"2.0\"."
        },
        "id": {
            "type": [
                "string",
                "integer"
            ],
            "description": "Correlates the response with this request. Omit it to send a notification, which is acknowledged rather than answered."
        },
        "method": {
            "type": "string",
            "description": "The JSON-RPC method. `initialize` opens the session, `tools/list` returns what this server offers, `tools/call` runs one.",
            "examples": [
                "initialize",
                "tools/list",
                "tools/call",
                "ping"
            ]
        },
        "params": {
            "type": "object",
            "description": "Method arguments. For `tools/call` this is `{\"name\": \"<tool>\", \"arguments\": { \u2026 }}`, where `arguments` must satisfy that tool's `inputSchema`.",
            "additionalProperties": true
        }
    },
    "x-generated-by": "php artisan mcp:manifest"
}
1. initialize — open the session

Sent once, first. Negotiates a protocol version and returns the server's instructions.

{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "initialize",
    "params": {
        "protocolVersion": "2025-06-18",
        "capabilities": {
            "roots": {
                "listChanged": true
            }
        },
        "clientInfo": {
            "name": "my-agent",
            "version": "1.0.0"
        }
    }
}
2. tools/list — discover what is here

The only way to learn the tool names. They are deliberately not in this OpenAPI document: what a credential can reach depends on the account, its modules and the granted scopes.

{
    "jsonrpc": "2.0",
    "id": 2,
    "method": "tools/list"
}
3. tools/call — run one

`params.arguments` must satisfy that tool's `inputSchema` from `tools/list`.

{
    "jsonrpc": "2.0",
    "id": 3,
    "method": "tools/call",
    "params": {
        "name": "list_ivr_flows",
        "arguments": {
            "search": "main"
        }
    }
}

Responses

200The JSON-RPC result. A notification — a request with no `id` — is answered `202` with an empty body instead.
jsonrpcstringrequired
Always the string "2.0".
const
2.0
idstring | integer | nulloptional
The `id` of the request being answered; null when the request could not be parsed.
resultobjectoptional
The method result. Shape depends on `method` — see the examples.
additionalProperties
true
errorobjectoptional
Present instead of `result` when the call was refused before any tool ran.
Show child properties
codeintegerrequired
JSON-RPC error code. `-32700` parse error, `-32600` invalid request, `-32601` unknown method, `-32602` invalid params, `-32001` unauthenticated, `-32002` refused.
messagestringrequired
A sentence written for a person, not a code.
dataobjectoptional
Extra detail. `reason` names the specific decision behind a refusal.
additionalProperties
true
Show child properties
reasonstringoptional
The specific decision — `missing_token`, `server_not_granted`, `module_disabled`, `surface_disabled`.
{
    "jsonrpc": "2.0",
    "id": 1,
    "result": {
        "protocolVersion": "2025-06-18",
        "capabilities": {
            "tools": {
                "listChanged": false
            },
            "resources": {
                "listChanged": false
            },
            "prompts": {
                "listChanged": false
            }
        },
        "serverInfo": {
            "name": "Momo IVR",
            "version": "1.0.0"
        },
        "instructions": "Build and edit the call flows (IVRs) that answer this business's phone lines. \u2026"
    }
}
initialize
{
    "jsonrpc": "2.0",
    "id": 1,
    "result": {
        "protocolVersion": "2025-06-18",
        "capabilities": {
            "tools": {
                "listChanged": false
            },
            "resources": {
                "listChanged": false
            },
            "prompts": {
                "listChanged": false
            }
        },
        "serverInfo": {
            "name": "Momo IVR",
            "version": "1.0.0"
        },
        "instructions": "Build and edit the call flows (IVRs) that answer this business's phone lines. \u2026"
    }
}
tools/list
{
    "jsonrpc": "2.0",
    "id": 2,
    "result": {
        "tools": [
            {
                "name": "list_ivr_flows",
                "title": "List Ivr Flows",
                "description": "The call flows on this account, newest first.",
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        "search": {
                            "type": "string",
                            "description": "Filter by name."
                        }
                    }
                },
                "annotations": {
                    "readOnlyHint": true
                }
            }
        ]
    }
}
tools/call
{
    "jsonrpc": "2.0",
    "id": 3,
    "result": {
        "content": [
            {
                "type": "text",
                "text": "{\"flows\":[{\"id\":41,\"name\":\"Main line\",\"status\":\"published\"}]}"
            }
        ],
        "isError": false
    }
}
202A notification was accepted. No body.
401No credential, or one that is expired or revoked. The `WWW-Authenticate` header points at the protected-resource document, which is what bootstraps the OAuth handshake.
jsonrpcstringrequired
Always the string "2.0".
const
2.0
idstring | integer | nulloptional
The `id` of the request being answered; null when the request could not be parsed.
resultobjectoptional
The method result. Shape depends on `method` — see the examples.
additionalProperties
true
errorobjectoptional
Present instead of `result` when the call was refused before any tool ran.
Show child properties
codeintegerrequired
JSON-RPC error code. `-32700` parse error, `-32600` invalid request, `-32601` unknown method, `-32602` invalid params, `-32001` unauthenticated, `-32002` refused.
messagestringrequired
A sentence written for a person, not a code.
dataobjectoptional
Extra detail. `reason` names the specific decision behind a refusal.
additionalProperties
true
Show child properties
reasonstringoptional
The specific decision — `missing_token`, `server_not_granted`, `module_disabled`, `surface_disabled`.
{
    "jsonrpc": "2.0",
    "error": {
        "code": -32001,
        "message": "Authentication required.",
        "data": {
            "reason": "missing_token"
        }
    },
    "id": null
}
403The credential is valid but this server is out of reach — a capability that was not granted, a module switched off for the account, a suspended account, or a v3 API key used in place of an MCP credential. `data.reason` says which.
jsonrpcstringrequired
Always the string "2.0".
const
2.0
idstring | integer | nulloptional
The `id` of the request being answered; null when the request could not be parsed.
resultobjectoptional
The method result. Shape depends on `method` — see the examples.
additionalProperties
true
errorobjectoptional
Present instead of `result` when the call was refused before any tool ran.
Show child properties
codeintegerrequired
JSON-RPC error code. `-32700` parse error, `-32600` invalid request, `-32601` unknown method, `-32602` invalid params, `-32001` unauthenticated, `-32002` refused.
messagestringrequired
A sentence written for a person, not a code.
dataobjectoptional
Extra detail. `reason` names the specific decision behind a refusal.
additionalProperties
true
Show child properties
reasonstringoptional
The specific decision — `missing_token`, `server_not_granted`, `module_disabled`, `surface_disabled`.
{
    "jsonrpc": "2.0",
    "error": {
        "code": -32002,
        "message": "This connection was not given access to Numbers. The account owner can add it by reconnecting.",
        "data": {
            "reason": "server_not_granted"
        }
    },
    "id": null
}
429More than 120 requests in a minute from this connection.
jsonrpcstringrequired
Always the string "2.0".
const
2.0
idstring | integer | nulloptional
The `id` of the request being answered; null when the request could not be parsed.
resultobjectoptional
The method result. Shape depends on `method` — see the examples.
additionalProperties
true
errorobjectoptional
Present instead of `result` when the call was refused before any tool ran.
Show child properties
codeintegerrequired
JSON-RPC error code. `-32700` parse error, `-32600` invalid request, `-32601` unknown method, `-32602` invalid params, `-32001` unauthenticated, `-32002` refused.
messagestringrequired
A sentence written for a person, not a code.
dataobjectoptional
Extra detail. `reason` names the specific decision behind a refusal.
additionalProperties
true
Show child properties
reasonstringoptional
The specific decision — `missing_token`, `server_not_granted`, `module_disabled`, `surface_disabled`.
{
    "jsonrpc": "2.0",
    "error": {
        "code": -32003,
        "message": "Too many MCP requests from this connection. Wait a minute and retry \u2014 do not loop."
    },
    "id": null
}
503The MCP surface is switched off platform-wide. Never cached — shutting it down is a database write that takes effect on the next request.
jsonrpcstringrequired
Always the string "2.0".
const
2.0
idstring | integer | nulloptional
The `id` of the request being answered; null when the request could not be parsed.
resultobjectoptional
The method result. Shape depends on `method` — see the examples.
additionalProperties
true
errorobjectoptional
Present instead of `result` when the call was refused before any tool ran.
Show child properties
codeintegerrequired
JSON-RPC error code. `-32700` parse error, `-32600` invalid request, `-32601` unknown method, `-32602` invalid params, `-32001` unauthenticated, `-32002` refused.
messagestringrequired
A sentence written for a person, not a code.
dataobjectoptional
Extra detail. `reason` names the specific decision behind a refusal.
additionalProperties
true
Show child properties
reasonstringoptional
The specific decision — `missing_token`, `server_not_granted`, `module_disabled`, `surface_disabled`.
{
    "jsonrpc": "2.0",
    "error": {
        "code": -32002,
        "message": "The MCP surface is not enabled on this platform right now.",
        "data": {
            "reason": "surface_disabled"
        }
    },
    "id": null
}

API REFERENCE / MCP

List the servers this credential can reach

GET/mcp/v1

What this credential can reach, which is not the same question as what exists: scopes differ per credential, and a capability the account holder did not grant leaves its server absent rather than merely unauthorized.

The public catalogue, for someone evaluating the platform before they have a token, is GET /api-docs/mcp.json.

AuthenticationMCP connection token or OAuth access token

Responses

200The servers, each marked reachable or not for this credential.
accountstring | nulloptional
The account this credential belongs to.
protocolstringoptional
Always "mcp".
const
mcp
transportstringoptional
The MCP transport these endpoints speak.
const
streamable-http
serversarray<object>optional
Every mounted server, marked reachable or not for this credential.
Show child properties
keystringrequired
The `{server}` path segment.
enum
["ivr","flows","data","approvals","payments","automations","alerts","operations","studio","numbers","groups","agents","orders","shop","tickets","kb","content","calls","routing","meetings","messaging","inbox","comments","posts","contacts","overview","accounts","navigate","account"]
namestringrequired
Display name.
descriptionstringoptional
What the server is for.
urlstringrequired
The absolute endpoint to point a client at.
format
uri
availablebooleanrequired
Whether this credential may reach it.
reasonstring | nulloptional
Why not, when `available` is false.
docsstringoptional
Where a person can read about all of this.
format
uri
{
    "account": "Workspace Alpha",
    "protocol": "mcp",
    "transport": "streamable-http",
    "servers": [
        {
            "key": "ivr",
            "name": "IVR",
            "description": "Build and edit call flows: read the graph, apply node operations, validate, simulate, version and assign to numbers.",
            "url": "https://business.momo.tz/mcp/v1/ivr",
            "available": true,
            "reason": null
        },
        {
            "key": "numbers",
            "name": "Numbers",
            "description": "Phone numbers: what you own, what is available, what one costs, and how to pay for it.",
            "url": "https://business.momo.tz/mcp/v1/numbers",
            "available": false,
            "reason": "This connection was not granted access to Numbers."
        }
    ],
    "docs": "https://business.momo.tz/api-docs#mcp"
}
401No credential, or one that is expired or revoked.
jsonrpcstringrequired
Always the string "2.0".
const
2.0
idstring | integer | nulloptional
The `id` of the request being answered; null when the request could not be parsed.
resultobjectoptional
The method result. Shape depends on `method` — see the examples.
additionalProperties
true
errorobjectoptional
Present instead of `result` when the call was refused before any tool ran.
Show child properties
codeintegerrequired
JSON-RPC error code. `-32700` parse error, `-32600` invalid request, `-32601` unknown method, `-32602` invalid params, `-32001` unauthenticated, `-32002` refused.
messagestringrequired
A sentence written for a person, not a code.
dataobjectoptional
Extra detail. `reason` names the specific decision behind a refusal.
additionalProperties
true
Show child properties
reasonstringoptional
The specific decision — `missing_token`, `server_not_granted`, `module_disabled`, `surface_disabled`.
{
    "jsonrpc": "2.0",
    "error": {
        "code": -32001,
        "message": "Authentication required.",
        "data": {
            "reason": "missing_token"
        }
    },
    "id": null
}
503The MCP surface is switched off platform-wide.
jsonrpcstringrequired
Always the string "2.0".
const
2.0
idstring | integer | nulloptional
The `id` of the request being answered; null when the request could not be parsed.
resultobjectoptional
The method result. Shape depends on `method` — see the examples.
additionalProperties
true
errorobjectoptional
Present instead of `result` when the call was refused before any tool ran.
Show child properties
codeintegerrequired
JSON-RPC error code. `-32700` parse error, `-32600` invalid request, `-32601` unknown method, `-32602` invalid params, `-32001` unauthenticated, `-32002` refused.
messagestringrequired
A sentence written for a person, not a code.
dataobjectoptional
Extra detail. `reason` names the specific decision behind a refusal.
additionalProperties
true
Show child properties
reasonstringoptional
The specific decision — `missing_token`, `server_not_granted`, `module_disabled`, `surface_disabled`.
{
    "jsonrpc": "2.0",
    "error": {
        "code": -32002,
        "message": "The MCP surface is not enabled on this platform right now.",
        "data": {
            "reason": "surface_disabled"
        }
    },
    "id": null
}

API REFERENCE / MCP

Call one MCP server

POST/mcp/v1/{server}

One server, one URL, one JSON-RPC endpoint. The tools it offers are discovered at runtime with tools/list; their arguments are a JSON Schema each, published for every server at GET /api-docs/mcp.json.

server Name What it covers
ivr IVR Build and edit call flows: read the graph, apply node operations, validate, simulate, version and assign to numbers.
flows Message flows Build and edit WhatsApp conversation flows: nodes, edges, triggers, validation, simulation and analytics.
data Data tables The tables this business defined for itself and their records: read with filters, create/update/upsert rows, shape fields, run and save reports, and group related tables into folders with reports that read across them. Flows and IVRs read the same tables.
approvals Approvals Decisions a person has been asked for before something happens: read the queue, read one in full with every comment on it, answer one.
payments Payments Money this business collects from its customers: what has been asked for and where each one got to, one payment's whole timeline, asking a customer to pay, and refunds. Not the business's own Momo bill.
automations Automations What happens without anybody there: the log of what has actually happened in the business, the subscriptions that react to it, and the schedules that run on a rhythm.
alerts Alerts & service levels The business watching itself: the alert rules it wrote, the service-level promises and the clocks running against them, the risk rules that hold or refuse an action, and one log of everything that fired — including what reached nobody.
operations Operations The named things this business can do — create a booking, register a customer, process a refund — each written down once, and the log of every time one ran.
studio Studio Voice and audio: browse the voice library, generate speech, convert audio and publish it for use in an IVR.
numbers Numbers Phone numbers: what you own, what is available, what one costs, and how to pay for it.
groups WhatsApp groups Groups the business runs from its WhatsApp number: create, invite, post, approve joins, remove members.
agents Agents Your own AI specialists: see the roster and ask one a question.
orders Orders Customer orders across every platform: find, read, move status, request payment.
shop Shop Products, brands and categories, plus the order tools.
tickets Tickets Support tickets: create, update, assign, reply, labels and notifications.
kb Knowledge base Your knowledge base: categories, search and full article text.
content Platform content Public help articles, changelog, roadmap and system status.
calls Calls Call history, recordings, transcripts, events and Call Studio scripts.
routing Call routing Routing rules, ring groups, working hours and forwarding targets.
meetings Meetings See and schedule meetings, and invite people to them.
messaging Messaging Templates, sender IDs, campaigns, message history — and sending SMS and WhatsApp.
inbox Inbox Customer conversations across WhatsApp, SMS, social and email — read, assign, reply, and send new mail.
comments Comments Comments on your Facebook, Instagram and TikTok posts.
posts Posts Social posts to Facebook, Instagram, TikTok, YouTube and LinkedIn: what is drafted, scheduled and sent; drafting a new one; scheduling or publishing it.
contacts Contacts The contact book and groups.
overview Overview The dashboard, business analytics, call stats and spend — how the business is doing.
accounts Connected accounts The WhatsApp numbers, social profiles, mailboxes and SMS routes this business has connected, and what each can actually do.
navigate Finding things Where pages and settings live in the app, and what each form asks for.
account Account A cross-domain starting point: overview, search, fetch, and the most-used read tools.
AuthenticationMCP connection token or OAuth access token

Path parameters

serverstringrequired
Which server to talk to.
enum
["ivr","flows","data","approvals","payments","automations","alerts","operations","studio","numbers","groups","agents","orders","shop","tickets","kb","content","calls","routing","meetings","messaging","inbox","comments","posts","contacts","overview","accounts","navigate","account"]

Example: ivr

Request body

application/json · required

A JSON-RPC 2.0 request. `Accept` must allow both `application/json` and `text/event-stream`.

jsonrpcstringrequired
Always the string "2.0".
const
2.0
idstring | integeroptional
Correlates the response with this request. Omit it to send a notification, which is acknowledged rather than answered.
methodstringrequired
The JSON-RPC method. `initialize` opens the session, `tools/list` returns what this server offers, `tools/call` runs one.

Example: initialize

paramsobjectoptional
Method arguments. For `tools/call` this is `{"name": "<tool>", "arguments": { … }}`, where `arguments` must satisfy that tool's `inputSchema`.
additionalProperties
true
Complete request schema
{
    "type": "object",
    "title": "JSON-RPC 2.0 request",
    "description": "The body of every MCP call. The **operation is `method`**, not the URL: one server answers `initialize`, `tools/list`, `tools/call`, `ping` and the notification methods on the same path.\n\nA notification (a request with no `id`) is answered with `202 Accepted` and an empty body.",
    "required": [
        "jsonrpc",
        "method"
    ],
    "properties": {
        "jsonrpc": {
            "type": "string",
            "const": "2.0",
            "description": "Always the string \"2.0\"."
        },
        "id": {
            "type": [
                "string",
                "integer"
            ],
            "description": "Correlates the response with this request. Omit it to send a notification, which is acknowledged rather than answered."
        },
        "method": {
            "type": "string",
            "description": "The JSON-RPC method. `initialize` opens the session, `tools/list` returns what this server offers, `tools/call` runs one.",
            "examples": [
                "initialize",
                "tools/list",
                "tools/call",
                "ping"
            ]
        },
        "params": {
            "type": "object",
            "description": "Method arguments. For `tools/call` this is `{\"name\": \"<tool>\", \"arguments\": { \u2026 }}`, where `arguments` must satisfy that tool's `inputSchema`.",
            "additionalProperties": true
        }
    },
    "x-generated-by": "php artisan mcp:manifest"
}
1. initialize — open the session

Sent once, first. Negotiates a protocol version and returns the server's instructions.

{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "initialize",
    "params": {
        "protocolVersion": "2025-06-18",
        "capabilities": {
            "roots": {
                "listChanged": true
            }
        },
        "clientInfo": {
            "name": "my-agent",
            "version": "1.0.0"
        }
    }
}
2. tools/list — discover what is here

The only way to learn the tool names. They are deliberately not in this OpenAPI document: what a credential can reach depends on the account, its modules and the granted scopes.

{
    "jsonrpc": "2.0",
    "id": 2,
    "method": "tools/list"
}
3. tools/call — run one

`params.arguments` must satisfy that tool's `inputSchema` from `tools/list`.

{
    "jsonrpc": "2.0",
    "id": 3,
    "method": "tools/call",
    "params": {
        "name": "list_ivr_flows",
        "arguments": {
            "search": "main"
        }
    }
}

Responses

200The JSON-RPC result. A notification — a request with no `id` — is answered `202` with an empty body instead.
jsonrpcstringrequired
Always the string "2.0".
const
2.0
idstring | integer | nulloptional
The `id` of the request being answered; null when the request could not be parsed.
resultobjectoptional
The method result. Shape depends on `method` — see the examples.
additionalProperties
true
errorobjectoptional
Present instead of `result` when the call was refused before any tool ran.
Show child properties
codeintegerrequired
JSON-RPC error code. `-32700` parse error, `-32600` invalid request, `-32601` unknown method, `-32602` invalid params, `-32001` unauthenticated, `-32002` refused.
messagestringrequired
A sentence written for a person, not a code.
dataobjectoptional
Extra detail. `reason` names the specific decision behind a refusal.
additionalProperties
true
Show child properties
reasonstringoptional
The specific decision — `missing_token`, `server_not_granted`, `module_disabled`, `surface_disabled`.
{
    "jsonrpc": "2.0",
    "id": 1,
    "result": {
        "protocolVersion": "2025-06-18",
        "capabilities": {
            "tools": {
                "listChanged": false
            },
            "resources": {
                "listChanged": false
            },
            "prompts": {
                "listChanged": false
            }
        },
        "serverInfo": {
            "name": "Momo IVR",
            "version": "1.0.0"
        },
        "instructions": "Build and edit the call flows (IVRs) that answer this business's phone lines. \u2026"
    }
}
initialize
{
    "jsonrpc": "2.0",
    "id": 1,
    "result": {
        "protocolVersion": "2025-06-18",
        "capabilities": {
            "tools": {
                "listChanged": false
            },
            "resources": {
                "listChanged": false
            },
            "prompts": {
                "listChanged": false
            }
        },
        "serverInfo": {
            "name": "Momo IVR",
            "version": "1.0.0"
        },
        "instructions": "Build and edit the call flows (IVRs) that answer this business's phone lines. \u2026"
    }
}
tools/list
{
    "jsonrpc": "2.0",
    "id": 2,
    "result": {
        "tools": [
            {
                "name": "list_ivr_flows",
                "title": "List Ivr Flows",
                "description": "The call flows on this account, newest first.",
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        "search": {
                            "type": "string",
                            "description": "Filter by name."
                        }
                    }
                },
                "annotations": {
                    "readOnlyHint": true
                }
            }
        ]
    }
}
tools/call
{
    "jsonrpc": "2.0",
    "id": 3,
    "result": {
        "content": [
            {
                "type": "text",
                "text": "{\"flows\":[{\"id\":41,\"name\":\"Main line\",\"status\":\"published\"}]}"
            }
        ],
        "isError": false
    }
}
202A notification was accepted. No body.
401No credential, or one that is expired or revoked. The `WWW-Authenticate` header points at the protected-resource document, which is what bootstraps the OAuth handshake.
jsonrpcstringrequired
Always the string "2.0".
const
2.0
idstring | integer | nulloptional
The `id` of the request being answered; null when the request could not be parsed.
resultobjectoptional
The method result. Shape depends on `method` — see the examples.
additionalProperties
true
errorobjectoptional
Present instead of `result` when the call was refused before any tool ran.
Show child properties
codeintegerrequired
JSON-RPC error code. `-32700` parse error, `-32600` invalid request, `-32601` unknown method, `-32602` invalid params, `-32001` unauthenticated, `-32002` refused.
messagestringrequired
A sentence written for a person, not a code.
dataobjectoptional
Extra detail. `reason` names the specific decision behind a refusal.
additionalProperties
true
Show child properties
reasonstringoptional
The specific decision — `missing_token`, `server_not_granted`, `module_disabled`, `surface_disabled`.
{
    "jsonrpc": "2.0",
    "error": {
        "code": -32001,
        "message": "Authentication required.",
        "data": {
            "reason": "missing_token"
        }
    },
    "id": null
}
403The credential is valid but this server is out of reach — a capability that was not granted, a module switched off for the account, a suspended account, or a v3 API key used in place of an MCP credential. `data.reason` says which.
jsonrpcstringrequired
Always the string "2.0".
const
2.0
idstring | integer | nulloptional
The `id` of the request being answered; null when the request could not be parsed.
resultobjectoptional
The method result. Shape depends on `method` — see the examples.
additionalProperties
true
errorobjectoptional
Present instead of `result` when the call was refused before any tool ran.
Show child properties
codeintegerrequired
JSON-RPC error code. `-32700` parse error, `-32600` invalid request, `-32601` unknown method, `-32602` invalid params, `-32001` unauthenticated, `-32002` refused.
messagestringrequired
A sentence written for a person, not a code.
dataobjectoptional
Extra detail. `reason` names the specific decision behind a refusal.
additionalProperties
true
Show child properties
reasonstringoptional
The specific decision — `missing_token`, `server_not_granted`, `module_disabled`, `surface_disabled`.
{
    "jsonrpc": "2.0",
    "error": {
        "code": -32002,
        "message": "This connection was not given access to Numbers. The account owner can add it by reconnecting.",
        "data": {
            "reason": "server_not_granted"
        }
    },
    "id": null
}
404No server by that key.
jsonrpcstringrequired
Always the string "2.0".
const
2.0
idstring | integer | nulloptional
The `id` of the request being answered; null when the request could not be parsed.
resultobjectoptional
The method result. Shape depends on `method` — see the examples.
additionalProperties
true
errorobjectoptional
Present instead of `result` when the call was refused before any tool ran.
Show child properties
codeintegerrequired
JSON-RPC error code. `-32700` parse error, `-32600` invalid request, `-32601` unknown method, `-32602` invalid params, `-32001` unauthenticated, `-32002` refused.
messagestringrequired
A sentence written for a person, not a code.
dataobjectoptional
Extra detail. `reason` names the specific decision behind a refusal.
additionalProperties
true
Show child properties
reasonstringoptional
The specific decision — `missing_token`, `server_not_granted`, `module_disabled`, `surface_disabled`.
{
    "jsonrpc": "2.0",
    "error": {
        "code": -32002,
        "message": "No such MCP server.",
        "data": {
            "reason": "unknown_server"
        }
    },
    "id": null
}
429More than 120 requests in a minute from this connection.
jsonrpcstringrequired
Always the string "2.0".
const
2.0
idstring | integer | nulloptional
The `id` of the request being answered; null when the request could not be parsed.
resultobjectoptional
The method result. Shape depends on `method` — see the examples.
additionalProperties
true
errorobjectoptional
Present instead of `result` when the call was refused before any tool ran.
Show child properties
codeintegerrequired
JSON-RPC error code. `-32700` parse error, `-32600` invalid request, `-32601` unknown method, `-32602` invalid params, `-32001` unauthenticated, `-32002` refused.
messagestringrequired
A sentence written for a person, not a code.
dataobjectoptional
Extra detail. `reason` names the specific decision behind a refusal.
additionalProperties
true
Show child properties
reasonstringoptional
The specific decision — `missing_token`, `server_not_granted`, `module_disabled`, `surface_disabled`.
{
    "jsonrpc": "2.0",
    "error": {
        "code": -32003,
        "message": "Too many MCP requests from this connection. Wait a minute and retry \u2014 do not loop."
    },
    "id": null
}
503The MCP surface is switched off platform-wide. Never cached — shutting it down is a database write that takes effect on the next request.
jsonrpcstringrequired
Always the string "2.0".
const
2.0
idstring | integer | nulloptional
The `id` of the request being answered; null when the request could not be parsed.
resultobjectoptional
The method result. Shape depends on `method` — see the examples.
additionalProperties
true
errorobjectoptional
Present instead of `result` when the call was refused before any tool ran.
Show child properties
codeintegerrequired
JSON-RPC error code. `-32700` parse error, `-32600` invalid request, `-32601` unknown method, `-32602` invalid params, `-32001` unauthenticated, `-32002` refused.
messagestringrequired
A sentence written for a person, not a code.
dataobjectoptional
Extra detail. `reason` names the specific decision behind a refusal.
additionalProperties
true
Show child properties
reasonstringoptional
The specific decision — `missing_token`, `server_not_granted`, `module_disabled`, `surface_disabled`.
{
    "jsonrpc": "2.0",
    "error": {
        "code": -32002,
        "message": "The MCP surface is not enabled on this platform right now.",
        "data": {
            "reason": "surface_disabled"
        }
    },
    "id": null
}

API REFERENCE / MCP

OAuth 2.1 authorization server metadata

GET/.well-known/oauth-authorization-server

RFC 8414 discovery, and the first call a hosted client makes. Claude will not finish a connector setup without a registration_endpoint here, and ChatGPT's OAuth mode needs the same handshake.

scopes_supported is the full granular set rather than a single blanket scope, because a client can only ask for what is advertised.

AuthenticationNo bearer token required

Responses

200The discovery document.
issuerstringoptional
The authorization server's identifier.
format
uri
authorization_endpointstringoptional
Where the person is sent to approve the connection.
format
uri
token_endpointstringoptional
Where the authorization code is exchanged for a token.
format
uri
registration_endpointstringoptional
RFC 7591 dynamic client registration. Claude will not finish a connector setup without this field.
format
uri
response_types_supportedarray<string>optional
Only `code`.
code_challenge_methods_supportedarray<string>optional
Only `S256`. PKCE is required, not optional.
scopes_supportedarray<string>optional
Every scope a client may ask for. A client can only request what is advertised here.
items.enum
["mcp:use","mcp:overview","mcp:calls","mcp:routing","mcp:numbers","mcp:meetings","mcp:builders","mcp:data","mcp:studio","mcp:contacts","mcp:agents","mcp:commerce","mcp:support","mcp:accounts","mcp:approvals","mcp:payments","mcp:automations","mcp:alerts","mcp:operations","mcp:navigate","mcp:messaging","mcp:inbox","mcp:comments","mcp:posts","mcp:groups","mcp:publish","mcp:send","mcp:spend","mcp:delete","mcp:write","mcp:shape","mcp:automate","mcp:approve"]
grant_types_supportedarray<string>optional
`authorization_code` and `refresh_token`.
token_endpoint_auth_methods_supportedarray<string>optional
`none`: clients are public and authenticate with PKCE.
{
    "issuer": "https://business.momo.tz",
    "authorization_endpoint": "https://business.momo.tz/oauth/authorize",
    "token_endpoint": "https://business.momo.tz/oauth/token",
    "registration_endpoint": "https://business.momo.tz/oauth/register",
    "response_types_supported": [
        "code"
    ],
    "code_challenge_methods_supported": [
        "S256"
    ],
    "scopes_supported": [
        "mcp:use",
        "mcp:overview",
        "mcp:calls",
        "mcp:routing",
        "mcp:numbers",
        "mcp:meetings",
        "mcp:builders",
        "mcp:data",
        "mcp:studio",
        "mcp:contacts",
        "mcp:agents",
        "mcp:commerce",
        "mcp:support",
        "mcp:accounts",
        "mcp:approvals",
        "mcp:payments",
        "mcp:automations",
        "mcp:alerts",
        "mcp:operations",
        "mcp:navigate",
        "mcp:messaging",
        "mcp:inbox",
        "mcp:comments",
        "mcp:posts",
        "mcp:groups",
        "mcp:publish",
        "mcp:send",
        "mcp:spend",
        "mcp:delete",
        "mcp:write",
        "mcp:shape",
        "mcp:automate",
        "mcp:approve"
    ],
    "grant_types_supported": [
        "authorization_code",
        "refresh_token"
    ],
    "token_endpoint_auth_methods_supported": [
        "none"
    ]
}

API REFERENCE / MCP

OAuth 2.1 protected resource metadata

GET/.well-known/oauth-protected-resource

RFC 9728. A client that gets a 401 from an MCP endpoint reads the WWW-Authenticate header, lands here, and learns which authorization server guards the resource. That chain is what turns an unauthenticated first request into a completed connector setup without anybody typing a URL.

AuthenticationNo bearer token required

Responses

200The resource metadata.
resourcestringoptional
The protected resource this document describes.
format
uri
authorization_serversarray<string>optional
Where to go to get a token for it.
items.format
uri
scopes_supportedarray<string>optional
The base scope every MCP token carries.
{
    "resource": "https://business.momo.tz",
    "authorization_servers": [
        "https://business.momo.tz"
    ],
    "scopes_supported": [
        "mcp:use"
    ]
}

API REFERENCE / MCP

Register an OAuth client

POST/oauth/register

RFC 7591 dynamic client registration. Open by design — a hosted client registers itself, unattended, the first time somebody adds the connector — which is why redirect_uris is checked against an allow-list of published callback hosts. Register a redirect you control and the authorization code for somebody's account would be delivered to you, so a redirect outside the list is rejected rather than trusted.

The issued client is public: no secret, PKCE required.

AuthenticationNo bearer token required

Request body

application/json · required

client_namestringoptional
A name for the client. `name` is accepted as an alias; one of the two is required.
maxLength
255
namestringoptional
Alias for `client_name`.
maxLength
255
redirect_urisarray<string>required
Absolute callback URLs. Each must sit under a permitted host, or under loopback for a desktop client that finishes the flow locally.
minItems
1
items.format
uri
Complete request schema
{
    "type": "object",
    "required": [
        "redirect_uris"
    ],
    "properties": {
        "client_name": {
            "type": "string",
            "maxLength": 255,
            "description": "A name for the client. `name` is accepted as an alias; one of the two is required."
        },
        "name": {
            "type": "string",
            "maxLength": 255,
            "description": "Alias for `client_name`."
        },
        "redirect_uris": {
            "type": "array",
            "minItems": 1,
            "items": {
                "type": "string",
                "format": "uri"
            },
            "description": "Absolute callback URLs. Each must sit under a permitted host, or under loopback for a desktop client that finishes the flow locally."
        }
    }
}

Responses

200The registered client.
client_idstringoptional
Send this on the authorize and token calls.
grant_typesarray<string>optional
The grants this client may use.
response_typesarray<string>optional
Only `code`.
redirect_urisarray<string>optional
The callbacks that were accepted.
items.format
uri
scopestringoptional
The default scope. Ask for more on the authorize call.
token_endpoint_auth_methodstringoptional
No client secret is issued: this is a public client and PKCE is the proof.
const
none
{
    "client_id": "9d1f6c2a-4e1b-4a77-9a3a-0f2f1b0d5c11",
    "grant_types": [
        "authorization_code",
        "refresh_token"
    ],
    "response_types": [
        "code"
    ],
    "redirect_uris": [
        "https://claude.ai/api/mcp/auth_callback"
    ],
    "scope": "mcp:use",
    "token_endpoint_auth_method": "none"
}
422The registration was rejected — most often a redirect URI outside the permitted hosts.
messagestringoptional
A sentence naming the first problem.
errorsobjectoptional
Each rejected field mapped to its messages.
additionalProperties
{"type":"array","items":{"type":"string"}}
{
    "message": "The redirect uris.0 field is not a permitted redirect domain.",
    "errors": {
        "redirect_uris.0": [
            "redirect_uris.0 is not a permitted redirect domain."
        ]
    }
}

API REFERENCE / MCP

Exchange an authorization code for an access token

POST/oauth/token

The standard OAuth 2.1 token endpoint, form-encoded. Public clients only: send code_verifier, not a client secret. refresh_token is supported with the same call and grant_type=refresh_token.

The returned token carries the scopes the account holder actually ticked, which may be fewer than the ones requested.

AuthenticationNo bearer token required

Request body

application/x-www-form-urlencoded · required

grant_typestringrequired
Which exchange this is.
enum
["authorization_code","refresh_token"]
client_idstringrequired
The client id from dynamic client registration.
codestringoptional
The authorization code, for `grant_type=authorization_code`.
redirect_uristringoptional
The same redirect used to obtain the code.
format
uri
code_verifierstringoptional
The PKCE verifier whose S256 challenge was sent to the authorize endpoint.
refresh_tokenstringoptional
For `grant_type=refresh_token`.
Complete request schema
{
    "type": "object",
    "required": [
        "grant_type",
        "client_id"
    ],
    "properties": {
        "grant_type": {
            "type": "string",
            "enum": [
                "authorization_code",
                "refresh_token"
            ],
            "description": "Which exchange this is."
        },
        "client_id": {
            "type": "string",
            "description": "The client id from dynamic client registration."
        },
        "code": {
            "type": "string",
            "description": "The authorization code, for `grant_type=authorization_code`."
        },
        "redirect_uri": {
            "type": "string",
            "format": "uri",
            "description": "The same redirect used to obtain the code."
        },
        "code_verifier": {
            "type": "string",
            "description": "The PKCE verifier whose S256 challenge was sent to the authorize endpoint."
        },
        "refresh_token": {
            "type": "string",
            "description": "For `grant_type=refresh_token`."
        }
    }
}

Responses

200The access token.
token_typestringoptional
Always "Bearer".
const
Bearer
expires_inintegeroptional
Seconds until the access token expires.
access_tokenstringoptional
Send as `Authorization: Bearer …` on the MCP endpoints.
refresh_tokenstringoptional
Exchange this for a new access token with `grant_type=refresh_token`.
scopestringoptional
Space-separated scopes actually granted, which may be fewer than were asked for.
{
    "token_type": "Bearer",
    "expires_in": 31536000,
    "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9\u2026",
    "refresh_token": "def50200f0a1\u2026",
    "scope": "mcp:use mcp:overview mcp:calls"
}
400The grant was refused — a spent or mismatched code, a bad `code_verifier`, or an unknown client.
errorstringoptional
The OAuth error code, such as `invalid_grant` or `invalid_client`.
error_descriptionstringoptional
What went wrong, in a sentence.
hintstringoptional
Which part of the request was at fault, when the server can tell.
messagestringoptional
The same text as `error_description`.
{
    "error": "invalid_grant",
    "error_description": "The provided authorization grant is invalid, expired, revoked, or was issued to another client.",
    "message": "The provided authorization grant is invalid, expired, revoked, or was issued to another client."
}
429Too many token requests. Back off and retry.
messagestringoptional
The refusal, in a sentence.
{
    "message": "Too Many Attempts."
}

API REFERENCE / MCP

Get the MCP tool manifest

GET/api-docs/mcp.json

The machine-readable tool contract: every server, every tool, and a complete JSON Schema for each tool's arguments — generate typed bindings from it rather than hand-writing them.

Public and unauthenticated on purpose, so a developer can point a client at us before they have signed up. It is the same document this OpenAPI file is for REST: this one describes the transport, that one describes the operations.

AuthenticationNo bearer token required

Responses

200The manifest.
generated_bystringoptional
The command that wrote this document. It is generated, never hand-edited.
const
php artisan mcp:manifest
transportstringoptional
The MCP transport every server speaks.
const
streamable-http
protocol_versionsarray<string>optional
Protocol versions accepted at `initialize`, newest first.
authobjectoptional
The OAuth handshake and the bearer alternative, plus the scopes a consent screen offers.
additionalProperties
true
presetsarray<object>optional
Ready-made server selections offered when somebody creates a connection.
items.additionalProperties
true
server_countintegeroptional
How many servers are mounted.
tool_countintegeroptional
How many tools they carry between them.
rootobjectoptional
The aggregate root at `/mcp` — every area a connection was granted, behind one URL.
additionalProperties
true
Show child properties
pathstringoptional
The endpoint, relative to the API host.
tool_countintegeroptional
How many distinct tools the whole surface carries.
tools_hashstringoptional
One hash over every tool's version. Store it, and a single comparison tells you whether the surface you generated against is the one being served.

Example: a1b2c3d4

serversarray<object>optional
Every server and the tools it carries.
Show child properties
keystringoptional
The `{server}` path segment.
namestringoptional
Display name.
summarystringoptional
What the server is for.
pathstringoptional
The endpoint, relative to the API host.
modulestring | nulloptional
The sidebar module this server follows; null when it is always available.
instructionsstringoptional
What the server tells a model about itself at `initialize`.
toolsarray<object>optional
Its tools, each with a full JSON Schema for its arguments.
Show child properties
namestringrequired
The value to send as `params.name` on `tools/call`.

Example: list_ivr_flows

titlestring | nulloptional
A human label, when the tool sets one.
descriptionstringoptional
What the tool does and when to reach for it. This is the text a model actually chooses on.
inputSchemaobjectrequired
JSON Schema (draft 2020-12) for `params.arguments`.
additionalProperties
true
outputSchemaobjectoptional
Present only when the tool declares a structured result.
additionalProperties
true
annotationsobjectoptional
Behavioural hints. `readOnlyHint` marks a tool that only reads; `destructiveHint` marks one that changes the account. Two more are ours. `version` is a hash of this tool's contract — its name, description and argument schema — so a cached definition can be checked rather than trusted. `available` says whether THIS connection could actually call it; when it is false, `withheld_capability` names the tick or permission that is missing and `withheld_reason` is the sentence a call would come back with. The aggregate root at `/mcp` leaves a tool it cannot offer out of the list entirely and explains it on the call; the per-area URLs list their tools whatever the credential holds, so that is where an unavailable one shows up.
additionalProperties
true
Show child properties
readOnlyHintbooleanoptional
True when the tool only reads.
destructiveHintbooleanoptional
True when the tool changes the account.
idempotentHintbooleanoptional
True when calling twice with the same arguments is the same as calling once.
openWorldHintbooleanoptional
True when the tool reaches something outside this platform.
versionstringoptional
Eight hex characters over the tool's name, description and argument schema. It changes when the contract changes, and never otherwise.

Example: 3f9c1a04

availablebooleanoptional
False when this connection was not granted what the tool needs. It is still listed, and calling it returns the reason rather than "not found".
withheld_capabilitystringoptional
Present when `available` is false: the consent-screen tick or the permission that is missing, worded as the refusal words it.

Example: Change tables and fields

withheld_reasonstringoptional
Present when `available` is false: what a call would come back with, in a sentence.
{
    "generated_by": "php artisan mcp:manifest",
    "transport": "streamable-http",
    "protocol_versions": [
        "2025-11-25",
        "2025-06-18",
        "2025-03-26"
    ],
    "server_count": 21,
    "tool_count": 104,
    "servers": [
        {
            "key": "ivr",
            "name": "IVR",
            "path": "/mcp/v1/ivr",
            "tools": [
                {
                    "name": "list_ivr_flows",
                    "version": "3f9c1a04",
                    "description": "The call flows on this account, newest first.",
                    "writes": false,
                    "permissions": [
                        "ivr.view"
                    ],
                    "input_schema": {
                        "$schema": "https://json-schema.org/draft/2020-12/schema",
                        "title": "list_ivr_flows arguments",
                        "type": "object",
                        "properties": {
                            "search": {
                                "type": "string",
                                "description": "Filter by name."
                            }
                        }
                    }
                }
            ]
        }
    ]
}

API REFERENCE / Webhooks

Every event we POST to your server

POST(your webhook URL)

A signed JSON POST to each receiver you registered for the event. Every delivery is a record you can read back at GET /api/v3/webhooks/{webhook}/deliveries and replay. Retries: a 5xx, a timeout or a connection failure is retried five times over about six hours (1 m, 5 m, 30 m, 2 h, 6 h). A 4xx other than 408/425/429 is treated as "understood and refused" and is not retried. After fifty consecutive failures the endpoint is paused and you are told; replaying any failed delivery resumes it. Answer 2xx quickly — persist or enqueue, then acknowledge — and verify the signature over the raw bytes before parsing.

Momo sends this request to your configured webhook URL. It is an incoming callback, not an API endpoint to call.

Header parameters

X-Signaturestringrequired
Plain hex HMAC-SHA256 of the raw body under the endpoint secret; no prefix. Unchanged from the first version, so existing receivers keep verifying.
pattern
^[a-f0-9]{64}$

Example: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

X-Signature-V2stringrequired
Prefer this. `sha256=` + hex HMAC-SHA256 of `"{X-Timestamp}.{X-Delivery-Id}.{raw body}"` under the endpoint secret. Checking it gives you replay protection for free: refuse a timestamp older than five minutes, and remember delivery ids you have seen.
pattern
^sha256=[a-f0-9]{64}$

Example: sha256=bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb

X-Timestampstringrequired
Unix seconds when this attempt was sent. Part of the V2 signature.

Example: 1757581442

X-Delivery-Idstringrequired
Unique per delivery, the same across retries of it. Dedupe on this.

Example: dlv_01j9qk3v8x2m7n4p5r6s

X-Eventstringrequired
The event name, so a receiver can route before parsing.

Example: order.received

Request body

application/json · required

The event payload.

eventstringrequired
Event name. The full list with a sample payload each is at `GET /api/v3/webhooks/events`; the dashboard picker and this enum both read the same catalogue.
enum
["message.received","message.sent","message.delivered","message.read","message.failed","message.echoed","message.updated","order.received","order.status_changed","order.cancelled","order.paid","sync.completed","stock.low","product.blocked","product.drifted","campaign.completed","campaign.failed","group.created","group.create_failed","group.updated","group.deleted","group.suspended","group.suspension_cleared","group.participant_joined","group.participant_left","group.participant_removed","group.join_requested","group.join_request_revoked","group.invite_sent","template.status_changed","webhook.paused"]
timestampstringrequired
Dispatch time in ISO8601.
format
date-time
message_idintegeroptional
Message events: local numeric message ID, usable in SMS/WhatsApp lookup.
directionstringoptional
Message direction.
enum
["inbound","outbound"]
senderstring | nulloptional
Message sender identity.
recipientstringoptional
Message recipient identity.
statusstringoptional
Message delivery state.
bodystring | nulloptional
Message body.
media_urlstring | nulloptional
Attached media URL.
channel_typestring | nulloptional
Message channel type.
order_idintegeroptional
Order events: local order ID.
customer_wa_idstringoptional
order.received: customer WhatsApp identifier.
customer_namestring | nulloptional
order.received: customer name.
product_itemsarray<object>optional
order.received: incoming cart items.
Show child properties
product_retailer_idstringoptional
The SKU the customer added to the cart.
quantityintegeroptional
How many.
item_priceintegeroptional
Unit price in the minor unit of `currency`.
currencystringoptional
ISO 4217 currency code.
unit_price_minorinteger | nulloptional
Unit price in the minor unit. Prefer this over `item_price`, which is Meta's own field and is in MAJOR units.
line_total_minorinteger | nulloptional
Unit price times quantity, in the minor unit.
reservedinteger | nulloptional
How many units of this line are actually being held for the order.
stock_shortboolean | nulloptional
True when we could not hold the whole quantity. The order was still recorded.
unresolvedboolean | nulloptional
True when this product code is not in the catalogue. The code is kept verbatim so a person can work out what the customer meant.
total_amountintegeroptional
order.received: total in integer hundredths.
total_currencystringoptional
order.received: currency code.
customer_notestring | nulloptional
order.received: customer note.
conversation_idinteger | nulloptional
Order event: linked conversation ID.
created_atstringoptional
order.received: creation time.
format
date-time
payment_idintegeroptional
order.paid: payment ID.
methodstringoptional
order.paid: payment method.
amount_minorintegeroptional
order.paid: paid amount in minor units.
currencystringoptional
order.paid: currency code.
payer_msisdnstring | nulloptional
order.paid: payer phone number.
paid_atstring | nulloptional
order.paid: settlement time.
format
date-time
groupobjectoptional
Current local WhatsApp group summary, when the event concerns a group.
Show child properties
idintegerrequired
Platform id of the group; what every group endpoint takes.
meta_group_idstring | nulloptional
WhatsApp's own group id. Null while the group is still being created.
request_idstring | nulloptional
WhatsApp's create request id; how the confirmation webhook is matched.
phone_number_idstringoptional
The business number the group was created from.
waba_idstring | nulloptional
The WhatsApp Business Account the number belongs to.
subjectstringrequired
The group name, up to 128 characters.
maxLength
128
descriptionstring | nulloptional
What the group is for; members see it before joining. Up to 2048 characters.
maxLength
2048
join_approval_modestringoptional
auto_approve: anyone with the link joins. approval_required: the business approves each request.
enum
["auto_approve","approval_required"]
invite_linkstring | nulloptional
The chat.whatsapp.com link people tap to join. Null until WhatsApp confirms the group.
statusstringrequired
creating (waiting for WhatsApp), active, suspended (by WhatsApp, for policy), deleted, or failed (WhatsApp refused to create it; see last_error).
enum
["creating","active","suspended","deleted","failed"]
participant_countintegerrequired
Members besides the business.
max_participantsintegerrequired
8, the business counted in.
seats_leftintegeroptional
How many more people can join.
pending_join_requestsintegeroptional
People waiting for approval on an approval_required group.
conversation_idinteger | nulloptional
The inbox thread for the group.
invite_template_idinteger | nulloptional
The approved template used for invites from this group.
last_message_atstring | nulloptional
When the thread last had a message, either way.
format
date-time
last_errorobject | array | nulloptional
WhatsApp's last refusal, when there was one.
last_synced_atstring | nulloptional
When the roster and settings were last read back from WhatsApp.
format
date-time
created_atstring | nulloptional
When the platform created the record.
format
date-time
updated_atstring | nulloptional
When it last changed.
format
date-time
wa_idsarray<string>optional
Participant event: affected WhatsApp IDs.
reasonstring | nulloptional
Participant event reason when supplied.
appliedobjectoptional
group.updated: applied settings.
additionalProperties
true
errorsarray | object | nulloptional
Provider/group error details.
additionalProperties
true
sentinteger | arrayoptional
group.invite_sent: successfully sent invitations.
failedinteger | arrayoptional
group.invite_sent: failed invitations.
template_idintegeroptional
template.status_changed: local template id, for `GET /api/v3/whatsapp/templates/{template}`.
namestringoptional
template.status_changed: the template name.
languagestringoptional
template.status_changed: the template language.
whatsapp_business_account_idstringoptional
template.status_changed: the WhatsApp Business Account whose review moved.
whatsapp_template_idstring | nulloptional
template.status_changed: Meta's id on that account.
previous_statusstring | nulloptional
template.status_changed: the status before this change.
whatsapp_statusstringoptional
template.status_changed: the status now.
enum
["pending","in_review","approved","rejected","disabled","paused"]
rejection_reasonstring | nulloptional
template.status_changed: Meta's reason when the new status is rejected.
Complete request schema
{
    "type": "object",
    "properties": {
        "event": {
            "type": "string",
            "description": "Event name. The full list with a sample payload each is at `GET /api/v3/webhooks/events`; the dashboard picker and this enum both read the same catalogue.",
            "enum": [
                "message.received",
                "message.sent",
                "message.delivered",
                "message.read",
                "message.failed",
                "message.echoed",
                "message.updated",
                "order.received",
                "order.status_changed",
                "order.cancelled",
                "order.paid",
                "sync.completed",
                "stock.low",
                "product.blocked",
                "product.drifted",
                "campaign.completed",
                "campaign.failed",
                "group.created",
                "group.create_failed",
                "group.updated",
                "group.deleted",
                "group.suspended",
                "group.suspension_cleared",
                "group.participant_joined",
                "group.participant_left",
                "group.participant_removed",
                "group.join_requested",
                "group.join_request_revoked",
                "group.invite_sent",
                "template.status_changed",
                "webhook.paused"
            ]
        },
        "timestamp": {
            "type": "string",
            "description": "Dispatch time in ISO8601.",
            "format": "date-time"
        },
        "message_id": {
            "type": "integer",
            "description": "Message events: local numeric message ID, usable in SMS/WhatsApp lookup."
        },
        "direction": {
            "type": "string",
            "description": "Message direction.",
            "enum": [
                "inbound",
                "outbound"
            ]
        },
        "sender": {
            "type": [
                "string",
                "null"
            ],
            "description": "Message sender identity."
        },
        "recipient": {
            "type": "string",
            "description": "Message recipient identity."
        },
        "status": {
            "type": "string",
            "description": "Message delivery state."
        },
        "body": {
            "type": [
                "string",
                "null"
            ],
            "description": "Message body."
        },
        "media_url": {
            "type": [
                "string",
                "null"
            ],
            "description": "Attached media URL."
        },
        "channel_type": {
            "type": [
                "string",
                "null"
            ],
            "description": "Message channel type."
        },
        "order_id": {
            "type": "integer",
            "description": "Order events: local order ID."
        },
        "customer_wa_id": {
            "type": "string",
            "description": "order.received: customer WhatsApp identifier."
        },
        "customer_name": {
            "type": [
                "string",
                "null"
            ],
            "description": "order.received: customer name."
        },
        "product_items": {
            "type": "array",
            "description": "order.received: incoming cart items.",
            "items": {
                "$ref": "#/components/schemas/OrderItem"
            }
        },
        "total_amount": {
            "type": "integer",
            "description": "order.received: total in integer hundredths."
        },
        "total_currency": {
            "type": "string",
            "description": "order.received: currency code."
        },
        "customer_note": {
            "type": [
                "string",
                "null"
            ],
            "description": "order.received: customer note."
        },
        "conversation_id": {
            "type": [
                "integer",
                "null"
            ],
            "description": "Order event: linked conversation ID."
        },
        "created_at": {
            "type": "string",
            "description": "order.received: creation time.",
            "format": "date-time"
        },
        "payment_id": {
            "type": "integer",
            "description": "order.paid: payment ID."
        },
        "method": {
            "type": "string",
            "description": "order.paid: payment method."
        },
        "amount_minor": {
            "type": "integer",
            "description": "order.paid: paid amount in minor units."
        },
        "currency": {
            "type": "string",
            "description": "order.paid: currency code."
        },
        "payer_msisdn": {
            "type": [
                "string",
                "null"
            ],
            "description": "order.paid: payer phone number."
        },
        "paid_at": {
            "type": [
                "string",
                "null"
            ],
            "description": "order.paid: settlement time.",
            "format": "date-time"
        },
        "group": {
            "$ref": "#/components/schemas/WhatsAppGroup",
            "description": "Current local WhatsApp group summary, when the event concerns a group."
        },
        "wa_ids": {
            "type": "array",
            "description": "Participant event: affected WhatsApp IDs.",
            "items": {
                "type": "string"
            }
        },
        "reason": {
            "type": [
                "string",
                "null"
            ],
            "description": "Participant event reason when supplied."
        },
        "applied": {
            "type": "object",
            "description": "group.updated: applied settings.",
            "additionalProperties": true
        },
        "errors": {
            "type": [
                "array",
                "object",
                "null"
            ],
            "description": "Provider/group error details.",
            "items": [],
            "additionalProperties": true
        },
        "sent": {
            "type": [
                "integer",
                "array"
            ],
            "description": "group.invite_sent: successfully sent invitations.",
            "items": []
        },
        "failed": {
            "type": [
                "integer",
                "array"
            ],
            "description": "group.invite_sent: failed invitations.",
            "items": []
        },
        "template_id": {
            "type": "integer",
            "description": "template.status_changed: local template id, for `GET /api/v3/whatsapp/templates/{template}`."
        },
        "name": {
            "type": "string",
            "description": "template.status_changed: the template name."
        },
        "language": {
            "type": "string",
            "description": "template.status_changed: the template language."
        },
        "whatsapp_business_account_id": {
            "type": "string",
            "description": "template.status_changed: the WhatsApp Business Account whose review moved."
        },
        "whatsapp_template_id": {
            "type": [
                "string",
                "null"
            ],
            "description": "template.status_changed: Meta's id on that account."
        },
        "previous_status": {
            "type": [
                "string",
                "null"
            ],
            "description": "template.status_changed: the status before this change."
        },
        "whatsapp_status": {
            "type": "string",
            "description": "template.status_changed: the status now.",
            "enum": [
                "pending",
                "in_review",
                "approved",
                "rejected",
                "disabled",
                "paused"
            ]
        },
        "rejection_reason": {
            "type": [
                "string",
                "null"
            ],
            "description": "template.status_changed: Meta's reason when the new status is rejected."
        }
    },
    "required": [
        "event",
        "timestamp"
    ],
    "description": "Actual flat ChannelWebhook payload. Message events include message_id/direction/sender/recipient/status/body/media_url/channel_type. Orders and groups supply their own fields. No data wrapper, tenant_id or occurred_at is added by this dispatcher."
}
A message arrived
{
    "event": "message.received",
    "message_id": 101,
    "direction": "inbound",
    "sender": "255712345678",
    "recipient": "MyBrand",
    "status": "received",
    "body": "Habari, mna kanga?",
    "media_url": null,
    "channel_type": "whatsapp",
    "timestamp": "2026-09-11T09:14:02+00:00"
}
A message was sent
{
    "event": "message.sent",
    "message_id": 102,
    "direction": "outbound",
    "sender": "MyBrand",
    "recipient": "255712345678",
    "status": "sent",
    "body": "Ndiyo, tuna kanga.",
    "media_url": null,
    "channel_type": "whatsapp",
    "timestamp": "2026-09-11T09:14:02+00:00"
}
A message was delivered
{
    "event": "message.delivered",
    "message_id": 102,
    "direction": "outbound",
    "sender": "MyBrand",
    "recipient": "255712345678",
    "status": "delivered",
    "body": "Ndiyo, tuna kanga.",
    "media_url": null,
    "channel_type": "whatsapp",
    "timestamp": "2026-09-11T09:14:02+00:00"
}
A message was read
{
    "event": "message.read",
    "message_id": 102,
    "direction": "outbound",
    "sender": "MyBrand",
    "recipient": "255712345678",
    "status": "read",
    "body": "Ndiyo, tuna kanga.",
    "media_url": null,
    "channel_type": "whatsapp",
    "timestamp": "2026-09-11T09:14:02+00:00"
}
A message failed
{
    "event": "message.failed",
    "message_id": 103,
    "direction": "outbound",
    "sender": "MyBrand",
    "recipient": "255712345678",
    "status": "failed",
    "body": "Ofa ya leo!",
    "media_url": null,
    "channel_type": "sms",
    "timestamp": "2026-09-11T09:14:02+00:00"
}
A message was sent from the phone itself
{
    "event": "message.echoed",
    "message_id": 104,
    "direction": "outbound",
    "sender": "MyBrand",
    "recipient": "255712345678",
    "status": "sent",
    "body": "Karibu!",
    "media_url": null,
    "channel_type": "whatsapp",
    "timestamp": "2026-09-11T09:14:02+00:00"
}
A message was edited
{
    "event": "message.updated",
    "message_id": 101,
    "direction": "inbound",
    "sender": "255712345678",
    "recipient": "MyBrand",
    "status": "received",
    "body": "Habari, mna kanga za bluu?",
    "media_url": null,
    "channel_type": "whatsapp",
    "timestamp": "2026-09-11T09:14:02+00:00"
}
An order arrived
{
    "event": "order.received",
    "order": {
        "id": 9182,
        "catalogue_id": 42,
        "platform": "whatsapp",
        "status": "pending",
        "needs_attention": false,
        "stock_policy": "external",
        "customer_handle": "255712345678",
        "customer_name": "Asha Mrisho",
        "customer_phone": "255712345678",
        "customer_note": null,
        "lines": [
            {
                "sku": "MNG-45W",
                "name": "Charger Mango 45W",
                "quantity": 1,
                "unit_price_minor": 3900000,
                "line_total_minor": 3900000,
                "currency": "TZS",
                "reserved": 1,
                "stock_short": false,
                "unresolved": false
            }
        ],
        "total_minor": 3900000,
        "currency": "TZS",
        "conversation_id": 771,
        "priced_at": "2026-09-11T09:14:02+00:00",
        "created_at": "2026-09-11T09:14:02+00:00",
        "order_id": 9182,
        "customer_wa_id": "255712345678",
        "total_amount": 3900000,
        "total_currency": "TZS"
    },
    "timestamp": "2026-09-11T09:14:02+00:00"
}
An order changed status
{
    "event": "order.status_changed",
    "order": {
        "id": 9182,
        "catalogue_id": 42,
        "platform": "whatsapp",
        "status": "confirmed",
        "needs_attention": false,
        "stock_policy": "external",
        "customer_handle": "255712345678",
        "customer_name": "Asha Mrisho",
        "customer_phone": "255712345678",
        "customer_note": null,
        "lines": [
            {
                "sku": "MNG-45W",
                "name": "Charger Mango 45W",
                "quantity": 1,
                "unit_price_minor": 3900000,
                "line_total_minor": 3900000,
                "currency": "TZS",
                "reserved": 1,
                "stock_short": false,
                "unresolved": false
            }
        ],
        "total_minor": 3900000,
        "currency": "TZS",
        "conversation_id": 771,
        "priced_at": "2026-09-11T09:14:02+00:00",
        "created_at": "2026-09-11T09:14:02+00:00",
        "order_id": 9182,
        "customer_wa_id": "255712345678",
        "total_amount": 3900000,
        "total_currency": "TZS"
    },
    "previous_status": "pending",
    "timestamp": "2026-09-11T09:14:02+00:00"
}
An order was cancelled
{
    "event": "order.cancelled",
    "order": {
        "id": 9182,
        "catalogue_id": 42,
        "platform": "whatsapp",
        "status": "cancelled",
        "needs_attention": false,
        "stock_policy": "external",
        "customer_handle": "255712345678",
        "customer_name": "Asha Mrisho",
        "customer_phone": "255712345678",
        "customer_note": null,
        "lines": [
            {
                "sku": "MNG-45W",
                "name": "Charger Mango 45W",
                "quantity": 1,
                "unit_price_minor": 3900000,
                "line_total_minor": 3900000,
                "currency": "TZS",
                "reserved": 1,
                "stock_short": false,
                "unresolved": false
            }
        ],
        "total_minor": 3900000,
        "currency": "TZS",
        "conversation_id": 771,
        "priced_at": "2026-09-11T09:14:02+00:00",
        "created_at": "2026-09-11T09:14:02+00:00",
        "order_id": 9182,
        "customer_wa_id": "255712345678",
        "total_amount": 3900000,
        "total_currency": "TZS"
    },
    "previous_status": "pending",
    "timestamp": "2026-09-11T09:14:02+00:00"
}
An order was paid
{
    "event": "order.paid",
    "order_id": 9182,
    "payment_id": 4410,
    "method": "ussd_push",
    "amount_minor": 3900000,
    "currency": "TZS",
    "payer_msisdn": "255712345678",
    "paid_at": "2026-09-11T09:14:02+00:00",
    "conversation_id": 771,
    "timestamp": "2026-09-11T09:14:02+00:00"
}
A bulk sync finished
{
    "event": "sync.completed",
    "sync": {
        "id": 812,
        "catalogue_id": 42,
        "source": "api",
        "mode": "upsert",
        "status": "completed",
        "received": 2000,
        "created": 12,
        "updated": 1982,
        "unchanged": 0,
        "rejected": 6,
        "retired": 0,
        "platforms": {
            "whatsapp": {
                "synced": 1960,
                "blocked": 34
            }
        },
        "problems_truncated": false
    },
    "timestamp": "2026-09-11T09:14:02+00:00"
}
A product is running low
{
    "event": "stock.low",
    "catalogue_id": 42,
    "sku": "MNG-45W",
    "name": "Charger Mango 45W",
    "available": 2,
    "threshold": 3,
    "timestamp": "2026-09-11T09:14:02+00:00"
}
A platform will not show a product
{
    "event": "product.blocked",
    "catalogue_id": 42,
    "sku": "MNG-KNIFE",
    "platform": "whatsapp",
    "problem": "WhatsApp needs a product image it can fetch.",
    "timestamp": "2026-09-11T09:14:02+00:00"
}
A platform's copy of a product differs from ours
{
    "event": "product.drifted",
    "catalogue_id": 42,
    "sku": "MNG-45W",
    "platform": "whatsapp",
    "source_of_truth": "api",
    "diff": {
        "price": {
            "ours": 4500000,
            "theirs": 4200000
        }
    },
    "timestamp": "2026-09-11T09:14:02+00:00"
}
A campaign finished
{
    "event": "campaign.completed",
    "campaign_uid": "cmp_8f2c",
    "sent": 1180,
    "failed": 20,
    "timestamp": "2026-09-11T09:14:02+00:00"
}
A campaign failed
{
    "event": "campaign.failed",
    "campaign_uid": "cmp_8f2c",
    "reason": "Insufficient balance.",
    "timestamp": "2026-09-11T09:14:02+00:00"
}
A group was created
{
    "event": "group.created",
    "group": {
        "id": 31,
        "subject": "Wateja wa Dar",
        "participants_count": 48
    },
    "timestamp": "2026-09-11T09:14:02+00:00"
}
A group could not be created
{
    "event": "group.create_failed",
    "group": {
        "id": 31,
        "subject": "Wateja wa Dar",
        "participants_count": 48
    },
    "timestamp": "2026-09-11T09:14:02+00:00"
}
A group was updated
{
    "event": "group.updated",
    "group": {
        "id": 31,
        "subject": "Wateja wa Dar",
        "participants_count": 48
    },
    "timestamp": "2026-09-11T09:14:02+00:00"
}
A group was deleted
{
    "event": "group.deleted",
    "group": {
        "id": 31,
        "subject": "Wateja wa Dar",
        "participants_count": 48
    },
    "timestamp": "2026-09-11T09:14:02+00:00"
}
A group was suspended by WhatsApp
{
    "event": "group.suspended",
    "group": {
        "id": 31,
        "subject": "Wateja wa Dar",
        "participants_count": 48
    },
    "timestamp": "2026-09-11T09:14:02+00:00"
}
A group suspension was lifted
{
    "event": "group.suspension_cleared",
    "group": {
        "id": 31,
        "subject": "Wateja wa Dar",
        "participants_count": 48
    },
    "timestamp": "2026-09-11T09:14:02+00:00"
}
Someone joined a group
{
    "event": "group.participant_joined",
    "group": {
        "id": 31,
        "subject": "Wateja wa Dar",
        "participants_count": 48
    },
    "timestamp": "2026-09-11T09:14:02+00:00"
}
Someone left a group
{
    "event": "group.participant_left",
    "group": {
        "id": 31,
        "subject": "Wateja wa Dar",
        "participants_count": 48
    },
    "timestamp": "2026-09-11T09:14:02+00:00"
}
Someone was removed from a group
{
    "event": "group.participant_removed",
    "group": {
        "id": 31,
        "subject": "Wateja wa Dar",
        "participants_count": 48
    },
    "timestamp": "2026-09-11T09:14:02+00:00"
}
Someone asked to join a group
{
    "event": "group.join_requested",
    "group": {
        "id": 31,
        "subject": "Wateja wa Dar",
        "participants_count": 48
    },
    "timestamp": "2026-09-11T09:14:02+00:00"
}
A join request was withdrawn
{
    "event": "group.join_request_revoked",
    "group": {
        "id": 31,
        "subject": "Wateja wa Dar",
        "participants_count": 48
    },
    "timestamp": "2026-09-11T09:14:02+00:00"
}
A group invite was sent
{
    "event": "group.invite_sent",
    "group": {
        "id": 31,
        "subject": "Wateja wa Dar",
        "participants_count": 48
    },
    "timestamp": "2026-09-11T09:14:02+00:00"
}
An endpoint was paused
{
    "event": "webhook.paused",
    "webhook_id": 7,
    "url": "https://store.example.com/momo",
    "consecutive_failures": 50,
    "timestamp": "2026-09-11T09:14:02+00:00"
}
A flow session started
{
    "event": "flow.session.started",
    "session_id": 5120,
    "flow_id": 17,
    "flow_name": "Oda ya chakula",
    "flow_version": 3,
    "conversation_id": 771,
    "contact_id": 2201,
    "contact_name": "Asha Mrisho",
    "contact_identifier": "255712345678",
    "trigger": "inbound",
    "status": "running",
    "outcome": "in_progress",
    "ended_reason": null,
    "node_id": "ask_name",
    "turns": 0,
    "started_at": "2026-09-11T09:14:02+00:00",
    "ended_at": null,
    "variables": [],
    "timestamp": "2026-09-11T09:14:02+00:00"
}
A flow session ended
{
    "event": "flow.session.ended",
    "session_id": 5120,
    "flow_id": 17,
    "flow_name": "Oda ya chakula",
    "flow_version": 3,
    "conversation_id": 771,
    "contact_id": 2201,
    "contact_name": "Asha Mrisho",
    "contact_identifier": "255712345678",
    "trigger": "inbound",
    "status": "completed",
    "outcome": "completed",
    "ended_reason": "completed",
    "node_id": "done",
    "turns": 6,
    "started_at": "2026-09-11T09:14:02+00:00",
    "ended_at": "2026-09-11T09:14:02+00:00",
    "variables": {
        "order_ref": "ORD-2026-0091",
        "total": 24000
    },
    "timestamp": "2026-09-11T09:14:02+00:00"
}

Responses

200Your receiver acknowledged the event. The current dispatcher does not retry based on receiver status or parse its response body.
receivedbooleanoptional
Illustrative acknowledgement chosen by your receiver.
{
    "received": true
}
default
{
    "received": true
}

API REFERENCE / Webhooks

Receive a signed automation business event

POST(your webhook URL)

Sent to the URL of a webhook event subscription. This is a different protocol from messageEvent communication callbacks. Verify X-Momo-Signature over timestamp + dot + raw body; the helper default timestamp tolerance is 300 seconds. The payload is serialized with unescaped Unicode and slashes. Respond with 2xx after durable acceptance. Up to six attempts use a 15-second HTTP timeout. Transport failures, 408, 429 and 5xx are retryable; other HTTP refusals are terminal. Default delays between attempts are 10,20,40,80,160 seconds. Positive numeric Retry-After overrides the delay, capped at 300 seconds; HTTP-date values are not parsed. Ten consecutive terminal delivery failures disable the subscription. The timestamp/signature is regenerated each attempt; deduplicate using event ID and subscription ID. Fan-out counts a webhook as delivered when it is queued and can reset subscription failure counters before the HTTP attempt; the receiver audit is the authoritative record of receipt.

Momo sends this request to your configured webhook URL. It is an incoming callback, not an API endpoint to call.

Header parameters

X-Momo-Signaturestringoptional
t=<unix seconds>,v1=<hex HMAC-SHA256 of timestamp + dot + raw body>. Use the subscription secret, constant-time comparison and a timestamp tolerance.

Example: t=1918015200,v1=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

X-Momo-Eventstringrequired
Business event key.

Example: record.created

X-Momo-Event-Idstringrequired
Stable event ID across retry attempts.

Example: 01953b60-4ce0-7000-8000-000000000001

X-Momo-Subscriptionstringrequired
Subscription receiving this event.

Example: 12

X-Momo-Attemptstringrequired
One-based queue attempt number.

Example: 1

Request body

application/json · required

idstringrequired
Stable business event UUID; retain to deduplicate each subscription delivery.
format
uuid
eventstringrequired
Known business event key. Use event_keys from the event read API to discover live publishers.

Example: record.created

occurred_atstring | nullrequired
Time the business event occurred, distinct from the signature delivery timestamp.
format
date-time
tenant_idintegerrequired
Account that owns this business event.
subjectobjectrequired
Business resource the event concerns.
Show child properties
typestring | nullrequired
Resource family, such as data_record.
idstring | nullrequired
Resource identifier within that family.
actorobject | arrayrequired
Publisher-provided actor attribution; shape varies by event source.
additionalProperties
true
dataobject | arrayrequired
Publisher-provided event payload; shape varies by event key.
additionalProperties
true
subscriptionobjectrequired
Subscription that caused this delivery.
Show child properties
idintegerrequired
Local event subscription ID.
labelstringrequired
Subscription label at delivery time.
Complete request schema
{
    "type": "object",
    "description": "Automation subscription delivery envelope. Verify the timestamped X-Momo-Signature before parsing the raw JSON bytes.",
    "required": [
        "id",
        "event",
        "occurred_at",
        "tenant_id",
        "subject",
        "actor",
        "data",
        "subscription"
    ],
    "properties": {
        "id": {
            "type": "string",
            "description": "Stable business event UUID; retain to deduplicate each subscription delivery.",
            "format": "uuid"
        },
        "event": {
            "type": "string",
            "description": "Known business event key. Use event_keys from the event read API to discover live publishers.",
            "example": "record.created"
        },
        "occurred_at": {
            "type": [
                "string",
                "null"
            ],
            "description": "Time the business event occurred, distinct from the signature delivery timestamp.",
            "format": "date-time"
        },
        "tenant_id": {
            "type": "integer",
            "description": "Account that owns this business event."
        },
        "subject": {
            "type": "object",
            "description": "Business resource the event concerns.",
            "properties": {
                "type": {
                    "type": [
                        "string",
                        "null"
                    ],
                    "description": "Resource family, such as data_record."
                },
                "id": {
                    "type": [
                        "string",
                        "null"
                    ],
                    "description": "Resource identifier within that family."
                }
            },
            "required": [
                "type",
                "id"
            ]
        },
        "actor": {
            "type": [
                "object",
                "array"
            ],
            "description": "Publisher-provided actor attribution; shape varies by event source.",
            "additionalProperties": true,
            "items": []
        },
        "data": {
            "type": [
                "object",
                "array"
            ],
            "description": "Publisher-provided event payload; shape varies by event key.",
            "additionalProperties": true,
            "items": []
        },
        "subscription": {
            "type": "object",
            "description": "Subscription that caused this delivery.",
            "properties": {
                "id": {
                    "type": "integer",
                    "description": "Local event subscription ID."
                },
                "label": {
                    "type": "string",
                    "description": "Subscription label at delivery time."
                }
            },
            "required": [
                "id",
                "label"
            ]
        }
    }
}
Business event envelope; publisher data varies
{
    "id": "01953b60-4ce0-7000-8000-000000000001",
    "event": "record.created",
    "occurred_at": "2030-10-12T06:00:00+00:00",
    "tenant_id": 42,
    "subject": {
        "type": "data_record",
        "id": "01953b60-4ce0-7000-8000-000000000002"
    },
    "actor": {
        "kind": "api",
        "label": "ERP integration",
        "id": 7
    },
    "data": {
        "table": {
            "id": "01953b60-4ce0-7000-8000-000000000003",
            "name": "Customers",
            "slug": "customers"
        },
        "record_id": "01953b60-4ce0-7000-8000-000000000002",
        "record": {
            "name": "Example"
        },
        "source": "api"
    },
    "subscription": {
        "id": 12,
        "label": "Forward record changes"
    }
}
template.status_changed — Meta approved a template
{
    "event": "template.status_changed",
    "template_id": 418,
    "name": "order_shipped",
    "language": "sw",
    "category": "utility",
    "whatsapp_business_account_id": "102290129340398",
    "whatsapp_template_id": "1189456212345678",
    "previous_status": "in_review",
    "whatsapp_status": "approved",
    "rejection_reason": null,
    "timestamp": "2026-09-13T09:14:02+00:00"
}

Responses

200Receiver has durably accepted the event. Any 2xx response is acknowledged as successful.

For AI assistants

Connect an AI to your account

Which should I use?

REST API

Your own code decides what to call — a cron job, a webhook handler, your backend. You know the request before you deploy, so a fixed contract is exactly what you want.

Your code holds the wheel Jump to the operations
MCP

A language model decides at run time — Claude, ChatGPT, an agent you built yourself. It picks from whatever tools/list told it, which is why the tool list is negotiated rather than compiled in.

A model holds the wheel Jump to the servers

They reach the same data and enforce the same permissions. What differs is who is holding the wheel — and nothing stops you using both on one account.

Connect Claude, ChatGPT or your own agent to your Momo Business account. It can build call flows, write WhatsApp conversations, generate voice recordings, and read your orders, tickets and numbers — with the permissions you choose, and nothing more.

You stay in control

Drafting, publishing, sending, spending, and deletion have separate grants. A connection is limited by both the access you grant and your current account permissions. Payments that require handset approval still need that approval, and you can disconnect at any time.

Quickstart

  1. Create a connection: Dashboard → Settings → API credentials → MCP connections.
  2. Paste the config block it gives you into your AI client.
  3. Ask for something — "show me my call flows", say.

Read the full guide to OAuth, session initialization, tool discovery, and troubleshooting.

curl -X POST https://business.momo.tz/mcp \
  -H 'Authorization: Bearer momo_mcp_…' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
https://business.momo.tz/mcp

One URL carrying every tool your connection was granted — up to 288 of them. Use this one for Claude and ChatGPT, which accept a single URL per connector. What you tick on the consent screen is what narrows the list.

The servers 29 · 288 tools

Each area also has its own URL, for a connection you want deliberately narrow — an agent that only builds IVRs, say. For normal use, connect /mcp above instead.

Every tool carries a version: eight characters that change when — and only when — its name, description or arguments change. The registry as a whole is 7e6437b5 right now. The same strings come back in annotations.version on tools/list, so a cached definition can be checked rather than trusted.

IVR /mcp/v1/ivr 11 read · 11 write

Build and edit call flows: read the graph, apply node operations, validate, simulate, version and assign to numbers.

  • get_ivr_call_steps v1828ab0c The steps one caller actually went through inside a call flow: which node they reached, what they pressed, and where the call ended. Use this to diagnose a real call rather than re-reading the flow definition — a flow can validate perfectly and still lose callers.

    Required permissions: calls.ivr-steps.view

    call_referencestring · required limitinteger orderstring
    Argument schema and validation
    call_referencestringrequired
    The call's room name, from the calls tools.
    limitintegeroptional
    Max steps to return (default 50, max 500).
    orderstringoptional
    Step order. Default asc, which reads the way the call happened.
    enum
    ["asc","desc"]
  • get_ivr_catalog vff05c216 The IVR building reference: every node kind you may use, the exact fields each one allows, which action dialect it speaks, and the resource ids that actually exist on this account (agents, models, voices, SMS senders, audio assets). ALWAYS call this before your first apply_ivr_ops — inventing a field or an id is the most common way a batch is rejected.

    Required permissions: ivr.view

  • get_ivr_data_tables va4e69064 The data tables a call flow reads or writes — which node touches which table and how — plus any tables pinned to its View data page. Use it before editing a data node, or when the user asks where a flow keeps its records; get_data_table_schema then gives the columns.

    Required permissions: ivr.view

    flow_idinteger · required
    Argument schema and validation
    flow_idintegerrequired
    The flow whose tables you want.
  • get_ivr_flow v87fd2010 Read one call flow: every node, where each one sends the caller, the entry point, the current version number, and what the IVR engine validator says about it right now. A flow too big to answer in one call comes back as an overview that names the sections to ask for next — pass section and cursor to walk it. Read this before proposing edits, and pass the version back to apply_ivr_ops.

    Required permissions: ivr.view

    flow_idinteger · required sectionstring cursorstring summaryboolean node_idsstring expected_versioninteger
    Argument schema and validation
    flow_idintegerrequired
    The flow id, from list_ivr_flows.
    sectionstringoptional
    Which piece to read: overview, nodes, transitions, errors or warnings. Leave it out to get the whole flow in one answer, which is what happens whenever it fits.
    cursorstringoptional
    Where to resume a section: the next_cursor the previous page returned, verbatim. It carries the graph snapshot with it, so a flow that is edited or rolled back mid-read is refused rather than stitched together. Omit it, or pass "0", for the first page — any LATER page must carry the snapshot, so build the cursor from next_cursor rather than from page.from.
    summarybooleanoptional
    On section "nodes", return id/type/name/targets/fields instead of whole nodes — the whole graph in far fewer pages.
    node_idsstringoptional
    Node ids to read in full, comma-separated ("entry,menu_malipo") or as a list. Ids that are not on the flow come back in not_found. This pages like any section: if the nodes asked for do not fit one answer, send the SAME node_ids back with the next_cursor.
    expected_versionintegeroptional
    Optional guard: refuse if the draft counter is not this. Paging does not need it — the cursor already pins the graph — and the counter also moves on a rename or a canvas drag, so passing it on every page can abandon a walk for an edit that changed no node.
  • lint_ivr_expression v7b4acfe9 Check a single branch condition or variable path against the flow expression language, without touching a flow. Use it before putting an expression into a node — a rejected batch tells you the graph was refused, this tells you which expression and why.

    Required permissions: ivr.view

    expressionstring · required kindstring
    Argument schema and validation
    expressionstringrequired
    The expression to check, e.g. vars.balance > 1000 — the same text a branch condition holds.
    kindstringoptional
    "expression" for a condition (default); "assignment_path" for the left-hand side of a set, like vars.customer.name.
    enum
    ["expression","assignment_path"]
  • list_ivr_assignments ve02dc8f3 Which phone numbers answer with which call flow. Read this before saying a flow is live: publishing makes a flow available, assigning it to a number is what makes a caller hear it, and the two are separate steps.

    Required permissions: ivr.view

  • list_ivr_flows vd1ab45b8 List the call (IVR) flows on this account: name, status, size, whether it has unpublished changes, and when it last changed. Start here before editing anything.

    Required permissions: ivr.view

    statusstring searchstring limitinteger
    Argument schema and validation
    statusstringoptional
    Filter by status: draft, active or paused.
    searchstringoptional
    Filter by name.
    limitintegeroptional
    Max flows to return (default 25, max 100).
  • list_ivr_resources vece02c64 List the things a call-flow node can point at: queues (queue.queueId), schedules and holiday calendars (decision conditions), SMS / email / payment / speech / webhook profiles, HTTP profiles (http_request.profileId, webhook_notify.profileId), database connections (sql_query.datasourceId), recorded audio (a prompt's assetId), and the do-not-call and VIP lists. Call this before writing any node that carries an id — the ids are real and must be copied, never invented.

    Required permissions: ivr.view

    kindstring · required refreshboolean
    Argument schema and validation
    kindstringrequired
    Which family to list. queues for a queue node, http_profiles for http_request and webhook_notify, datasources for sql_query, payment_profiles for pay, audio_assets for a prompt that plays a recording instead of speaking, and so on.
    enum
    ["queues","schedules","holiday_calendars","experiments","sms_profiles","email_profiles","payment_profiles","stt_profiles","webhook_profiles","http_profiles","datasources","audio_assets","dnc","vip"]
    refreshbooleanoptional
    Re-read from the telephony service first. Slower; use it when the user says they just created something and you cannot see it.
  • list_ivr_versions vbb5f5d85 The version history of a call flow, newest first: each publish and each rollback, who did it and when, and which snapshot the phone system is running. Use it before rollback_ivr_flow, or when the user asks what changed and when.

    Required permissions: ivr.view

    flow_idinteger · required limitinteger
    Argument schema and validation
    flow_idintegerrequired
    The flow whose history you want.
    limitintegeroptional
    Default 25, max 100.
  • simulate_ivr_flow vb8034762 Walk a published call flow the way a caller would and get back the step-by-step transcript the web simulator shows — prompts played, digits taken, where the call ended. No phone rings. Pass digits_json for a quick keypad walk, or script_json for the full timeline (speech, hangups, transfer outcomes). The flow must have been published at least once.

    Required permissions: ivr.simulate

    flow_idinteger · required digits_jsonstring script_jsonstring
    Argument schema and validation
    flow_idintegerrequired
    The flow to simulate.
    digits_jsonstringoptional
    A JSON array of key presses in order, e.g. ["1","3"]. Each is pressed two seconds after the last.
    script_jsonstringoptional
    Instead of digits: the full simulator script {"ctx":{...},"timeline":{"inputs":[{"at":ms,"type":"dtmf|speech|hangup|silence",...}],"transfers":[...],"outbound":{...}}}.
  • validate_ivr_flow vc43a91e7 Check a flow against the real IVR engine validator without changing anything. Pass ops_json to test a batch BEFORE applying it — useful when you are unsure and would rather not write a draft you have to undo.

    Required permissions: ivr.view

    flow_idinteger · required ops_jsonstring
    Argument schema and validation
    flow_idintegerrequired
    The flow to check.
    ops_jsonstringoptional
    Optional {"ops":[...]} to test against the flow without writing.
  • apply_ivr_ops v370cd99e writes Build or edit a call flow by applying graph operations to its draft. The whole batch is checked by the real IVR engine validator before anything is written, and the result appears immediately on the canvas if the user has it open. The flow stays a DRAFT — publishing is the user's.

    Required permissions: ivr.edit

    flow_idinteger · required ops_jsonstring · required expected_versioninteger auto_layoutboolean
    Argument schema and validation
    flow_idintegerrequired
    The flow to edit, from list_ivr_flows.
    ops_jsonstringrequired
    A JSON object string {"ops":[...]}. Each op is {"op":"add_node","node":{...}} | {"op":"update_node","id":"...","set":{...}} | {"op":"remove_node","id":"..."} | {"op":"set_entry","id":"..."}. Max 30. Call get_ivr_catalog first for the node kinds and fields.
    expected_versionintegeroptional
    The version you read in get_ivr_flow. Strongly recommended: it is what stops you overwriting a change somebody else made in the meantime.
    auto_layoutbooleanoptional
    Arrange the canvas as a tidy tree after applying (default true). Set false only if you are placing nodes yourself with format_ivr_layout.
  • assign_ivr_to_number v0926e0e3 writes Make a phone number answer with a call flow, or clear it. This reaches REAL callers the moment it succeeds — the next person to ring that number hears the new flow. The flow must be published first. Pass flow_id: null to clear a number.

    Required permissions: ivr.edit

    numberstring · required flow_idinteger modestring
    Argument schema and validation
    numberstringrequired
    The phone number in international format, e.g. +255752771650. From list_ivr_assignments or list_my_numbers.
    flow_idintegeroptional
    The call flow to put on the number. Omit or pass null to clear the number so it answers with no flow.
    modestringoptional
    How the number runs the flow. Leave unset to keep what it already uses.
    enum
    ["inherit_flow","traditional","ai_assisted"]
  • create_ivr_flow va6159aad writes Create a new call flow, optionally building its whole graph in the same call by passing operations. It appears on the user's flow list immediately, as a draft.

    Required permissions: ivr.create

    namestring · required ops_jsonstring
    Argument schema and validation
    namestringrequired
    What to call the flow, e.g. "Main line".
    ops_jsonstringoptional
    Optional {"ops":[...]} to build the graph in the same call. Same shape as apply_ivr_ops.
  • delete_ivr_flow v48a62902 writes Delete a call flow permanently. Refuses while any phone number still routes to it, and names those numbers — deleting a flow a live number points at would drop real calls.

    Required permissions: ivr.delete

    flow_idinteger · required confirmstring confirm_unverifiedstring
    Argument schema and validation
    flow_idintegerrequired
    The flow to delete.
    confirmstringoptional
    The exact flow name, as confirmation. Ask the user before sending this.
    confirm_unverifiedstringoptional
    Only when the phone system was unreachable and the user has explicitly accepted the risk: "yes".
  • format_ivr_layout v4fa53b3d writes Tidy the canvas — arrange the nodes as a readable tree, the same as the builder's "Format layout" button. Call this after building or reshaping a flow, or the user opens a pile of overlapping boxes. You can steer it: direction (down the page or across it), spacing (compact / normal / roomy), and only_ids to straighten just a few nodes and leave the rest where the user put them. positions_json places every node yourself.

    Required permissions: ivr.edit

    flow_idinteger · required directionstring spacingstring only_idsarray positions_jsonstring expected_versioninteger
    Argument schema and validation
    flow_idintegerrequired
    The flow to arrange.
    directionstringoptional
    Which way the call reads: "top_to_bottom" (default, and what the builder does) or "left_to_right" for a wide flow with few branches.
    enum
    ["top_to_bottom","left_to_right"]
    spacingstringoptional
    How far apart: "compact" to fit a big flow on one screen, "normal" (default), or "roomy" when the user says it looks cramped.
    enum
    ["compact","normal","roomy"]
    only_idsarray<string>optional
    Straighten just these node ids and leave every other node exactly where it is. They are placed relative to the corner they already occupy, so the rest of the canvas does not appear to move. Omit to tidy the whole flow.
    positions_jsonstringoptional
    Optional explicit placement: a JSON object of node id => {"x":123,"y":456}. Omit it to auto-arrange, which is what you usually want. Overrides direction/spacing/only_ids.
    expected_versionintegeroptional
    The version from get_ivr_flow.
  • pin_ivr_data_table vab02a647 writes Pin a data table to a call flow's View data page so the team sees its records next to the flow. It changes what the page shows and nothing else: no node, no access for the flow. Use when the user wants a table kept in view alongside this flow.

    Required permissions: ivr.edit

    flow_idinteger · required table_idstring · required
    Argument schema and validation
    flow_idintegerrequired
    The flow to pin the table to.
    table_idstringrequired
    The data table id (uuid), from list_data_tables.
  • publish_ivr_flow vf4cf0d45 writes Make a call flow LIVE on this business's phone lines. Real callers reach it immediately. Only use when the user has explicitly asked to publish — building and validating never require this.

    Required permissions: ivr.publish

    flow_idinteger · required
    Argument schema and validation
    flow_idintegerrequired
    The flow to publish.
  • rollback_ivr_flow v0faea550 writes Restore an earlier version of a call flow INTO THE DRAFT, replacing whatever is on the canvas now. Callers are not affected until the user publishes — this is how the IVR builder undoes a bad edit. Use when the user asks to go back to a previous version; list_ivr_versions gives the ids.

    Required permissions: ivr.edit

    flow_idinteger · required version_idinteger · required confirmboolean
    Argument schema and validation
    flow_idintegerrequired
    The flow to roll back.
    version_idintegerrequired
    The version id to restore, from list_ivr_versions.
    confirmbooleanoptional
    Set true only after the user has agreed that the current draft is replaced.
  • unpin_ivr_data_table ved94d588 writes Take a hand-pinned data table off a call flow's View data page. Only pins go: a table a node actually reads or writes stays listed until that node is removed. Use when the user no longer wants the table shown with this flow.

    Required permissions: ivr.edit

    flow_idinteger · required table_idstring · required
    Argument schema and validation
    flow_idintegerrequired
    The flow to unpin the table from.
    table_idstringrequired
    The data table id (uuid) that was pinned.
  • update_ivr_flow v3ffaef4a writes Rename a call flow, or change whether it is a draft, active or paused. Renaming is safe and reversible; pausing an active flow stops it answering, so say what you are about to do first.

    Required permissions: ivr.edit

    flow_idinteger · required namestring statusstring confirm_pauseboolean expected_versioninteger
    Argument schema and validation
    flow_idintegerrequired
    The flow to change.
    namestringoptional
    A new name.
    statusstringoptional
    draft, active or paused.
    confirm_pausebooleanoptional
    Required to pause a flow that is currently live.
    expected_versionintegeroptional
    The version from get_ivr_flow.
  • upsert_ivr_resource v5b20a925 writes Create or update the resources a call-flow node points at: queues, schedules, holiday calendars, A/B experiments, SMS / email / payment / speech / webhook provider profiles, and HTTP profiles. Pass the id to change an existing one, omit it to create. Everything else about the flow stays a draft — this only makes the resource exist so a node can reference it.

    Required permissions: ivr.edit

    kindstring · required namestring idstring config_jsonstring
    Argument schema and validation
    kindstringrequired
    What to create or change. list_ivr_resources shows what already exists.
    enum
    ["queues","schedules","holiday_calendars","experiments","http_profiles","sms_profiles","email_profiles","payment_profiles","stt_profiles","webhook_profiles"]
    namestringoptional
    What to call it. Required when creating; omit to leave an existing name alone.
    idstringoptional
    The id of an existing resource, from list_ivr_resources. Omit to create a new one.
    config_jsonstringoptional
    A JSON object of the rest of its settings. queues: maxConcurrent, priorityMode (fifo|priority), skills, holdMusicAssetId. schedules: timezone and slots are BOTH required, plus holidayCalendarId. holiday_calendars: timezone, dates. experiments: variants (at least two, each {"name":"...","weight":50}), status, stickyByCaller. *_profiles: provider, config. http_profiles: method and url are required, plus headers, queryParams, timeoutMs, maxResponseChars, enabled.
Message flows /mcp/v1/flows 13 read · 19 write

Build and edit WhatsApp conversation flows: nodes, edges, triggers, validation, simulation and analytics.

  • diff_message_flow_versions v3bee5179 What changed between two versions of a message flow — steps added, removed and changed field by field, plus triggers and settings. Leave `to` out to compare a published version against the current draft, which is what somebody about to publish wants to know. Ids come from list_flow_versions.

    Required permissions: flows.view

    flow_idinteger · required from_version_idinteger · required to_version_idinteger
    Argument schema and validation
    flow_idintegerrequired
    The flow whose versions you are comparing.
    from_version_idintegerrequired
    The OLDER side, from list_flow_versions.
    to_version_idintegeroptional
    The newer side. Leave it out to compare against the current draft.
  • get_flow_catalog v52d5fb1a The message-flow building reference: every node kind, the config keys it takes and what each means, the named exits ("outs") each one leaves by, whether it waits for a reply, and whether it can actually be published yet. Also the templating rule and the WhatsApp limits you must design within. ALWAYS read this before your first apply_flow_ops.

    Required permissions: flows.view

    topicstring
    Argument schema and validation
    topicstringoptional
    Ask for one deep dive instead of the whole catalog: transaction (the transaction and lock block nodes), allocate, rule, transition, approval, collect_payment or record_trigger. Omit it for the full catalog, which lists these under "topics".
  • get_flow_data_tables v988ee1e7 The data tables a message flow reads or writes — which node touches which table and how — plus any tables pinned to its View data page. Use it before editing a data node, or when the user asks where a flow keeps its records; get_data_table_schema then gives the columns.

    Required permissions: flows.view

    flow_idinteger · required
    Argument schema and validation
    flow_idintegerrequired
    The flow whose tables you want.
  • get_flow_session v4d4b645c One customer's walk through a message flow: the session, its outcome, and the redacted step trace — which node, which exit, which error, when. Variables only with include_variables, only for a connection that may edit flows, and the read is audited.

    Required permissions: flows.view

    session_idinteger · required include_variablesboolean
    Argument schema and validation
    session_idintegerrequired
    The session, from list_flow_sessions.
    include_variablesbooleanoptional
    Also return the session variables (redacted). Needs flows.edit; the read is audited.
  • get_message_flow v04f29c16 Read one message flow: nodes, edges, triggers, its version number, and what the validator currently says. A flow too big to answer in one call comes back as an overview that names the sections to ask for next — pass section and cursor to walk it, and expected_version to be sure every page is the same draft. Pass the version back to apply_flow_ops so you do not overwrite somebody else.

    Required permissions: flows.view

    flow_idinteger · required sectionstring cursorstring summaryboolean node_idsstring expected_versioninteger
    Argument schema and validation
    flow_idintegerrequired
    The flow id, from list_message_flows.
    sectionstringoptional
    Which piece to read: overview, nodes, edges, variables, issues or triggers. Leave it out to get the whole flow in one answer, which is what happens whenever it fits.
    cursorstringoptional
    Where to resume a section: the next_cursor the previous page returned, verbatim. It carries the graph snapshot with it, so a flow that is edited or rolled back mid-read is refused rather than stitched together. Omit it, or pass "0", for the first page.
    summarybooleanoptional
    On section "nodes", return id/kind/name/outs/exits instead of full config — the whole graph in far fewer pages.
    node_idsstringoptional
    Comma-separated node ids to read in full, e.g. "start,pay_collect_v12". Ids that are not on the flow come back in not_found.
    expected_versionintegeroptional
    Optional guard: refuse if the draft counter is not this. Paging does not need it — the cursor already pins the graph — and the counter also moves on a rename or a canvas drag, so passing it on every page can abandon a walk for an edit that changed no node.
  • get_message_flow_insights v1525064a How a message flow is performing with real customers over a period: sessions started, completed and failed, why they ended, and the drop-off per node — where conversations stop. Use it when the user asks whether a flow works, which step loses people, or what to fix first.

    Required permissions: flows.view

    flow_idinteger · required rangestring
    Argument schema and validation
    flow_idintegerrequired
    The flow to report on.
    rangestringoptional
    How far back: "7d", "30d" (default), "90d", "2w" or a number of days, max 365.
  • list_flow_sessions v86d6e0a9 The conversations customers have had with a message flow: who, which step they are at, how it ended. Filter by phone, status, outcome, step, date or version. Session ids from here go to get_flow_session, end_flow_session, nudge_flow_session and replay_flow_session. Never carries variables.

    Required permissions: flows.view

    flow_idinteger · required phonestring statusstring outcomestring nodestring fromstring tostring versioninteger limitinteger cursorstring
    Argument schema and validation
    flow_idintegerrequired
    The flow whose conversations to list.
    phonestringoptional
    A customer phone number in any shape (0712…, 255712…, +255…). Exact match after normalising.
    statusstringoptional
    running, waiting, completed, failed, expired, superseded_by_human or cancelled.
    outcomestringoptional
    live, completed, handed_over, dead_end, abandoned, gave_up, error, switched_off or ended_by_staff.
    nodestringoptional
    Only sessions currently at this step id.
    fromstringoptional
    Started on or after this date (Y-m-d).
    tostringoptional
    Started on or before this date (Y-m-d).
    versionintegeroptional
    Only sessions pinned to this version id.
    limitintegeroptional
    Rows per page, 1–50 (default 20).
    cursorstringoptional
    next_cursor from the previous page.
  • list_flow_versions v6ae09688 The publish history of a message flow, newest first: every version that has been live, who published it, when, and which one customers are on right now. Use it before rollback_message_flow, or when the user asks what changed and when.

    Required permissions: flows.view

    flow_idinteger · required limitinteger
    Argument schema and validation
    flow_idintegerrequired
    The flow whose history you want.
    limitintegeroptional
    Default 25, max 100.
  • list_message_flows v91dff075 List the WhatsApp conversation flows on this account, with status, priority, how many triggers each has and whether it is actually live for customers.

    Required permissions: flows.view

    statusstring searchstring limitinteger
    Argument schema and validation
    statusstringoptional
    draft, active, paused or archived.
    searchstringoptional
    Filter by name.
    limitintegeroptional
    Default 25, max 100.
  • replay_flow_session vffaef2ac Take a conversation that really happened and re-run its turns against the flow's CURRENT draft, then say where the two paths part company. Nothing is sent: every emitter is faked, the run is marked simulated and it is rolled back. Use it to answer "would the fix have helped this customer?" — session ids come from get_message_flow_insights.

    Required permissions: flows.simulate

    session_idinteger · required flow_idinteger variables_jsonstring
    Argument schema and validation
    session_idintegerrequired
    The session to replay.
    flow_idintegeroptional
    Optional: refuse if the session is not on this flow.
    variables_jsonstringoptional
    Optional JSON object of starting variables, replacing the seed the session was recorded with — to ask "and if it had started with this?".
  • run_flow_scenarios veb984b93 Run the tests written against a message flow and report which expectations held. Every provider is faked and the whole run is rolled back, so nothing is sent, charged or saved. An ENABLED test that fails also refuses the next publish — so run this before publish_message_flow and tell the user what failed.

    Required permissions: flows.simulate

    flow_idinteger · required scenario_idinteger enabled_onlyboolean
    Argument schema and validation
    flow_idintegerrequired
    The flow whose tests to run.
    scenario_idintegeroptional
    Run just this one test. Leave it out to run them all.
    enabled_onlybooleanoptional
    Default true — only the tests that can refuse a publish. False runs the switched-off ones too.
  • simulate_message_flow v83fd539b Drive a scripted conversation through the flow and get back exactly what a customer would see. Nothing is sent to anyone. This is how you check your own work before handing it over — use it.

    Required permissions: flows.simulate

    flow_idinteger · required inbound_jsonstring variables_jsonstring contact_jsonstring window_openboolean sealedboolean fakes_jsonstring
    Argument schema and validation
    flow_idintegerrequired
    The flow to simulate.
    inbound_jsonstringoptional
    The customer's turns, in order, as a JSON array. A turn is {"text":"what they typed"} or {"choice":"the button or list-row ID they tapped"} — the id, not its label — or both. A bare string is shorthand for text, so ["hi","1","Amina"] works. {"timeout":true} is the clock instead of the customer: it takes the timeout branch of a question nobody answered. A photo, a location, a cart or a form answer cannot be scripted — step past that wait with a fakes_json entry on the node. Anything else is refused by name rather than skipped.
    variables_jsonstringoptional
    A JSON object of starting vars, e.g. {"group_code":"KATORO1234"} — what the trigger would have carried in.
    contact_jsonstringoptional
    Who is texting, as {"phone":"255700000001","name":"Test"}. Use a test number: the phone is what {{contact.phone}} and every record lookup keyed on it will see.
    window_openbooleanoptional
    Whether the 24-hour WhatsApp window is open (default true). False makes the run take the template path instead.
    sealedbooleanoptional
    Answer for every node that would reach the network — http_request, webhook_notify and the AI kinds — instead of letting it out (default true). Set false only to watch a real agent answer.
    fakes_jsonstringoptional
    Scripted answers for particular nodes, as {"<node_id>":{"out":"yes","set":{"vars.x":1}}} or {"kind:http_request":{"fail":"provider_failure"}}. Use this to drive a payment, an API or an approval down a chosen branch.
  • validate_message_flow v72c6f892 Is this draft publishable? The graph's own errors and warnings (the same the builder shows), plus go-live readiness: whether the ACCOUNT can run it — a WhatsApp number, a template for the 24-hour fallback, a catalogue, a payment gateway, a schedule, the classifier, AI profiles, a trigger. Read this before telling a user to publish.

    Required permissions: flows.view

    flow_idinteger · required
    Argument schema and validation
    flow_idintegerrequired
    The flow to check.
  • apply_flow_ops v65b46727 writes Build or edit a WhatsApp conversation flow by applying graph operations to its draft. Checked by the real flow validator before anything is written, and the user's canvas updates immediately. The flow stays a DRAFT — you cannot make it reach customers.

    Required permissions: flows.edit

    flow_idinteger · required ops_jsonstring · required expected_versioninteger auto_layoutboolean
    Argument schema and validation
    flow_idintegerrequired
    The flow to edit.
    ops_jsonstringrequired
    A JSON object string {"ops":[...]}. Ops: add_node, update_node, remove_node, set_edge {from,out,to}, remove_edge {from,out}, set_entry. Max 40. Call get_flow_catalog first — an edge "out" must be one the node kind actually has.
    expected_versionintegeroptional
    The version from get_message_flow. Stops you overwriting somebody else.
    auto_layoutbooleanoptional
    Arrange the canvas after applying (default true). Set false only if you are placing nodes yourself.
  • create_flow_scenario v529db7b0 writes Write a test against a message flow: the customer's turns, the answers to fake for steps that reach outside, and what should be true at the end. An ENABLED test that fails refuses the next publish, so write one for the path that matters most — usually the one that takes money. Run it with run_flow_scenarios.

    Required permissions: flows.edit

    flow_idinteger · required namestring · required kindstring script_jsonstring expectations_jsonstring enabledboolean
    Argument schema and validation
    flow_idintegerrequired
    The flow to write the test against.
    namestringrequired
    What the customer is doing, e.g. "Pays with an expired prompt".
    kindstringoptional
    A label for the list: happy_path, validation_failure, timeout, duplicate, payment_failure, handoff, custom (default happy_path).
    script_jsonstringoptional
    What happens, as a JSON object. turns: the customer's turns in order — a bare string is a text, {"choice":"yes"} is a tap by button or row ID, {"timeout":true} is the wait running out. fakes: {"<node_id>": {"out": "expired"}} or {"kind:http_request": {"out": "failed", "set": {"stock": 0}}} — the answer a step that reaches outside should give (every other such step takes its first exit). variables: starting variables. trigger: {"variables": {"record": {...}}} for a record- or event-started flow. contact: {"phone", "name"}. window_open: false to run outside the 24-hour window.
    expectations_jsonstringoptional
    What should be true at the end, as a JSON object of ends_at, status, ended_reason, variables, unset, sent_contains, never_sent, sent_count, reached, not_reached, faked_reached, exits, turns. exits is [{"node","out","visit"?}] (visit picks the nth pass through a loop); turns is a list, one per scripted turn, each {"expect": {"sent_contains": [...], "never_sent": [...], "offered_choices": ["yes","no"]}} judged on the replies to that turn. A fake on a step the run never reaches fails the test unless the step is named in faked_reached.
    enabledbooleanoptional
    Whether a failure refuses the next publish (default true).
  • create_message_flow v0572b768 writes Create a new WhatsApp conversation flow. It starts as a draft with one node, appears on the user's list immediately, and reaches nobody until they publish it.

    Required permissions: flows.create

    namestring · required descriptionstring first_messagestring idempotency_keystring
    Argument schema and validation
    namestringrequired
    What to call the flow, e.g. "Ordering".
    descriptionstringoptional
    One line on what it does.
    first_messagestringoptional
    The opening message. Defaults to a Kiswahili greeting.
    idempotency_keystringoptional
    Any string you choose. Send the same key on a retry and you get the same flow back instead of a second one (kept for 24 hours).
  • delete_flow_scenario v67663561 writes Delete a test written against a message flow. Gone for good — to stop a test refusing publishes without losing it, switch it off with update_flow_scenario instead.

    Required permissions: flows.edit

    flow_idinteger · required scenario_idinteger · required
    Argument schema and validation
    flow_idintegerrequired
    The flow the test belongs to.
    scenario_idintegerrequired
    The test to delete.
  • delete_message_flow v32744bed writes Archive a message flow — the same thing the builder's Delete does: it stops answering, every live conversation inside it is ended and handed to a person, and its history is kept. Nothing is hard-deleted. Asks for confirm.

    Required permissions: flows.delete

    flow_idinteger · required confirmboolean
    Argument schema and validation
    flow_idintegerrequired
    The flow to archive.
    confirmbooleanoptional
    Set true only after the user agreed.
  • duplicate_message_flow vfadb8b63 writes Copy a message flow into a new draft: the same nodes, canvas and trigger shapes, with the keyword triggers CLEARED so the copy cannot compete with the original for the same messages. The copy reaches nobody until published.

    Required permissions: flows.create

    flow_idinteger · required namestring
    Argument schema and validation
    flow_idintegerrequired
    The flow to copy.
    namestringoptional
    What to call the copy. Defaults to the original name with "(copy)".
  • end_flow_session v59eba045 writes End a live flow conversation by hand: any open payment is cancelled, its locks released, and — with notify_customer — the customer is told a person will continue and the thread is handed to the inbox. Use when a customer is stuck or the flow is misbehaving. Asks for confirm first.

    Required permissions: flows.edit

    session_idinteger · required notify_customerboolean reasonstring confirmboolean
    Argument schema and validation
    session_idintegerrequired
    The live session to end, from list_flow_sessions.
    notify_customerbooleanoptional
    Send the customer the bilingual "a person will continue" line and hand the thread to the inbox.
    reasonstringoptional
    Why, for the audit trail.
    confirmbooleanoptional
    Set true only after the user agreed to end a live customer conversation.
  • format_flow_layout v764ad480 writes Tidy the canvas — arrange every node as a readable top-to-bottom tree, the same as the builder's "Format layout" button. Call this after building or reshaping a flow. You can also place nodes yourself with positions_json.

    Required permissions: flows.edit

    flow_idinteger · required positions_jsonstring
    Argument schema and validation
    flow_idintegerrequired
    The flow to arrange.
    positions_jsonstringoptional
    Optional explicit placement: node id => {"x":123,"y":456}. Omit to auto-arrange.
  • install_sample_flow ved14732c writes Start from a template: install one of the platform's sample WhatsApp flows on this account as a DRAFT — a welcome menu, a shop ordering flow, a support desk, a contributions circle. Call with no key to see the gallery. The draft reaches nobody until published.

    Required permissions: flows.create

    keystring namestring
    Argument schema and validation
    keystringoptional
    The sample to install: welcome, duka_orders, msaada, dili_customer or dili_supplier. Leave empty to list them.
    namestringoptional
    What to call the new flow. Defaults to the sample's own name.
  • nudge_flow_session v237e13df writes Send a waiting customer the question the flow is waiting on, again, with a short reminder line before it. Only for steps that merely ask (buttons, lists, questions, forms) — never money, an approval or a timer — and at most once a minute per conversation. The message reaches a real customer on WhatsApp.

    Required permissions: flows.edit

    session_idinteger · required
    Argument schema and validation
    session_idintegerrequired
    The waiting session, from list_flow_sessions.
  • pause_message_flow ve1865d24 writes Take a live message flow quiet: nothing new starts, and the conversations already inside it finish on their own ("let finish"). The published version is kept, so resume_message_flow puts it straight back. Gated like publish; asks for confirm.

    Required permissions: flows.publish

    flow_idinteger · required confirmboolean
    Argument schema and validation
    flow_idintegerrequired
    The live flow to pause.
    confirmbooleanoptional
    Set true only after the user agreed.
  • pin_flow_data_table vdb35330f writes Pin a data table to a message flow's View data page so the team sees its records next to the flow. It changes what the page shows and nothing else: no node, no access for the flow. Use when the user wants a table kept in view alongside this flow.

    Required permissions: flows.edit

    flow_idinteger · required table_idstring · required
    Argument schema and validation
    flow_idintegerrequired
    The flow to pin the table to.
    table_idstringrequired
    The data table id (uuid), from list_data_tables.
  • publish_message_flow v7f2858cb writes Make a message flow LIVE. It will start intercepting real customer conversations on WhatsApp, ahead of any AI agent. Only use when the user has explicitly asked to publish.

    Required permissions: flows.publish

    flow_idinteger · required expected_versioninteger · required acknowledge_overlapboolean
    Argument schema and validation
    flow_idintegerrequired
    The flow to publish.
    expected_versionintegerrequired
    The draft version you read (get_message_flow → version). Required: a publish of a draft somebody changed under you is refused.
    acknowledge_overlapbooleanoptional
    Set true only after telling the user another live flow answers the same words and they confirmed.
  • resume_message_flow v1024d52b writes Put a paused message flow back LIVE on the version that was published. It starts intercepting real customer messages again the moment this returns, so it is gated like publish and asks for confirm.

    Required permissions: flows.publish

    flow_idinteger · required confirmboolean
    Argument schema and validation
    flow_idintegerrequired
    The paused flow to resume.
    confirmbooleanoptional
    Set true only after the user agreed it goes live again now.
  • rollback_message_flow vf262e96f writes Put an earlier published version of a message flow back LIVE for customers, as a new version so history stays complete. It also replaces the current draft with that snapshot. Use only when the user has asked to undo a publish; list_flow_versions gives the ids.

    Required permissions: flows.publish

    flow_idinteger · required version_idinteger · required expected_versioninteger · required confirmboolean
    Argument schema and validation
    flow_idintegerrequired
    The flow to roll back.
    version_idintegerrequired
    The version id to restore, from list_flow_versions.
    expected_versionintegerrequired
    The draft version you read (get_message_flow → version). Required: a rollback over a draft somebody changed under you is refused.
    confirmbooleanoptional
    Set true only after the user has agreed that this version goes live now and the draft is replaced.
  • set_flow_triggers va13fa8ce writes Replace a flow's trigger list — what makes it start. The list is ORDERED and the first match wins, so send the whole list, not a patch. Reports which other flows on the account would compete for the same messages.

    Required permissions: flows.edit

    flow_idinteger · required expected_versioninteger · required triggers_jsonstring · required
    Argument schema and validation
    flow_idintegerrequired
    The flow to set triggers on.
    expected_versionintegerrequired
    The draft version you read (get_message_flow → version). Required: triggers replaced on a draft somebody changed under you are refused.
    triggers_jsonstringrequired
    A JSON array of triggers, ordered, first match wins. e.g. [{"type":"keyword","match":"any","values":["order","oda"],"channels":["whatsapp"]}]
  • unpin_flow_data_table vc21d624d writes Take a hand-pinned data table off a message flow's View data page. Only pins go: a table a node actually reads or writes stays listed until that node is removed. Use when the user no longer wants the table shown with this flow.

    Required permissions: flows.edit

    flow_idinteger · required table_idstring · required
    Argument schema and validation
    flow_idintegerrequired
    The flow to unpin the table from.
    table_idstringrequired
    The data table id (uuid) that was pinned.
  • update_flow_scenario vd0f3da40 writes Change a test written against a message flow — its name, turns, fakes, expectations, or whether it can refuse a publish. Send only the fields to change; a script or expectations you send replaces the stored one whole.

    Required permissions: flows.edit

    flow_idinteger · required scenario_idinteger · required namestring kindstring script_jsonstring expectations_jsonstring enabledboolean
    Argument schema and validation
    flow_idintegerrequired
    The flow the test belongs to.
    scenario_idintegerrequired
    The test to change, from run_flow_scenarios or create_flow_scenario.
    namestringoptional
    A new name.
    kindstringoptional
    A label for the list: happy_path, validation_failure, timeout, duplicate, payment_failure, handoff, custom (default happy_path).
    script_jsonstringoptional
    What happens, as a JSON object. turns: the customer's turns in order — a bare string is a text, {"choice":"yes"} is a tap by button or row ID, {"timeout":true} is the wait running out. fakes: {"<node_id>": {"out": "expired"}} or {"kind:http_request": {"out": "failed", "set": {"stock": 0}}} — the answer a step that reaches outside should give (every other such step takes its first exit). variables: starting variables. trigger: {"variables": {"record": {...}}} for a record- or event-started flow. contact: {"phone", "name"}. window_open: false to run outside the 24-hour window.
    expectations_jsonstringoptional
    What should be true at the end, as a JSON object of ends_at, status, ended_reason, variables, unset, sent_contains, never_sent, sent_count, reached, not_reached, faked_reached, exits, turns. exits is [{"node","out","visit"?}] (visit picks the nth pass through a loop); turns is a list, one per scripted turn, each {"expect": {"sent_contains": [...], "never_sent": [...], "offered_choices": ["yes","no"]}} judged on the replies to that turn. A fake on a step the run never reaches fails the test unless the step is named in faked_reached.
    enabledbooleanoptional
    Whether a failure refuses the next publish (default true).
  • update_message_flow veb34fce3 writes Rename a message flow, change its description, its status, its priority, or its own node ceiling (max_nodes). Priority decides which flow wins when two match the same message — lower runs first. Pausing an active flow stops it answering customers. max_nodes can only LOWER the workspace's nodes_per_flow ceiling for this one flow; 0 clears it.

    Required permissions: flows.edit

    flow_idinteger · required namestring descriptionstring statusstring priorityinteger max_nodesinteger confirm_pauseboolean expected_versioninteger
    Argument schema and validation
    flow_idintegerrequired
    The flow to change.
    namestringoptional
    A new name.
    descriptionstringoptional
    One line on what it does.
    statusstringoptional
    draft, active, paused or archived.
    priorityintegeroptional
    1–9999. Lower runs first when two flows match the same message.
    max_nodesintegeroptional
    This flow's own node ceiling. Only ever LOWER than the workspace's nodes_per_flow quota (get_flow_catalog names it) and never below the nodes it already has; 0 clears it so the workspace's applies.
    confirm_pausebooleanoptional
    Required to pause or archive a flow that is currently live.
    expected_versionintegeroptional
    The version from get_message_flow.
Data tables /mcp/v1/data 21 read · 32 write

The tables this business defined for itself and their records: read with filters, create/update/upsert rows, shape fields, run and save reports, and group related tables into folders with reports that read across them. Flows and IVRs read the same tables.

  • evaluate_business_rule vae31d075 Ask a business rule for its answer, rather than working it out yourself. Give it the rule key and the values it needs and it answers `passed` (may this go ahead?), `value` (the fee, the number left, the branch to take, whether the business is open) and `reason` — a sentence written for the customer, which you should quote rather than paraphrase. Nothing is changed and nothing is reserved: a limit that answers "one left" does not hold that one for you. Use this before quoting a charge or promising a slot, and say plainly when a rule refuses.

    Required permissions: rules.view, data.view

    keystring · required inputsstring
    Argument schema and validation
    keystringrequired
    The rule key, e.g. daily_withdrawals.
    inputsstringoptional
    A JSON object of the values the rule needs, e.g. {"amount": 50000, "subject": "+255712345678"}. Pass `at` as an ISO-8601 time to ask about a moment other than now.
  • get_business_rule v11031495 One business rule in full: its kind and the definition behind it — the ceiling and the period for a limit, the formula and tiers for a fee, the condition for an eligibility test, the hours and holidays for a window, the map for a choice. Read this when you need to explain WHY a rule said no, or before changing one with upsert_business_rule, so you keep the parts you are not changing.

    Required permissions: rules.view

    keystring · required
    Argument schema and validation
    keystringrequired
    The rule key, e.g. daily_withdrawals.
  • get_data_export v2b264696 One export by id: its status and, once ready, the signed download link (no login needed; expires with the file), plus rows, size, any note (e.g. a PDF that became xlsx), any error, and the delivery outcome. Poll this after export_data_records or export_data_report answered "rendering".

    Required permissions: data.view

    export_idstring · required
    Argument schema and validation
    export_idstringrequired
    The export id from export_data_records, export_data_report or list_data_exports.
  • get_data_group v6778f941 One table group with its member tables and the overview for a range: totals, a card per table (records, columns, the headline amount total, records created in the range), records over time stacked by table, every amount-like column totalled across the group, the relations between member tables, and the saved cross-table reports. Every card carries a drill {table_id, filter, range} you can pass to query_data_records to see the rows behind a number.

    Required permissions: data.view

    group_idstring · required rangestring
    Argument schema and validation
    group_idstringrequired
    Group id or slug from list_data_groups.
    rangestringoptional
    A preset (today, yesterday, last_7_days, last_30_days, last_90_days, this_month, last_month) or a JSON period {"from":"YYYY-MM-DD","to":"YYYY-MM-DD"}. Default last_30_days.
  • get_data_record v3935c788 Read one record of a table by its id: every field value, its source (ui, api, mcp, a flow or an IVR), timestamps and the record title. Use query_data_records when you only know a value, not the id.

    Required permissions: data.view

    table_idstring · required record_idstring · required
    Argument schema and validation
    table_idstringrequired
    Table id or slug.
    record_idstringrequired
    The record id.
  • get_data_record_history vd6877361 What has happened to one record: every create, edit and delete, newest first, with who made it (a person, an API key, an assistant connection, a message flow, a phone menu or the platform itself), when, which fields moved and from what to what. Use it to answer "who changed this" and "what did it say before" — the record itself only shows the current values.

    Required permissions: data.view

    table_idstring · required record_idstring · required limitinteger
    Argument schema and validation
    table_idstringrequired
    Table id or slug.
    record_idstringrequired
    The record id. The record may already be deleted — its trail is still here.
    limitintegeroptional
    How many entries, newest first (default 25, max 100).
  • get_data_table_governance v48388040 How one table is governed: who may see it (per-table access lines for roles, teams and people), which fields are hidden on the way out and who may see through them, how long records are kept before they are deleted or anonymised, whether the table is under legal hold, and the last retention runs. A table with no access lines is not restricted at all — whoever holds the Data permission sees it. Use this before explaining why somebody cannot see a table, or before changing a retention rule.

    Required permissions: data.manage

    table_idstring · required
    Argument schema and validation
    table_idstringrequired
    The table id (uuid) or slug from list_data_tables.
  • get_data_table_schema v38c4cf19 Everything about one table: every field with its key, type, validation rules, the filter operators it accepts, whether it is required/unique/indexed, the select options, and the quotas in use. Read this before writing records or filters — keys and operators come from here, not from guesswork. Also lists the field types available when adding a column.

    Required permissions: data.view

    table_idstring · required
    Argument schema and validation
    table_idstringrequired
    The table id (uuid) or slug from list_data_tables.
  • get_data_table_states vced879cc The state machine behind a table's status fields: every state (key, label, colour, whether a new record starts there and whether it is an end state) and, for each one, exactly which states a record in it may move to next. Read this before transition_data_record or before writing a status value — a move that is not listed here is refused, and the state keys are what a record stores. Answers an empty fields list when the table has no status field.

    Required permissions: data.view

    table_idstring · required
    Argument schema and validation
    table_idstringrequired
    The table id (uuid) or slug from list_data_tables.
  • get_data_table_usage v563dd259 Where a table is used: the message flows and the IVR phone menus that read or write it through their Find/Save/Delete record steps (with each flow's name and whether it reads, writes or both), the reports saved on it, and the group (folder) it sits in. Call it before changing or deleting a table or a field, so you can tell the person which flows would be affected, or when they ask "what uses this?".

    Required permissions: data.view

    table_idstring · required
    Argument schema and validation
    table_idstringrequired
    Table id or slug.
  • list_business_rules vb2aa30f4 Every business rule this account has defined: the limits, fees, eligibility tests, opening windows and routing maps that its message flows, phone menus and assistants all enforce. Read this before quoting a charge, promising a booking or telling a customer they qualify for something — the rule is the answer, not your own arithmetic. Returns each rule's key (what a flow asks for), label, kind, whether it is switched on, and one line about it; call get_business_rule for the full definition.

    Required permissions: rules.view

    kindstring enabled_onlyboolean limitinteger
    Argument schema and validation
    kindstringoptional
    Only this kind: limit, fee, eligibility, window or choice.
    enabled_onlybooleanoptional
    Leave out the rules that are switched off.
    limitintegeroptional
    How many to return (default 100, max 200).
  • list_data_actions v70284bb7 The record actions a table defines (.data-store/03 §D): the buttons people press on a record — start a message flow for its phone, call a webhook with it, set some fields, export it as a file, or open a link built from it. Each comes back as {id, label, icon, kind: flow|webhook|set_fields|export|open_url, scope: row|bulk|both, confirm, permission} plus what the kind needs (flow_id, url, patch, format). Run one with run_data_action.

    Required permissions: data.view

    table_idstring · required
    Argument schema and validation
    table_idstringrequired
    Table id or slug.
  • list_data_changes v185a9e68 Recent changes across one table, newest first: which records were created, edited or deleted, by whom, and which fields moved. Filter with since (an ISO-8601 moment or a relative window like "last_7_days") and actor_kind (user, api, mcp, flow, ivr, schedule, system, import) to answer "what did the flow write last night" or "what has anyone touched today". get_data_record_history is the same trail for one record.

    Required permissions: data.view

    table_idstring · required sincestring actor_kindstring limitinteger
    Argument schema and validation
    table_idstringrequired
    Table id or slug.
    sincestringoptional
    Only changes at or after this moment: an ISO-8601 timestamp, or one of today, last_24_hours, last_7_days, last_30_days, last_90_days. Omit for the most recent changes whenever they were.
    actor_kindstringoptional
    Only changes made by this kind of writer: user, api, mcp, flow, ivr, schedule, system, import.
    limitintegeroptional
    How many entries, newest first (default 25, max 100).
  • list_data_exports v87f5c04b The files this business has exported from its tables and reports, newest first: what each is (records / report, format), its status (pending, rendering, ready, failed, expired), row count, who asked (a person, a message flow, a phone menu, an assistant, a schedule), how it was delivered, when it expires, and a signed download link while it is ready. Pass table_id to narrow to one table.

    Required permissions: data.view

    table_idstring statusstring limitinteger
    Argument schema and validation
    table_idstringoptional
    Only exports of this table (id or slug).
    statusstringoptional
    Only this status: pending, rendering, ready, failed or expired.
    limitintegeroptional
    Rows, default 20, max 100.
  • list_data_groups v9c9c84ea List the table groups of this business — named folders of related tables ("Mauzo": customers, orders, payments) with a report layer that reads across every table in them. Each row carries the member count and the records across them. get_data_group reads one with its overview; run_data_group_report computes across its tables.

    Required permissions: data.view

    searchstring
    Argument schema and validation
    searchstringoptional
    Filter by name or slug.
  • list_data_reports v13515dd3 The reports available on a table (pass table_id): the defaults derived from its fields (record count, records over time, totals of number fields, breakdowns of select/boolean fields) and the ones people saved. Each carries a ready definition you can pass to run_data_report as it is, or tweak. Pass group_id instead for the cross-table reports saved on a group (run them with run_data_group_report; the group's live overview is on get_data_group). Exactly one of table_id or group_id.

    Required permissions: data.view

    table_idstring group_idstring
    Argument schema and validation
    table_idstringoptional
    Table id or slug — for a table's reports.
    group_idstringoptional
    Group id or slug — for a group's cross-table reports instead.
  • list_data_tables vc9d35f01 List the data tables this business defined for itself (customers, orders, bookings — whatever a flow or a person keeps here), with record counts, how many fields each has and the group (folder) it sits in. Start here; every other data tool takes a table_id (or slug) from this list.

    Required permissions: data.view

    searchstring group_idstring limitinteger
    Argument schema and validation
    searchstringoptional
    Filter by name or slug.
    group_idstringoptional
    Only the tables of this group (id or slug from list_data_groups).
    limitintegeroptional
    Default 25, max 100.
  • query_data_records va1c92540 Read records from a table, newest first, with an optional filter, free-text search and sort. Pages by cursor: pass next_cursor from the previous answer to continue, never a page number. A filter is a JSON condition tree: {"all":[{"column":"opt_in","op":"equals","value":true},{"any":[...]}]}. Leaves are {column, op, value}; nest "all"/"any" freely. Operators: equals, not_equals, contains, starts_with, greater_than, less_than, between ([low, high]), is_empty, is_not_empty, in (a list). Each column type accepts a subset — get_data_table_schema lists them per column. System columns: $id, $created_at, $updated_at (temporal ops; a value may be {"relative":"last_30_days"} — presets today, yesterday, last_7_days, last_30_days, last_90_days, this_month, last_month), $source (equals/in/starts_with: ui, api, mcp, import, seed, flow:<id>, ivr:<id>). Sorting on a field that is not indexed is refused on large tables; sort by $created_at instead. Each row carries its id, data, source, timestamps and a title.

    Required permissions: data.view

    table_idstring · required filterstring searchstring sortstring dirstring cursorstring limitinteger with_countboolean
    Argument schema and validation
    table_idstringrequired
    Table id or slug.
    filterstringoptional
    JSON condition tree (see the tool description).
    searchstringoptional
    Free text matched against the text, phone and email fields.
    sortstringoptional
    A field key or $created_at. Default $created_at.
    dirstringoptional
    asc or desc (default desc).
    cursorstringoptional
    next_cursor from the previous page.
    limitintegeroptional
    Rows per page, default 25, max 100.
    with_countbooleanoptional
    Also count every matching record (one extra query).
  • run_data_group_report v7dcf173e Run a report across the tables of a group and get the rows back: orders total and new customers per week on one axis, totals per table side by side, or a ratio between two tables. A group report definition is JSON: {"series":[{"table_id":"<member table id or slug>","metric":{"fn":"sum","column":"amount"},"filters":<condition tree or null>,"label":"Orders","breakdown":{"column":"status","top":5},"date_column":"paid_at"},{"table_id":"…","metric":{"fn":"count"},"label":"Customers"},{"label":"Orders per customer","formula":"orders / customers"}],"dimension":{"column":"$created_at","bucket":"day|week|month|quarter"} (one shared time axis; rows {bucket, series:{label: value}, drill:{label: {table_id, filter, range}}}) or {"kind":"table"} (one row per series: {label, table_id, value, drill}) or null (one number per series),"date_range":{"relative":"last_30_days"} or {"from":"2026-08-01","to":"2026-09-01"},"chart":"number|line|bar|stacked_bar|donut|table","compare":"previous_period"|"previous_year" (optional),"sort":{"by":"value|label","dir":"asc|desc"} and "limit" (by-table reports only). A formula series names other series by their slugified label (Orders → orders; "Kiasi (TZS)" → kiasi_tzs) and may use + - * / parentheses and percent_of(a,b). Every table_id must be a member of the group; each series is validated against its own table. A measure with "as":"percent_of_total" also answers its share of the total. Every run is bounded by the date range (default last 90 days) and a 10-second budget across all series; results are cached for a minute. Every cell carries a drill {table_id, filter, range} for query_data_records. A filter is a JSON condition tree: {"all":[{"column":"opt_in","op":"equals","value":true},{"any":[...]}]}. Leaves are {column, op, value}; nest "all"/"any" freely. Operators: equals, not_equals, contains, starts_with, greater_than, less_than, between ([low, high]), is_empty, is_not_empty, in (a list). Each column type accepts a subset — get_data_table_schema lists them per column. System columns: $id, $created_at, $updated_at (temporal ops; a value may be {"relative":"last_30_days"} — presets today, yesterday, last_7_days, last_30_days, last_90_days, this_month, last_month), $source (equals/in/starts_with: ui, api, mcp, import, seed, flow:<id>, ivr:<id>).

    Required permissions: data.view

    group_idstring · required definitionstring · required
    Argument schema and validation
    group_idstringrequired
    Group id or slug.
    definitionstringrequired
    JSON group report definition (see the tool description). Member tables may be named by slug.
  • run_data_report v87a3f6d7 Run an aggregate over a table and get the rows back: a count, a sum by region, records per week. A report definition is JSON: {"metrics":[{"fn":"sum","column":"amount"},{"fn":"count"}],"dimension":{"column":"region"} or {"column":"$created_at","bucket":"week"} or null,"filters":<condition tree or null>,"date_range":{"relative":"last_30_days"} or {"from":"2026-08-01","to":"2026-09-01"},"chart":"number|line|bar|stacked_bar|donut|table"}. fn: count, count_distinct, sum, avg, min, max (the last four need a number/currency column). A dimension may be a select, boolean or relation field, or a date field with bucket day|week|month|quarter. No dimension gives one number. Every run is bounded by a date range (default last 90 days) and a 10-second budget; results are cached for a minute. Optional keys: "breakdown":{"column":"status","top":5} splits every row into per-value series (rows gain series:{value:{metrics}}, values past top fold into "other"); "compare":"previous_period"|"previous_year" re-runs the same query over the preceding window and adds previous/delta/delta_pct per metric on every row; a measure {"fn":"formula","expr":"sum_amount / count","label":"Average order"} is computed per row from the other measures' keys (+ - * / parentheses, percent_of(a,b); division by zero gives null); "dimension":{"column":"region","top":6} with "sort":{"by":"sum_amount","dir":"desc"} and "limit":20 cut a category dimension; a measure with "as":"percent_of_total" also answers <key>_pct per row. Example: {"metrics":[{"fn":"sum","column":"amount"},{"fn":"count"},{"fn":"formula","expr":"sum_amount / count","label":"Average"}],"dimension":{"column":"$created_at","bucket":"week"},"compare":"previous_period","date_range":{"relative":"last_90_days"},"chart":"line"}. A filter is a JSON condition tree: {"all":[{"column":"opt_in","op":"equals","value":true},{"any":[...]}]}. Leaves are {column, op, value}; nest "all"/"any" freely. Operators: equals, not_equals, contains, starts_with, greater_than, less_than, between ([low, high]), is_empty, is_not_empty, in (a list). Each column type accepts a subset — get_data_table_schema lists them per column. System columns: $id, $created_at, $updated_at (temporal ops; a value may be {"relative":"last_30_days"} — presets today, yesterday, last_7_days, last_30_days, last_90_days, this_month, last_month), $source (equals/in/starts_with: ui, api, mcp, import, seed, flow:<id>, ivr:<id>).

    Required permissions: data.view

    table_idstring · required definitionstring · required
    Argument schema and validation
    table_idstringrequired
    Table id or slug.
    definitionstringrequired
    JSON report definition (see the tool description).
  • suggest_data_reports vc372ef6f Three to six ready-made report definitions read off a table's or a group's schema — the amount headline by week against the previous period, the top category by amount broken down by a second category, a computed average per record, records over time, a share-of-total split, and for a group a cross-table ratio when two members share a relation. Each comes with a name and a one-line reason; pass its definition to run_data_report (table_id) or run_data_group_report (group_id) as it is, or tweak it. Pass exactly one of table_id or group_id.

    Required permissions: data.view

    table_idstring group_idstring
    Argument schema and validation
    table_idstringoptional
    Table id or slug — suggestions for one table.
    group_idstringoptional
    Group id or slug — suggestions across the group's tables.
  • add_data_column v19fa8488 writes Add a field to a table. The grid, the form, the filters and the default reports pick it up on the next load — nothing else to do. Types: text, long_text, number, currency, boolean, date, datetime, phone, email, select, multi_select, status, relation, file, auto_number. Config by type — number/currency: {precision, unit|currency, min, max}; select/multi_select: {options:[{key,label,color}]}; status: {states:[{key,label,color,initial,final}],transitions:[{from:'<key>|*',to:'<key>',label,requires}],strict:true} — a state machine, so a record may only be created in an initial state and only move along a declared transition (get_data_table_states reads one back); relation: {target_table_id}; phone: {default_region:"TZ"}; auto_number: {prefix:"ORD-", pad:6, yearly:false}; any: {default, ui:{is_title_field, is_summary_metric, help_text, placeholder, hidden_in_grid, hidden_in_form}}. An auto_number is written by the platform only: never send a value for it, and existing records are numbered in the background when the field is added. The key is derived from the label unless given. Indexes and uniqueness are a separate call, not a job for the owner: request_data_column_index puts a btree index (sort and filter) or a unique rule on one field, or a "unique together" rule over 2 to 4 fields, and drop_data_column_index removes either. They build in the background and get_data_table_schema shows index_status move to ready.

    Required permissions: data.manage

    table_idstring · required labelstring · required typestring · required keystring requiredboolean configstring
    Argument schema and validation
    table_idstringrequired
    Table id or slug.
    labelstringrequired
    What people see, e.g. "Kiasi cha mwisho".
    typestringrequired
    One of: text, long_text, number, currency, boolean, date, datetime, phone, email, select, status, multi_select, relation, file, auto_number.
    keystringoptional
    Machine key used in filters and flows ({{vars.record.key}}); derived from the label if omitted.
    requiredbooleanoptional
    Refuse records without a value. Default false.
    configstringoptional
    JSON object of type settings (see the tool description).
  • bulk_create_data_records v5325cef5 writes Write many records in one call — an import from a spreadsheet, a list the person dictated, a batch pulled from another system. rows is a JSON array of at most 200 objects keyed by field key (from get_data_table_schema). mode "create" inserts every row; mode "upsert" matches each row on match_column (a unique field the row must contain) and updates the existing record or creates one, so re-running the same batch never duplicates. Rows are written one by one through the same checks as create_data_record: a row that fails validation is skipped and reported with its field errors while the others go in; the batch stops at the first quota refusal (records or storage) and reports how many were written. The answer lists every row by index with ok, id or errors. Pass an idempotency_key (any string you make up, e.g. a UUID) when a retry must not create a second copy: the first successful answer is kept for 24 hours and replayed for the same key, so a call that timed out can be repeated safely.

    Required permissions: data.records.edit

    table_idstring · required rowsstring · required modestring match_columnstring idempotency_keystring
    Argument schema and validation
    table_idstringrequired
    Table id or slug.
    rowsstringrequired
    JSON array (max 200) of objects keyed by field key.
    modestringoptional
    "create" (default) inserts every row; "upsert" matches on match_column and updates or creates.
    match_columnstringoptional
    For upsert: the unique field every row carries, e.g. phone.
    idempotency_keystringoptional
    Optional: a key you make up so a retried call replays the first answer instead of writing the batch again (kept 24 h).
  • create_data_group v58fc0b64 writes Create a table group — a named folder of related tables with a report layer across them. Give it a human name ("Mauzo", "Bookings"); the slug is derived unless you pass one; optionally list the tables (ids or slugs) to put in it straight away. A table belongs to at most one group, so a listed table already in another group moves. Counts against the workspace's group quota. Pass an idempotency_key (any string you make up, e.g. a UUID) when a retry must not create a second copy: the first successful answer is kept for 24 hours and replayed for the same key, so a call that timed out can be repeated safely.

    Required permissions: data.manage

    namestring · required slugstring descriptionstring iconstring colorstring table_idsstring idempotency_keystring
    Argument schema and validation
    namestringrequired
    Human name, e.g. "Mauzo".
    slugstringoptional
    Optional machine name: lowercase letters, digits, underscores, starting with a letter.
    descriptionstringoptional
    One line on what the group holds.
    iconstringoptional
    An emoji shown before the name, e.g. "🛒".
    colorstringoptional
    A palette key: gray, red, orange, amber, yellow, lime, green, emerald, teal, cyan, sky, blue, indigo, violet, purple, fuchsia, pink, rose.
    table_idsstringoptional
    JSON list (or comma list) of table ids or slugs to put in the group, in order.
    idempotency_keystringoptional
    Optional: a key you make up so a retried call cannot create the group twice (kept 24 h).
  • create_data_record veacc2df3 writes Create one record. Pass data as a JSON object keyed by field key (from get_data_table_schema). Values are checked against each field's type: numbers as numbers, booleans as true/false, dates as YYYY-MM-DD, datetimes as ISO-8601, phones in any Tanzanian form (0712…, +255…), select values as option keys. A required field missing, a unique field clashing, or a value of the wrong shape is refused with the field named, and nothing is saved. To avoid duplicates on a unique field, prefer upsert_data_record. For many rows at once use bulk_create_data_records. Pass an idempotency_key (any string you make up, e.g. a UUID) when a retry must not create a second copy: the first successful answer is kept for 24 hours and replayed for the same key, so a call that timed out can be repeated safely.

    Required permissions: data.records.edit

    table_idstring · required datastring · required idempotency_keystring
    Argument schema and validation
    table_idstringrequired
    Table id or slug.
    datastringrequired
    JSON object: {"phone":"+255712345678","name":"Asha"}.
    idempotency_keystringoptional
    Optional: a key you make up so a retried call cannot create the record twice (kept 24 h).
  • create_data_table ve95f0eb0 writes Create a new, empty data table for this business. Give it a human name ("Wateja", "Bookings"); the slug is derived unless you pass one. Then add fields with add_data_column. Counts against the workspace's table quota. Pass an idempotency_key (any string you make up, e.g. a UUID) when a retry must not create a second copy: the first successful answer is kept for 24 hours and replayed for the same key, so a call that timed out can be repeated safely.

    Required permissions: data.manage

    namestring · required slugstring descriptionstring iconstring idempotency_keystring
    Argument schema and validation
    namestringrequired
    Human name, e.g. "Wateja" or "Bookings".
    slugstringoptional
    Optional machine name: lowercase letters, digits, underscores, starting with a letter. Derived from the name if omitted.
    descriptionstringoptional
    One line on what the table holds.
    iconstringoptional
    An emoji shown before the name, e.g. "👥".
    idempotency_keystringoptional
    Optional: a key you make up so a retried call cannot create the table twice (kept 24 h).
  • delete_data_column v9edd7d19 writes Remove a field from a table. The field disappears from the grid, form, filters and reports immediately; its index is dropped in the background; the stored values are purged after a day, so a mistake is recoverable by the owner until then. The key stays reserved — a new field cannot reuse it. Flows that reference the field will fail validation until edited.

    Required permissions: data.manage

    table_idstring · required columnstring · required
    Argument schema and validation
    table_idstringrequired
    Table id or slug.
    columnstringrequired
    The field key or id to remove.
  • delete_data_group v02b5838d writes Delete a table group (folder). The tables in it are KEPT — they simply become ungrouped, with every field and record intact — but the group's own saved cross-table reports go with it. Use it to dissolve a folder the person no longer wants; to remove a whole table and its records use delete_data_table instead. Says which tables were left ungrouped.

    Required permissions: data.manage

    group_idstring · required
    Argument schema and validation
    group_idstringrequired
    Group id or slug from list_data_groups.
  • delete_data_record v28d4105a writes Delete records by id (one, or a comma-separated list). Soft: the rows leave every list and count but the owner can still recover them from the database for a while. Returns how many were removed.

    Required permissions: data.records.edit

    table_idstring · required record_idsstring · required
    Argument schema and validation
    table_idstringrequired
    Table id or slug.
    record_idsstringrequired
    One record id, or several separated by commas.
  • delete_data_report v62c1c257 writes Remove a saved report from a table's Reports tab (pass table_id) or from a group's (pass group_id) — exactly one of the two, plus the report_id from list_data_reports. Only the saved definition goes; the records it counted are untouched, and the default reports derived from the fields cannot be removed. Use it when a saved card is wrong or no longer wanted; to change one instead, save_data_report / save_data_group_report with its report_id.

    Required permissions: data.reports.manage

    table_idstring group_idstring report_idstring · required
    Argument schema and validation
    table_idstringoptional
    Table id or slug (for a table report).
    group_idstringoptional
    Group id or slug (for a group report).
    report_idstringrequired
    The saved report's id.
  • delete_data_table v9789fb07 writes Delete a whole table — its fields, every record, its saved reports and its flow bindings — permanently and at once; there is no recovery. Because of that it works in two steps: called without confirm it only reports what would go (the record count and the message flows and phone menus that read or write the table), and nothing is deleted. Read that back to the person; when they agree, call again with confirm set to the table's slug exactly. A flow that used the table will fail its Find/Save/Delete steps until edited.

    Required permissions: data.manage

    table_idstring · required confirmstring
    Argument schema and validation
    table_idstringrequired
    Table id or slug.
    confirmstringoptional
    The table's slug, typed exactly, once the person has agreed. Omit it first to see what would be deleted.
  • drop_data_column_index v86a4d85f writes Drop the index a field has, or a "unique together" rule. One field: pass column. The field stops being sortable on big tables at once; if it was unique, duplicates are allowed again from this moment and upsert_data_record can no longer match on it. A set: pass columns with the same field keys the rule was made with (get_data_table_schema lists them under unique_sets) and that combination may repeat again. The physical index is removed in the background (index_status dropping, then gone). Needed before update_data_column may rename the field's key or change its type, and the way to free an index slot when the quota is full. Nothing about the values changes.

    Required permissions: data.manage

    table_idstring · required columnstring columnsstring
    Argument schema and validation
    table_idstringrequired
    Table id or slug.
    columnstringoptional
    The field key or id, to drop the index on ONE field. Leave out when passing columns.
    columnsstringoptional
    The field keys of a "unique together" rule to drop, as a JSON list ["room","day"] or a comma list, exactly as get_data_table_schema lists them under unique_sets.
  • export_data_records v261bc2d2 writes Turn a table's records into a file — CSV, an Excel workbook (xlsx) or a PDF — with an optional filter, sort and choice of columns, and either get a signed download link or have it delivered. Up to 50,000 rows (a PDF holds 2,000; a longer list comes back as xlsx and the export's note says so). Short lists (≤ 500 rows) render on this call; longer ones render in the background — poll get_data_export. Delivery is optional: deliver_via none (default — you get a signed download link), whatsapp (a document to the phone in deliver_to, which must already have a WhatsApp conversation with this business), email (an attachment to the address in deliver_to; deliver_subject optional) or sms (the link, texted to deliver_to). Sending needs the "Send messages and place calls" tick on this connection. Files expire after 72 hours; get_data_export answers the current status and link. A filter is a JSON condition tree: {"all":[{"column":"opt_in","op":"equals","value":true},{"any":[...]}]}. Leaves are {column, op, value}; nest "all"/"any" freely. Operators: equals, not_equals, contains, starts_with, greater_than, less_than, between ([low, high]), is_empty, is_not_empty, in (a list). Each column type accepts a subset — get_data_table_schema lists them per column. System columns: $id, $created_at, $updated_at (temporal ops; a value may be {"relative":"last_30_days"} — presets today, yesterday, last_7_days, last_30_days, last_90_days, this_month, last_month), $source (equals/in/starts_with: ui, api, mcp, import, seed, flow:<id>, ivr:<id>).

    Required permissions: data.view

    table_idstring · required formatstring filterstring searchstring sortstring dirstring columnsstring titlestring max_rowsinteger deliver_viastring deliver_tostring deliver_captionstring deliver_subjectstring waitboolean
    Argument schema and validation
    table_idstringrequired
    Table id or slug.
    formatstringoptional
    csv (default), xlsx or pdf.
    filterstringoptional
    JSON condition tree (see the tool description).
    searchstringoptional
    Free text matched against the text, phone and email fields.
    sortstringoptional
    A field key or $created_at (default: newest first).
    dirstringoptional
    asc or desc (default desc).
    columnsstringoptional
    JSON list or comma list of field keys to include, in order. Default: every field.
    titlestringoptional
    Title on the file (PDF header, filename). Default: the table name.
    max_rowsintegeroptional
    Cap the rows (1–50,000).
    deliver_viastringoptional
    none (default), whatsapp, email or sms.
    deliver_tostringoptional
    Phone (whatsapp/sms) or email address the file goes to.
    deliver_captionstringoptional
    Caption under a WhatsApp document.
    deliver_subjectstringoptional
    Email subject (default: the title).
    waitbooleanoptional
    Render on this call when the list is short (default true).
  • export_data_report v17bbef96 writes Render a report as a file — a PDF with the table, totals and bars, an Excel workbook (a Summary sheet plus one sheet per series for a group report) or a CSV — and get a signed link or have it delivered. Pass table_id with a definition (the same JSON run_data_report takes) or report_id (a saved report from list_data_reports); or group_id with a group report definition (run_data_group_report's) or report_id. Optional range overrides the definition's date_range, e.g. {"relative":"last_30_days"}. Reports always render on this call. Delivery is optional: deliver_via none (default — you get a signed download link), whatsapp (a document to the phone in deliver_to, which must already have a WhatsApp conversation with this business), email (an attachment to the address in deliver_to; deliver_subject optional) or sms (the link, texted to deliver_to). Sending needs the "Send messages and place calls" tick on this connection. Files expire after 72 hours; get_data_export answers the current status and link.

    Required permissions: data.view

    table_idstring group_idstring definitionstring report_idstring rangestring formatstring titlestring deliver_viastring deliver_tostring deliver_captionstring deliver_subjectstring
    Argument schema and validation
    table_idstringoptional
    Table id or slug (table report).
    group_idstringoptional
    Group id or slug (group report) — instead of table_id.
    definitionstringoptional
    JSON report definition (see run_data_report / run_data_group_report).
    report_idstringoptional
    A saved report id, instead of a definition.
    rangestringoptional
    JSON date_range override: {"relative":"last_30_days"} or {"from":"2026-08-01","to":"2026-08-31"}.
    formatstringoptional
    pdf (default), xlsx or csv.
    titlestringoptional
    Title on the file. Default: the saved report's name, or one derived from the definition.
    deliver_viastringoptional
    none (default), whatsapp, email or sms.
    deliver_tostringoptional
    Phone (whatsapp/sms) or email address the file goes to.
    deliver_captionstringoptional
    Caption under a WhatsApp document.
    deliver_subjectstringoptional
    Email subject (default: the title).
  • reorder_data_columns v0343b235 writes Put a table's fields in a chosen order — the column order of the grid, the form and every export. Pass the fields (keys or ids) first-to-last; fields you leave out keep their relative order after the ones you named. Purely cosmetic: keys, types, values and indexes are untouched, so flows are unaffected. Use it when the person wants the name column first or the notes last.

    Required permissions: data.manage

    table_idstring · required orderstring · required
    Argument schema and validation
    table_idstringrequired
    Table id or slug.
    orderstringrequired
    JSON list (or comma list) of field keys or ids, first to last.
  • reorder_data_group_tables ve5b62339 writes Put the tables inside a group in a chosen order — the order the group page, its overview cards and the Data page show them in. Pass the member tables (ids or slugs) first-to-last; members you leave out keep their relative order after the ones you named, and a table that is not in this group is ignored (use set_data_table_group to move it in first). Purely cosmetic: no field, record or report changes.

    Required permissions: data.manage

    group_idstring · required orderstring · required
    Argument schema and validation
    group_idstringrequired
    Group id or slug from list_data_groups.
    orderstringrequired
    JSON list (or comma list) of table ids or slugs, first to last.
  • reorder_data_groups vcac3bf66 writes Put the table groups (folders) of this workspace in a chosen order — the order their sections appear in on the Data page. Pass the groups (ids or slugs) first-to-last; groups you leave out keep their relative order after the ones you named. Purely cosmetic: nothing inside any group changes. Use reorder_data_group_tables for the tables within one group.

    Required permissions: data.manage

    orderstring · required
    Argument schema and validation
    orderstringrequired
    JSON list (or comma list) of group ids or slugs, first to last.
  • request_data_column_index v48a6c824 writes Ask for an index on a field, or for a "unique together" rule over several fields. One field: pass column and kind — "btree" makes the field sortable and fast to filter on big tables (query_data_records refuses to sort on an unindexed field past the sort threshold); "unique" additionally forbids two records with the same value and is what upsert_data_record matches on — a phone, an order reference. Several fields: pass columns as a list of 2 to 4 field keys instead of column, and no two records may share that whole combination while each value on its own may repeat — one booking per room per day, one enrolment per student per course. A unique request is refused up front when duplicates already exist, naming up to ten of them; a field can hold one index, and asking for the other kind replaces it; a set that already exists is returned unchanged. The index is built in the background: the answer carries index_status pending, and get_data_table_schema shows it move to ready (or failed, with the reason) and lists the sets under unique_sets. Counts against the per-table and per-workspace index quotas — a set costs one slot.

    Required permissions: data.manage

    table_idstring · required columnstring columnsstring kindstring
    Argument schema and validation
    table_idstringrequired
    Table id or slug.
    columnstringoptional
    The field key or id, for an index on ONE field. Leave out when passing columns.
    columnsstringoptional
    For a "unique together" rule: 2 to 4 field keys as a JSON list ["room","day"] or a comma list. No two records may then share that whole combination. Leave out when passing column.
    kindstringoptional
    "btree" for sorting and filtering, or "unique" to forbid duplicate values. Required with column; a set of columns is always unique.
  • run_data_action v70945f49 writes Run a record action on one or more records: what a person gets by pressing the action button in the grid. list_data_actions shows the actions a table defines and what each does. Pass record_ids as a JSON list or comma list (up to 200; a row-scoped action takes one). Answers a line per record — {id, ok, message, url} — and a summary; an export answers the file too. Needs "Save and change records"; an action of kind flow or webhook also needs "Send messages and place calls" on this connection, because it reaches outside the account.

    Required permissions: data.view

    table_idstring · required action_idstring · required record_idsstring · required
    Argument schema and validation
    table_idstringrequired
    Table id or slug.
    action_idstringrequired
    The action id from list_data_actions.
    record_idsstringrequired
    JSON list or comma list of record ids (up to 200).
  • save_data_group_report vdeb4b11d writes Save a cross-table report so it appears in the group's Reports tab for everyone, or update one by report_id (name, description, definition, pinned). A group report definition is JSON: {"series":[{"table_id":"<member table id or slug>","metric":{"fn":"sum","column":"amount"},"filters":<condition tree or null>,"label":"Orders","breakdown":{"column":"status","top":5},"date_column":"paid_at"},{"table_id":"…","metric":{"fn":"count"},"label":"Customers"},{"label":"Orders per customer","formula":"orders / customers"}],"dimension":{"column":"$created_at","bucket":"day|week|month|quarter"} (one shared time axis; rows {bucket, series:{label: value}, drill:{label: {table_id, filter, range}}}) or {"kind":"table"} (one row per series: {label, table_id, value, drill}) or null (one number per series),"date_range":{"relative":"last_30_days"} or {"from":"2026-08-01","to":"2026-09-01"},"chart":"number|line|bar|stacked_bar|donut|table","compare":"previous_period"|"previous_year" (optional),"sort":{"by":"value|label","dir":"asc|desc"} and "limit" (by-table reports only). A formula series names other series by their slugified label (Orders → orders; "Kiasi (TZS)" → kiasi_tzs) and may use + - * / parentheses and percent_of(a,b). Every table_id must be a member of the group; each series is validated against its own table. A measure with "as":"percent_of_total" also answers its share of the total. The definition is validated against the group before it is stored.

    Required permissions: data.reports.manage

    group_idstring · required report_idstring namestring descriptionstring definitionstring is_pinnedboolean
    Argument schema and validation
    group_idstringrequired
    Group id or slug.
    report_idstringoptional
    Update this saved report instead of creating one.
    namestringoptional
    Report name (required when creating).
    descriptionstringoptional
    One line on what it shows.
    definitionstringoptional
    JSON group report definition (required when creating).
    is_pinnedbooleanoptional
    Pin it to the top of the Reports tab.
  • save_data_report v56f91062 writes Save a report so it appears in the table's Reports tab for everyone, or update one by report_id (name, description, definition, pinned). A report definition is JSON: {"metrics":[{"fn":"sum","column":"amount"},{"fn":"count"}],"dimension":{"column":"region"} or {"column":"$created_at","bucket":"week"} or null,"filters":<condition tree or null>,"date_range":{"relative":"last_30_days"} or {"from":"2026-08-01","to":"2026-09-01"},"chart":"number|line|bar|stacked_bar|donut|table"}. fn: count, count_distinct, sum, avg, min, max (the last four need a number/currency column). A dimension may be a select, boolean or relation field, or a date field with bucket day|week|month|quarter. No dimension gives one number. Optional keys: "breakdown":{"column":"status","top":5} splits every row into per-value series (rows gain series:{value:{metrics}}, values past top fold into "other"); "compare":"previous_period"|"previous_year" re-runs the same query over the preceding window and adds previous/delta/delta_pct per metric on every row; a measure {"fn":"formula","expr":"sum_amount / count","label":"Average order"} is computed per row from the other measures' keys (+ - * / parentheses, percent_of(a,b); division by zero gives null); "dimension":{"column":"region","top":6} with "sort":{"by":"sum_amount","dir":"desc"} and "limit":20 cut a category dimension; a measure with "as":"percent_of_total" also answers <key>_pct per row. Example: {"metrics":[{"fn":"sum","column":"amount"},{"fn":"count"},{"fn":"formula","expr":"sum_amount / count","label":"Average"}],"dimension":{"column":"$created_at","bucket":"week"},"compare":"previous_period","date_range":{"relative":"last_90_days"},"chart":"line"}. The definition is validated against the table before it is stored.

    Required permissions: data.reports.manage

    table_idstring · required report_idstring namestring descriptionstring definitionstring is_pinnedboolean
    Argument schema and validation
    table_idstringrequired
    Table id or slug.
    report_idstringoptional
    Update this saved report instead of creating one.
    namestringoptional
    Report name (required when creating).
    descriptionstringoptional
    One line on what it shows.
    definitionstringoptional
    JSON report definition (required when creating).
    is_pinnedbooleanoptional
    Pin it to the top of the Reports tab.
  • schedule_data_report v65c1a73e writes Create, change or remove a standing export: the same spec rendered daily, weekly or monthly at a local time and delivered by WhatsApp, email or SMS (or just kept on the Exports page with deliver via none). spec is JSON in the export_data_records / export_data_report shape: {"kind":"records","table_id":"…","filter":…,"columns":[…],"format":"xlsx","title":"…"} or {"kind":"table_report","table_id":"…","definition":{…}|"saved_report_id":"…","format":"pdf"} or {"kind":"group_report","group_id":"…","definition":{…}}. deliver is JSON {"via":"whatsapp|email|sms|none","to":"…","subject":"…","caption":"…"} — whatsapp and sms need to; the phone must already have a WhatsApp conversation for whatsapp. Pass schedule_id to change or (with delete = true) remove one; omit it to create. Without any argument but list = true, answers the existing schedules.

    Required permissions: data.reports.manage

    schedule_idstring listboolean deleteboolean namestring specstring cadencestring atstring weekdayinteger dayinteger timezonestring deliverstring enabledboolean
    Argument schema and validation
    schedule_idstringoptional
    An existing schedule to change or delete; omit to create.
    listbooleanoptional
    true: just list the schedules.
    deletebooleanoptional
    true with schedule_id: remove it.
    namestringoptional
    What this schedule is for, e.g. "Weekly orders to the owner".
    specstringoptional
    JSON export spec (see the tool description).
    cadencestringoptional
    daily, weekly or monthly.
    atstringoptional
    Local time HH:MM, e.g. 08:00.
    weekdayintegeroptional
    Weekly: 1 (Monday) to 7 (Sunday).
    dayintegeroptional
    Monthly: day of month 1–28.
    timezonestringoptional
    IANA zone, default Africa/Dar_es_Salaam.
    deliverstringoptional
    JSON {"via":"whatsapp|email|sms|none","to":"…","subject":"…","caption":"…"}.
    enabledbooleanoptional
    false pauses the schedule.
  • set_data_record_file_from_url v27208ec0 writes Put a file into a file field of one record by fetching it from a URL: the server downloads it, keeps a copy in this workspace and stores it on the record. Pass table_id, record_id, the file field's column key, and an http(s) url (a document link, a voicemail recording, a public image); a WhatsApp media id works too. Optional name sets the file name shown. A field that takes one file is replaced; a field that takes many gets the file added. The field's allowed types and size cap apply and a refusal names the field. The answer is the record with signed one-hour links (url, thumb) on every file value.

    Required permissions: data.records.edit

    table_idstring · required record_idstring · required columnstring · required urlstring · required namestring
    Argument schema and validation
    table_idstringrequired
    Table id or slug.
    record_idstringrequired
    The record id.
    columnstringrequired
    Key of the file field.
    urlstringrequired
    http(s) URL of the file, or a WhatsApp media id.
    namestringoptional
    File name to show (optional; taken from the URL otherwise).
  • set_data_table_grant va578823b writes Decide who may see one table. subject_type is "role" (a role name like manager or agent), "team" (an agent group id) or "user" (a person id); level is none, view, edit or manage. Pass remove:true to take one line away. THE FIRST LINE ON A TABLE CHANGES IT for everybody: until then the table is unrestricted and whoever holds the Data permission sees it, and afterwards only the people named do. A line can only narrow what somebody already holds — granting "manage" to a viewer does not let them edit — and the account owner always keeps full access. get_data_table_governance reads the lines back and lists the roles, teams and people that exist.

    Required permissions: data.manage

    table_idstring · required subject_typestring · required subject_idstring · required levelstring removeboolean
    Argument schema and validation
    table_idstringrequired
    The table id (uuid) or slug from list_data_tables.
    subject_typestringrequired
    role, team or user.
    subject_idstringrequired
    A role name, an agent group id, or a client user id. get_data_table_governance lists what exists.
    levelstringoptional
    none, view, edit or manage. Ignored when remove is true.
    removebooleanoptional
    Take this access line away instead of setting it.
  • set_data_table_group v1b04699f writes Move a table into a group, or out of any group (omit group_id, or pass an empty one). A table belongs to at most one group; moving it out of one folder into another is one call. Nothing about the table's fields or records changes — only where it is filed and which group reports can read it.

    Required permissions: data.manage

    table_idstring · required group_idstring
    Argument schema and validation
    table_idstringrequired
    Table id or slug.
    group_idstringoptional
    Group id or slug to move the table into; leave empty to take it out of its group.
  • set_data_table_retention v4b2a90fe writes Set how long a table keeps its records. days is the age at which a record is past the window; action is "delete" (soft-delete the record) or "anonymise" (keep the row and empty the fields named in field_rules, so the counts still work but the person is out of them). field_rules is {"<field key>": "clear"|"redact"|"hash"} — redact and hash only fit text and email fields, everything else can only be cleared, and a required field cannot be cleared. Pass legal_hold:true to suspend the whole policy: the nightly sweep will record that it ran and changed nothing, which is what a hold has to look like. Pass remove:true to drop the policy. The sweep runs nightly; it never runs from this tool.

    Required permissions: data.manage

    table_idstring · required daysinteger actionstring date_columnstring field_rulesstring enabledboolean legal_holdboolean removeboolean
    Argument schema and validation
    table_idstringrequired
    The table id (uuid) or slug from list_data_tables.
    daysintegeroptional
    Keep records this many days (1–3650), then act.
    actionstringoptional
    delete or anonymise.
    date_columnstringoptional
    Count the age from $created_at (default), $updated_at, or a date field of the table.
    field_rulesstringoptional
    JSON object for anonymise: {"phone":"clear","notes":"redact","email":"hash"}.
    enabledbooleanoptional
    Set false to keep the policy but stop the sweep.
    legal_holdbooleanoptional
    Suspend every sweep of this table while true.
    removebooleanoptional
    Drop the retention policy entirely.
  • transition_data_record v70bef6dc writes Move one record to another state — a booking to confirmed, an order to paid, a ticket to closed. Only the moves the table's owner declared are allowed: get_data_table_states says which state the record may go to next, and an illegal move (paid back to draft) is refused as a conflict with the legal ones named, nothing saved. Pass a reason and it goes in the record's history beside the change. Moving a record that is already in that state is not an error: it answers changed = false.

    Required permissions: data.records.edit

    table_idstring · required record_idstring · required tostring · required reasonstring columnstring
    Argument schema and validation
    table_idstringrequired
    Table id or slug.
    record_idstringrequired
    The record id.
    tostringrequired
    The state key to move into, from get_data_table_states (its label also works).
    reasonstringoptional
    Why, in one line. Kept on the history entry so a person reading the trail later knows.
    columnstringoptional
    Which status field, when the table has more than one. Left out, the table's only status field is used.
  • update_data_column v135af39a writes Change a field: its label, key, type, required flag or config. Only the arguments you pass change. Renaming the key or changing the type is refused while the field has an index — call drop_data_column_index on it first, then ask for the index again afterwards; changing a type keeps old values, and values that no longer fit read as empty. Types: text, long_text, number, currency, boolean, date, datetime, phone, email, select, multi_select, status, relation, file, auto_number. Config by type — number/currency: {precision, unit|currency, min, max}; select/multi_select: {options:[{key,label,color}]}; status: {states:[{key,label,color,initial,final}],transitions:[{from:'<key>|*',to:'<key>',label,requires}],strict:true} — a state machine, so a record may only be created in an initial state and only move along a declared transition (get_data_table_states reads one back); relation: {target_table_id}; phone: {default_region:"TZ"}; auto_number: {prefix:"ORD-", pad:6, yearly:false} (the platform assigns the value; never send one); any: {default, ui:{is_title_field, is_summary_metric, help_text, placeholder, hidden_in_grid, hidden_in_form}}.

    Required permissions: data.manage

    table_idstring · required columnstring · required labelstring keystring typestring requiredboolean configstring
    Argument schema and validation
    table_idstringrequired
    Table id or slug.
    columnstringrequired
    The field key or id.
    labelstringoptional
    New label.
    keystringoptional
    New key (refused while indexed).
    typestringoptional
    New type (refused while indexed).
    requiredbooleanoptional
    Whether a value is required.
    configstringoptional
    JSON object replacing the type settings.
  • update_data_group v4940fcab writes Rename or restyle a table group (folder): its name, one-line description, emoji icon or palette colour. Only the arguments you pass change; the slug, the member tables and the group's saved reports stay. Use it when the person wants the folder called something else or coloured differently — to add or remove tables use set_data_table_group, to change their order use reorder_data_group_tables.

    Required permissions: data.manage

    group_idstring · required namestring descriptionstring iconstring colorstring
    Argument schema and validation
    group_idstringrequired
    Group id or slug from list_data_groups.
    namestringoptional
    New human name (at most 80 characters).
    descriptionstringoptional
    New one-line description; pass an empty string to clear it.
    iconstringoptional
    New emoji icon; pass an empty string to clear it.
    colorstringoptional
    A palette key (gray, red, orange, amber, yellow, lime, green, emerald, teal, cyan, sky, blue, indigo, violet, purple, fuchsia, pink, rose); pass an empty string to clear it.
  • update_data_record vdf2cf0c4 writes Change some fields of one record by id. Only the keys you pass change; pass null to clear a field. Values are checked against each field's type: numbers as numbers, booleans as true/false, dates as YYYY-MM-DD, datetimes as ISO-8601, phones in any Tanzanian form (0712…, +255…), select values as option keys. A required field missing, a unique field clashing, or a value of the wrong shape is refused with the field named, and nothing is saved.

    Required permissions: data.records.edit

    table_idstring · required record_idstring · required datastring · required
    Argument schema and validation
    table_idstringrequired
    Table id or slug.
    record_idstringrequired
    The record id.
    datastringrequired
    JSON object of the fields to change.
  • update_data_table v8a4cba00 writes Rename or re-describe a table: its human name, one-line description or emoji icon. Only the arguments you pass change; the slug, the fields and the records stay exactly as they are, so flows keep working. Use it when the person wants "Wateja" called "Customers" or wants a table to explain itself on the Data page — not to change fields (update_data_column) or to move it into a folder (set_data_table_group).

    Required permissions: data.manage

    table_idstring · required namestring descriptionstring iconstring
    Argument schema and validation
    table_idstringrequired
    Table id or slug.
    namestringoptional
    New human name (at most 80 characters).
    descriptionstringoptional
    New one-line description; pass an empty string to clear it.
    iconstringoptional
    New emoji icon; pass an empty string to clear it.
  • upsert_business_rule v5bd88e37 writes Create or change a business rule — what this business enforces, everywhere at once. A rule saved here is read by every message flow, phone menu and assistant that asks for its key, so changing one changes real outcomes for real customers: what they are charged, how often they may do something, whether they qualify, and when you are open. Confirm the numbers with the account owner before you save. Pass the key to change an existing rule; the kind of an existing rule cannot change, because callers depend on the shape of its answer. `definition` is a JSON object whose shape depends on the kind: - limit: {max, per: hour|day|week|month|rolling|total, rolling_seconds?, table_id?, subject_column?, subject_input?, where?, used_input?, reason?} - fee: {expression: "amount * 0.03", tiers?: [{above, expression}], tier_input?, min?, max?, round?, currency?} - eligibility: {subject: inputs|record, table_id?, match_column?, match_input?, record_id_input?, condition: {all: [{column, op, value}]}, reason?} - window: {source: schedule|ivr_schedule|inline, schedule_id?, ivr_schedule_id?, timezone?, slots?: [{day, start, end}], holidays?: [{date, name}], reason?} - choice: {input, map: {value: outcome}, rules?: [{when, then}], default?} A definition that cannot run is refused with the field named, and nothing is saved.

    Required permissions: rules.manage

    keystring · required labelstring kindstring definitionstring descriptionstring enabledboolean
    Argument schema and validation
    keystringrequired
    The name a flow asks for, e.g. daily_withdrawals. Pass an existing key to change that rule.
    labelstringoptional
    What a person reads, e.g. "Daily withdrawals".
    kindstringoptional
    limit, fee, eligibility, window or choice. Required when creating; cannot change afterwards.
    definitionstringoptional
    A JSON object shaped for the kind — see the description.
    descriptionstringoptional
    One sentence on why this rule exists.
    enabledbooleanoptional
    Whether the rule is enforced (default true).
  • upsert_data_record v51a83380 writes Create or update a record matched on one unique field — the right call for "save this customer by phone". match_column must be a field marked unique in get_data_table_schema; data must contain it. If a record with that value exists it is updated with the other keys, otherwise one is created. Values are checked against each field's type: numbers as numbers, booleans as true/false, dates as YYYY-MM-DD, datetimes as ISO-8601, phones in any Tanzanian form (0712…, +255…), select values as option keys. A required field missing, a unique field clashing, or a value of the wrong shape is refused with the field named, and nothing is saved. Pass an idempotency_key (any string you make up, e.g. a UUID) when a retry must not create a second copy: the first successful answer is kept for 24 hours and replayed for the same key, so a call that timed out can be repeated safely.

    Required permissions: data.records.edit

    table_idstring · required match_columnstring · required datastring · required idempotency_keystring
    Argument schema and validation
    table_idstringrequired
    Table id or slug.
    match_columnstringrequired
    The unique field to match on, e.g. phone.
    datastringrequired
    JSON object including the match field.
    idempotency_keystringoptional
    Optional: a key you make up so a retried call is replayed rather than run twice (kept 24 h).
Approvals /mcp/v1/approvals 2 read · 1 write

Decisions a person has been asked for before something happens: read the queue, read one in full with every comment on it, answer one.

  • get_approval vcc487b47 Read one approval in full: what is being asked and why, the amount if there is one, who it is waiting on, every decision already recorded with its comment, and when it runs out of time.

    Required permissions: approvals.view

    approval_idinteger · required
    Argument schema and validation
    approval_idintegerrequired
    The approval to read.
  • list_approvals v8da9cca8 List approval requests in this workspace: what is waiting on you right now, what has been settled, and who said what. Defaults to the ones waiting on you, because that is the only list anybody can act on.

    Required permissions: approvals.view

    waiting_on_meboolean statestring kindstring limitinteger
    Argument schema and validation
    waiting_on_mebooleanoptional
    Only approvals this person may answer right now. Defaults to true when no state is given.
    statestringoptional
    pending | approved | rejected | expired | all. Ignored when waiting_on_me is true.
    kindstringoptional
    Only approvals of this kind, as the requester labelled them.
    limitintegeroptional
    At most this many, max 100.
  • decide_approval v0b1c26a1 writes Approve or reject one approval, as the person this connection belongs to, with an optional comment. This is a real-world act: whatever was held waiting for a person now proceeds, or does not. Read the approval first and say what you are about to do.

    Required permissions: approvals.decide

    approval_idinteger · required decisionstring · required commentstring
    Argument schema and validation
    approval_idintegerrequired
    The approval to answer.
    decisionstringrequired
    approve or reject.
    commentstringoptional
    Why. Kept on the record and shown to whoever asked. Say something: a bare refusal helps nobody.
Payments /mcp/v1/payments 2 read · 2 write

Money this business collects from its customers: what has been asked for and where each one got to, one payment's whole timeline, asking a customer to pay, and refunds. Not the business's own Momo bill.

  • get_payment v0d023fe6 Read one payment in full: the amount, who was asked, where it got to, everything that has happened to it in order, what it wrote in the books, and any refunds raised against it. Accepts the payment id or its human reference like PAY-20260908-0042.

    Required permissions: payments.view

    payment_idstring · required
    Argument schema and validation
    payment_idstringrequired
    The payment id, or its reference (PAY-YYYYMMDD-NNNN).
  • list_payments vcb38d765 List payments this business has asked its customers for: what was asked, who was asked, and where each one got to. Defaults to the ones still waiting on a customer, because those are the only ones anybody can act on. Amounts come back both as a whole number of the smallest unit and as a formatted string — never divide or multiply them yourself.

    Required permissions: payments.view

    statestring subject_idstring limitinteger
    Argument schema and validation
    statestringoptional
    open (the default) | all | draft | pending | authorised | paid | failed | expired | cancelled | refunded | partly_refunded.
    subject_idstringoptional
    Only payments raised for this record — an order id, a Daftari record id, an invoice number.
    limitintegeroptional
    At most this many, max 100.
  • create_payment_intent v8a37cd6f writes Ask a customer to pay: create a payment and, unless told not to, send the request — a mobile money prompt to their phone, or a checkout link to give them. This asks a real person for real money, so only use it when the customer has agreed to pay now. The amount is a whole number of the currency's smallest unit (40000 is TZS 400.00), never a decimal.

    Required permissions: payments.collect

    amount_minorinteger · required currencystring payer_namestring payer_phonestring payer_emailstring methodstring subject_typestring subject_idstring sendboolean idempotency_keystring
    Argument schema and validation
    amount_minorintegerrequired
    A whole number of the smallest currency unit. 40000 means TZS 400.00. Never a decimal.
    currencystringoptional
    ISO code, e.g. TZS. Defaults to TZS.
    payer_namestringoptional
    Who is paying, as they would like to be addressed.
    payer_phonestringoptional
    Their mobile money number. Required for a phone prompt.
    payer_emailstringoptional
    Their email, for a card or bank checkout receipt.
    methodstringoptional
    ussd_push (a PIN prompt on their phone), link (a checkout page) or lipa (a short number they pay from any wallet app). Defaults to link.
    subject_typestringoptional
    What is being paid for, e.g. an order or table name.
    subject_idstringoptional
    The id of the thing being paid for.
    sendbooleanoptional
    Ask the customer now. True by default; pass false to write the payment down without contacting anybody.
    idempotency_keystringoptional
    Your own key for this request. The same key always returns the same payment rather than asking twice.
  • refund_payment v4dfb4eed writes Give a customer their money back, in full or in part. This raises a refund and asks a person in the business to approve it before any money moves — you cannot complete a refund on your own, and that is deliberate. Read the payment first with get_payment, say plainly what you are about to refund and why, and let the person decide.

    Required permissions: payments.refund

    payment_idstring · required reasonstring · required amount_minorinteger approver_user_idsarray
    Argument schema and validation
    payment_idstringrequired
    The payment to refund — its id or its reference (PAY-YYYYMMDD-NNNN).
    reasonstringrequired
    Why. Kept on the record, shown to whoever approves it, and the first thing anybody asks about a refund.
    amount_minorintegeroptional
    A whole number of the smallest currency unit to give back. Leave it out to refund everything still refundable.
    approver_user_idsarray<integer>optional
    Who should be asked to approve. Defaults to the owners and managers of the workspace.
Automations /mcp/v1/automations 2 read · 2 write

What happens without anybody there: the log of what has actually happened in the business, the subscriptions that react to it, and the schedules that run on a rhythm.

  • list_business_events v5e04c0de What has actually happened in this business, newest first: records created, changed and moved between states, payments settled, approvals decided. This is the log an automation acts on, so it is also the place to look when somebody asks why something fired — or why it did not. Each row says what happened, what it was about, who did it (a person, an API key, an assistant, a flow, a schedule), and how many subscriptions acted on it. `delivered_count: 0` with a `delivered_at` means nothing was listening — that is the usual reason "the automation did not run". Filter by `key` for one kind of event, or by `subject_id` to read everything that ever happened to one record. The reply also carries the closed list of event keys this platform publishes, so you never have to guess one.

    Required permissions: automations.view

    keystring subject_idstring sincestring limitinteger
    Argument schema and validation
    keystringoptional
    Only this event, e.g. record.transitioned or payment.paid.
    subject_idstringoptional
    Everything that ever happened to one thing — a record id, an approval id.
    sincestringoptional
    Only events at or after this time, e.g. 2026-09-08T00:00:00Z.
    limitintegeroptional
    How many to return (default 25, max 100).
  • list_event_subscriptions v80b9e3de Everything this business has arranged to happen without a person: the subscriptions that react to events ("when an order is marked paid, start this flow") and, with `include_schedules`, the schedules that run on a rhythm ("send the sales report every Monday at 09:00"). Read this before writing a new one — a business that already forwards paid orders to its warehouse does not want a second subscription doing the same thing, and a schedule that has been failing for a week is usually the actual answer to "why did nothing arrive". Each row carries `fire_count`, `last_fired_at`, and `last_error` when the last attempt failed. A subscription switched off by repeated failures says so in `last_error`; switching it back on clears the counter. Signing secrets are never returned. `signed: true` says a webhook is signed; the secret itself is shown once, on the Automations page.

    Required permissions: automations.view

    keystring kindstring enabled_onlyboolean include_schedulesboolean limitinteger
    Argument schema and validation
    keystringoptional
    Only subscriptions listening for this event (wildcard rows are always included).
    kindstringoptional
    Only this kind: flow, notification, webhook or agent.
    enabled_onlybooleanoptional
    Leave out the ones that are switched off.
    include_schedulesbooleanoptional
    Also return the schedules that run on a rhythm.
    limitintegeroptional
    How many subscriptions to return (default 50, max 200).
  • upsert_event_subscription v6ab49195 writes Create, change or remove a subscription: "when this happens in the business, do that". What you save here acts on real events without anybody watching, so confirm the details with the account holder before saving one — especially a webhook, which sends this business's data to an address outside it. `key` is one of the events this platform publishes (list_business_events returns them all), or `*` for every event. `kind` decides what `target` means and cannot change once saved: - flow — target is a flow id. The event must name a conversation for the flow to talk into; set config.conversation_path to the field that carries one. - notification — target is who to tell: user ids separated by commas, a role name, or * for everybody. It goes through the notification engine, so people's own preferences, quiet hours and opt-outs all still apply. - webhook — target is an https URL. Every delivery is signed (HMAC-SHA256 over "<timestamp>.<raw body>" in the X-Momo-Signature header) and retried on a temporary failure; the secret is shown once, on the Automations page. Addresses inside our own network are refused. - agent — target is an assistant id, and config.prompt is the standing instruction it gets. `filter` narrows it to matching events, written as a condition over the event: {"all":[{"column":"record.status","op":"equals","value":"paid"}]}. Read fields with dots — record.status, table.slug, changes.status.to. Pass `id` to change one, or `id` with `delete: true` to remove it. A subscription that has failed ten times in a row switches itself off; saving it with enabled: true clears that.

    Required permissions: automations.manage

    idinteger deleteboolean keystring kindstring targetstring labelstring filterstring configstring enabledboolean rotate_secretboolean
    Argument schema and validation
    idintegeroptional
    An existing subscription to change or delete; omit to create.
    deletebooleanoptional
    With id, remove that subscription.
    keystringoptional
    The event to listen for: record.created, record.updated, record.deleted, record.transitioned, payment.paid, payment.failed, payment.refunded, order.completed, approval.requested, approval.settled, booking.confirmed, ticket.opened, ticket.closed, call.completed, message.received, operation.completed, operation.failed, or * for all.
    kindstringoptional
    flow, notification, webhook or agent. Required when creating; cannot change afterwards.
    targetstringoptional
    What to act on — a flow id, who to tell, a URL, or an assistant id. See the description.
    labelstringoptional
    What a person reads, e.g. "Paid orders to the warehouse".
    filterstringoptional
    A JSON condition over the event; leave out to fire on every one.
    configstringoptional
    A JSON object of per-kind extras — title/body for a notification, prompt for an agent, conversation_path for a flow.
    enabledbooleanoptional
    Whether it is switched on (default true).
    rotate_secretbooleanoptional
    For a webhook: issue a new signing secret. The old one stops working immediately.
  • upsert_schedule va6a45b47 writes Create, change or remove a schedule: something this business does again, on a rhythm, with nobody there. A schedule sends real messages, places real calls and writes real rows, so confirm the times and the recipients with the account holder before saving one. `kind` decides what it does and cannot change once saved: - report — render an export and deliver it. payload: {"export": {…the export_data_records / export_data_report spec…}, "deliver": {"via":"whatsapp|email|sms|none","to":"…"}} - record — write a row into a table. target is the table id; payload: {"record": {field: value}}. {{slot_date}} and {{slot_time}} become the run's own date and time. - message — send one SMS or WhatsApp. target is the phone number; payload: {"channel":"sms|whatsapp","body":"…"} - flow — start a message flow. target is the flow id; payload: {"conversation_id": 123} - call — ring everybody in a contact group. target is the contact group id; payload: {"call_mode":"ai|human","agent_config_id":…} `spec` is the rhythm: {"every":1,"unit":"minutes|hours|days|weeks|months","at":"08:00","weekdays":[1,3,5],"day_of_month":1,"timezone":"Africa/Dar_es_Salaam","until":"2026-12-31","count":10}. `at` applies to days, weeks and months; `weekdays` (1 = Monday) to weeks; `day_of_month` (1–28) to months. The shortest interval is every 5 minutes. Left out, the timezone is the account's own. `misfire_policy` says what happens to runs missed while the platform was down: run_once (fire once and carry on — the default and almost always right), skip (do not fire at all), run_all (catch up, capped). Sixty missed minutes must never become sixty messages. Pass `id` to change one, or `id` with `delete: true` to remove it.

    Required permissions: automations.manage

    idstring deleteboolean namestring kindstring specstring targetstring payloadstring misfire_policystring enabledboolean
    Argument schema and validation
    idstringoptional
    An existing schedule to change or delete; omit to create.
    deletebooleanoptional
    With id, remove that schedule.
    namestringoptional
    What a person reads, e.g. "Monday sales report".
    kindstringoptional
    report, record, message, flow or call. Required when creating; cannot change afterwards.
    specstringoptional
    A JSON object describing the rhythm — see the description.
    targetstringoptional
    What it acts on: a table id, a phone number, a flow id, a contact group id. Not used by report.
    payloadstringoptional
    A JSON object of the kind's own arguments — see the description.
    misfire_policystringoptional
    run_once (default), skip or run_all.
    enabledbooleanoptional
    Whether it runs (default true).
Alerts & service levels /mcp/v1/alerts 4 read · 3 write

The business watching itself: the alert rules it wrote, the service-level promises and the clocks running against them, the risk rules that hold or refuse an action, and one log of everything that fired — including what reached nobody.

  • list_alert_rules v25f90218 What this business has asked to be watched: its alert rules, and — with `include_policies` — its service-level promises and risk rules too, since a person asking "what are we watching for?" means all three. Each alert rule carries `describes`, one checkable sentence ("Tell me when failed notifications goes over 5 in an hour"), and `last_value` against `threshold`, which is what it is reading right now. That lets you answer "it is at three of ten" instead of only "it has not fired" — a healthy rule and a broken one produce the same silence otherwise. A rule with `last_error` set is BROKEN, not quiet: it could not be measured at all. Say that plainly rather than reporting it as fine. `dedupe_minutes` is how long a rule stays quiet after firing, defaulting to its window. It is why a rule fires once about a bad hour rather than twelve times. The reply also carries the closed list of alert kinds this platform can measure, so you never have to guess one.

    Required permissions: alerts.view

    enabled_onlyboolean include_policiesboolean limitinteger
    Argument schema and validation
    enabled_onlybooleanoptional
    Only rules that are switched on.
    include_policiesbooleanoptional
    Also return the service-level promises and the risk rules.
    limitintegeroptional
    How many alert rules to return (default 50, max 200).
  • list_alerts v7975b73d Everything this business's own watching has raised, newest first: alert rules that went over a limit, service levels that were warned about, breached or escalated, and risk rules that flagged or refused something. All three engines write here, so this is the one place to answer "did anything go wrong?". Read the `delivery` block on every row. An alert whose state is not `delivered` FIRED and reached nobody, and `delivery.reason` says which of the three reasons it was: nobody was named or nobody named is still active, no channel is switched on for this workspace, or the send itself failed. That is nearly always the real answer when somebody says the alerting is not working — the noticing worked and the telling did not. `undelivered_only: true` narrows to exactly those. Start there when the complaint is "I was never told".

    Required permissions: alerts.view

    kindstring rule_keystring undelivered_onlyboolean sincestring limitinteger
    Argument schema and validation
    kindstringoptional
    Only this kind, e.g. notification_failure, sla_breach, risk_block.
    rule_keystringoptional
    Only alerts raised by this rule or policy key.
    undelivered_onlybooleanoptional
    Only the ones that fired and reached nobody.
    sincestringoptional
    Only alerts at or after this time, e.g. 2026-09-09T00:00:00Z.
    limitintegeroptional
    How many to return (default 25, max 100).
  • list_risk_decisions v37a24f19 Every risk decision this business made, newest first: what was allowed, what was flagged, what was held for a person to approve, and what was refused outright. Every row carries its own explanation and you should quote it rather than guess. `inputs` is what the engine was given — the amount, the customer, the device, the country. `signals` is every rule that ran, what it READ, what it compared that against, and whether it tripped. When somebody asks why a payment was refused, the answer is in those two fields and nowhere else. `decision: hold` means the action has NOT gone ahead and has NOT been refused: somebody is being asked, and `approval_id` names the approval they are answering. Never report a hold as an allow. A signal marked `errored` did not find anything — it could not be read at all — and is deliberately never treated as a trip. A decision made while a signal was erroring is worth mentioning.

    Required permissions: alerts.view

    decisionstring subject_idstring limitinteger
    Argument schema and validation
    decisionstringoptional
    allow, alert, hold or block.
    subject_idstringoptional
    Everything ever decided about one thing — a payment id, a record id.
    limitintegeroptional
    How many to return (default 25, max 100).
  • list_sla_clocks vb030173a What is running against a service-level promise right now, worst first — the orders, tickets and records whose clock is closest to running out. `pct_used` is the number that matters: 80 means four fifths of the promised time is gone. `state` is where it has got to — running, warned, breached, escalated. `elapsed_minutes` counts only the time the clock was actually running, so under a working-hours policy a weekend adds nothing to it, and comparing `started_at` with the wall clock will disagree with this number on purpose. `outcome.transition` appears on a breached clock whose policy moves the record on. When `applied` is false the state machine REFUSED the move and `refused` says why — the breach still stands, and the record did not move. That is a fact somebody needs to know rather than assume.

    Required permissions: alerts.view

    statestring policy_keystring limitinteger
    Argument schema and validation
    statestringoptional
    running, warned, breached, escalated, met or stopped. Omit for everything still ticking.
    policy_keystringoptional
    Only clocks under this policy.
    limitintegeroptional
    How many to return (default 25, max 100).
  • upsert_alert_rule v71beb6d1 writes Create or change an alert rule: what to watch, what counts as too much, over what window, and who to tell. Matched on `key` — the same key updates the rule that already has it. Say plainly what you are about to create before you save it: what is watched, the limit, the window, and who will be woken. It fires against real traffic from the moment it is saved. Two things people get wrong and you should state rather than let them discover: - The threshold is STRICTLY over. "Over 5" does not fire at 5. - A rule fires at most once per window. A sixty-minute window means one alert an hour however bad the hour is, and again next hour if it is still bad. `dedupe_minutes` changes that quiet period; leaving it out ties it to the window, which is what makes one bad hour one alert. `kind` cannot be changed on an existing rule — the whole `config` shape belongs to the kind, and the alerts already logged were measured against the old one. Make a new rule with a new key instead.

    Required permissions: alerts.manage

    keystring · required kindstring · required thresholdnumber · required window_minutesinteger · required labelstring dedupe_minutesinteger configobject recipientsobject enabledboolean
    Argument schema and validation
    keystringrequired
    The rule's handle. An existing one is updated.
    kindstringrequired
    What to watch: failure_rate, stuck_state, queue_depth, payment_stuck, flow_broken, tool_outage, notification_failure. Cannot change later.
    thresholdnumberrequired
    Fire when the reading goes STRICTLY over this. A count for most kinds; a percentage for failure_rate.
    window_minutesintegerrequired
    How far back to look, 5 minutes to 14 days.
    labelstringoptional
    What a person sees. Defaults to the kind's own name.
    dedupe_minutesintegeroptional
    How long it stays quiet after firing. Defaults to the window.
    configobjectoptional
    Per-kind settings, e.g. {"source":"notifications"} or {"table_id":"orders","status":"confirmed","minutes":120}.
    recipientsobjectoptional
    Who to tell: {"users":[1,2]} or {"roles":["owner"]}. Omit to tell everybody in the workspace.
    enabledbooleanoptional
    Switch it on or off.
  • upsert_risk_rule ve3603b9d writes Create or change a risk rule: what to look at before something goes ahead, and what to do when it looks wrong. Matched on `key`. THIS IS THE STRONGEST WRITE ON THIS SERVER. A rule whose action is `block` REFUSES A REAL CUSTOMER, and one whose action is `hold` stops the action and raises an approval a person has to answer. Say exactly what the rule will catch and what it will do, and let the account holder confirm before you save. The four signals: - `velocity` — how often something has already happened for this customer. It is the same counting a `limit` business rule does, and `{"rule_key":"daily_withdrawals"}` points at one instead of repeating its numbers, so the risk engine and the flows enforcing the same limit can never drift apart. - `amount` — `{"above": 2000000}`, or your own condition, or an existing `eligibility` rule by key. - `new_device` — needs a `device` and a `subject` in the inputs. It never fingerprints anybody; it only remembers what it was told. - `geography` — an allow list, a deny list, or `{"unusual_for_subject": true}` for somewhere this customer has not acted from before. Two rules can trip at once. The strongest action always wins — block over hold over alert — and `priority` only decides whose sentence gets quoted, never what the answer is. For `hold`, `config.approval` is a Phase 2 approval policy and it is checked when you save: a policy naming nobody is refused here rather than failing on a real customer's payment.

    Required permissions: alerts.manage

    keystring · required signalstring · required actionstring · required definitionobject · required labelstring configobject priorityinteger enabledboolean
    Argument schema and validation
    keystringrequired
    The rule's handle. An existing one is updated.
    signalstringrequired
    What to look at: velocity, amount, new_device, geography. Cannot change later.
    actionstringrequired
    allow, alert, hold or block. hold raises an approval; block refuses.
    definitionobjectrequired
    The signal's own settings, or {"rule_key":"..."} to reuse a business rule.
    labelstringoptional
    What a person sees.
    configobjectoptional
    For hold: {"approval":{...}}. For alert: {"recipients":{"roles":["owner"]}}.
    priorityintegeroptional
    Lowest first. Decides whose sentence is quoted, never the answer.
    enabledbooleanoptional
    Switch it on or off.
  • upsert_sla_policy v21d1bfe1 writes Create or change a service-level promise: what it is about, the status that starts the clock, how long the business has, and what happens when it runs out. Matched on `key`. Read the saved policy's `describes` sentence back to the person before you consider this done — "A record that reaches 'confirmed' must leave it within 4 hours of working time" is checkable in a way that a settings object is not. Two things to say out loud before saving: - `breach_action: "transition"` MOVES THE RECORD ON when the promise is missed, with nobody watching. It goes through the status machine, so a move the business has not declared is refused rather than forced — but a declared one happens. Name the destination state when you describe it. - `calendar.mode: "business_hours"` makes the clock stop outside working hours, so a four-hour promise made on Friday afternoon can breach on Monday. Working hours come from an opening-window business rule (`calendar.rule_key`) or the workspace's own schedule, holidays included — never from a second calendar this feature keeps. A policy applies to everything already sitting in the start status, not just to what arrives afterwards. Writing one on a backlog reports the backlog immediately, which is usually what somebody wants and always a surprise if nobody said so.

    Required permissions: alerts.manage

    keystring · required subject_typestring · required target_minutesinteger · required start_onobject · required subjectobject stop_onobject labelstring warn_at_pctinteger breach_actionstring breach_configobject escalate_after_minutesinteger calendarobject recipientsobject enabledboolean
    Argument schema and validation
    keystringrequired
    The policy's handle. An existing one is updated.
    subject_typestringrequired
    record or ticket. Cannot change later.
    target_minutesintegerrequired
    How long the business has, in minutes of whatever the calendar counts.
    start_onobjectrequired
    What starts the clock, e.g. {"status":"confirmed"}.
    subjectobjectoptional
    Which ones: {"table_id":"orders","column":"status"} for a record, {"priority":"urgent"} for a ticket.
    stop_onobjectoptional
    What stops it, e.g. {"status":"paid"}. Omit and simply leaving the start status stops it.
    labelstringoptional
    What a person sees.
    warn_at_pctintegeroptional
    Warn at this share of the target, 1-99. Default 80.
    breach_actionstringoptional
    notify, escalate or transition. transition MOVES the record.
    breach_configobjectoptional
    For transition: {"to":"cancelled"}. For escalate: {"escalate_to":{"roles":["owner"]}}.
    escalate_after_minutesintegeroptional
    Minutes past the breach before escalating. Omit for no escalation stage.
    calendarobjectoptional
    {"mode":"24_7"} or {"mode":"business_hours","rule_key":"opening_hours"}.
    recipientsobjectoptional
    Who to tell: {"roles":["manager"]}.
    enabledbooleanoptional
    Switch it on or off.
Operations /mcp/v1/operations 2 read · 2 write

The named things this business can do — create a booking, register a customer, process a refund — each written down once, and the log of every time one ran.

  • describe_operation vf165bed2 One operation in full: every value it asks for with the kind it must be (a phone number, a date, an amount), what it does step by step, what it hands back, and what running it would actually cause — records written, money asked for, messages sent, approvals raised. Read the `effects` before you run one. `money` means a real customer is asked to pay; `message` means a message lands on somebody's phone. Say what will happen and let the account holder confirm it before you call run_operation. `recent_runs` shows the last few attempts, which is usually the fastest answer to "did that work" — including which step failed and how long it took.

    Required permissions: operations.view

    keystring · required
    Argument schema and validation
    keystringrequired
    The operation's key, as list_operations returns it.
  • list_operations vbfc17cc7 Everything this business has written down as a named thing it can do — create a booking, register a customer, process a refund, close a case. Start here before deciding to do any of that yourself: an operation already knows the workspace's own rules, so calling one is both safer and shorter than reproducing it out of separate tool calls. Each entry says what it asks for, what it does, and whether it is switched on. describe_operation gives one in full, including the exact input names; run_operation does it.

    Required permissions: operations.view

    enabled_onlyboolean
    Argument schema and validation
    enabled_onlybooleanoptional
    Only the ones that can actually be run right now.
  • run_operation v498abf9d writes Do one of this business's named operations: create the booking, register the customer, process the refund. Call describe_operation first and read its `effects`. An operation can write records, ask a real customer to pay, send a message to a real phone and raise an approval that notifies real people — say which of those will happen, in those words, and let the account holder confirm before you run it. `inputs` is a JSON object keyed exactly as describe_operation lists them. The operation validates every value itself and refuses the whole thing before anything happens, naming the field it did not like — so pass what you have and read the refusal rather than guessing at formats. Pass `idempotency_key` (any string you make up) whenever a retry must not do it twice: the first answer is kept and replayed for the same key, so a call that timed out can be repeated safely. The answer's `steps` say what each part did and how long it took. When it fails, `rolled_back` says which record writes were put back and `not_undone` says what could not be — a message already sent, money already asked for. Read that back to the person rather than saying it was undone.

    Required permissions: operations.run

    keystring · required inputsstring idempotency_keystring
    Argument schema and validation
    keystringrequired
    The operation's key, as list_operations returns it.
    inputsstringoptional
    A JSON object of the values it asks for, keyed exactly as describe_operation lists them.
    idempotency_keystringoptional
    Any string you make up. The same key replays the first answer instead of doing it twice.
  • upsert_operation v7b2cb212 writes Write down a named thing this business can do, or change one that already exists — create_booking, register_customer, process_refund. Everything an operation knows lives in its definition, and every channel that calls it by name — a message flow, a phone menu, an assistant, the API — runs exactly this. Pass `key` to say which one. A new key creates; an existing key changes that one, and only the arguments you actually pass change — leave `steps` out and the steps it already has are kept. A key can never be renamed: flows, menus and integrations call the operation by it, so a rename is a new operation plus a deletion somebody has looked at. `inputs` is a JSON list of {key, label, type, required, help, default} — the type is checked once, here, so every channel gets the same answer to "is 0712 345 678 a phone number". `steps` is a JSON list of {id, type, config} run in order, and `outputs` a JSON list of {key, from} where `from` is usually "{{steps.<step id>.record_id}}". Step types and the config each wants: - data_save {table, mode: create|update|upsert, record_id, match, values} — one row, with everything that table enforces - data_find {table, where, sort, limit, required, not_found_message} — answers `record`, `records`, `count` - transition {table, record_id, to, column, reason} — move a row along its status machine - allocate {table, pool, claim, sort, ttl_seconds, wait_seconds, unavailable_message} — claim one free row under a lock - rule {key, inputs, require_pass} — ask a business rule and stop when it says no - set {values} — work something out and name it, so later steps can read it - payment_intent {amount, currency, method, payer, collect, hold, expires_in_minutes} — ASKS a customer to pay; it does not wait for the money - approval {policy, title, summary, amount, link, fields} — asks people to decide, and MUST be the last step, because an operation never waits - send {channel, to, body, sender_id} — one SMS or WhatsApp from the workspace's own number - http {url, method, headers, body, query, expect_ok} — one request to a system outside this platform Writing a step down needs the same permission running it does. What a definition's steps CAUSE is worked out from the steps themselves — a payment step wants "Start purchases and ask customers to pay", a send step "Send messages and place calls", an http step "Set up things that run without you" — and a connection that was not given one of those is refused here, not only at `run_operation`. The reason is that saving is not the last gate: a flow, a phone menu or the API can call this operation by name afterwards without asking again, so an operation is armed the moment it is saved. The answer says which ticks a run of it needs; say that plainly rather than implying the operation is ready for anyone to use. A definition that could not work is refused before anything is written, naming the field: read the refusal and fix it rather than sending it again. A `inputs`, `steps` or `outputs` argument that is not valid JSON is refused the same way and nothing is changed — send the whole list again, or leave the argument out to keep the one already saved. describe_operation reads one back in full.

    Required permissions: operations.manage

    keystring · required namestring descriptionstring inputsstring stepsstring outputsstring enabledboolean
    Argument schema and validation
    keystringrequired
    The name every channel calls it by, in snake_case, e.g. create_booking. An existing key changes that operation; it can never be renamed.
    namestringoptional
    What a person reads, e.g. "Create a booking". Required when creating.
    descriptionstringoptional
    One or two sentences on what it is for.
    inputsstringoptional
    JSON list of {key, label, type, required, help, default}. Leave out to keep the ones it has.
    stepsstringoptional
    JSON list of {id, type, config}, run in order — see the tool description for each type's config. Leave out to keep the ones it has.
    outputsstringoptional
    JSON list of {key, from}, where from is a template such as "{{steps.booking.record_id}}". Leave out to keep the ones it has; [] means it promises none.
    enabledbooleanoptional
    Whether it can be run. Default true on a new one; unchanged on an existing one unless you pass it. Only true or false — a word that is neither is refused rather than read as false.
Studio /mcp/v1/studio 3 read · 2 write

Voice and audio: browse the voice library, generate speech, convert audio and publish it for use in an IVR.

  • get_asset vfc94eee4 Check one audio asset — mainly to see whether a generation that was still running has finished.

    Required permissions: asset-studio.view

    asset_idinteger · required
    Argument schema and validation
    asset_idintegerrequired
    The asset id.
  • list_assets v55ed7b79 List the audio already in this account's Asset Studio. Check here before generating — the clip you need may exist.

    Required permissions: asset-studio.view

    searchstring statusstring limitinteger
    Argument schema and validation
    searchstringoptional
    Filter by name.
    statusstringoptional
    ready, processing or failed.
    limitintegeroptional
    Default 25, max 100.
  • list_voices vd9762d10 List the voices available for generating speech, with language, gender, style and a preview URL. Pick from here rather than generating candidates — previews already exist and cost nothing, generation costs money.

    Required permissions: asset-studio.view

    languagestring genderstring providerstring limitinteger
    Argument schema and validation
    languagestringoptional
    e.g. "sw" for Kiswahili, "en" for English.
    genderstringoptional
    male or female.
    providerstringoptional
    Filter to one provider.
    limitintegeroptional
    Default 25, max 100.
  • generate_speech v5dd9ab56 writes Turn text into spoken audio using one of the account's voices, and put it in Asset Studio. Use it for IVR greetings, menu prompts and voicemail messages. Generation costs money, so pick the voice with list_voices first and do not generate variations speculatively.

    Required permissions: asset-studio.manage

    textstring · required voice_idstring · required namestring · required wait_msinteger confirm_longboolean
    Argument schema and validation
    textstringrequired
    What to say. Write it in the language the caller will hear.
    voice_idstringrequired
    A voice_id from list_voices.
    namestringrequired
    A name for the clip, e.g. "greeting_sw".
    wait_msintegeroptional
    How long to wait for it to finish before returning a handle. Default 8000, max 20000.
    confirm_longbooleanoptional
    Required for text over 1200 characters, after checking with the user.
  • publish_asset_to_ivr v3b2eebf6 writes Make an Asset Studio clip usable inside a call flow. This step is required and easy to forget: an Asset Studio id is NOT an IVR asset id, and a node that references the wrong one will not play.

    Required permissions: ivr.assets.manage

    asset_idinteger · required
    Argument schema and validation
    asset_idintegerrequired
    The Asset Studio asset to publish.
Numbers /mcp/v1/numbers 4 read · 2 write

Phone numbers: what you own, what is available, what one costs, and how to pay for it.

  • check_payment_status v62f6c248 Check whether a payment you started has actually settled. "Processing" means the prompt is out and unanswered — wait for it, do not start a second payment.

    Required permissions: numbers.view

    payment_idinteger · required
    Argument schema and validation
    payment_idintegerrequired
    The payment_id from start_number_payment.
  • list_my_numbers v7b8bfcd0 The phone numbers this business already owns.

    Required permissions: numbers.view

    limitinteger
    Argument schema and validation
    limitintegeroptional
    Default 25, max 100.
  • quote_number vc98d1e2f Get the binding total for buying a number — monthly fee plus any one-off deposit, converted to the billing currency. Returns a quote_id that expires in 15 minutes. You MUST read the total back to the user and get their agreement before starting a payment.

    Required permissions: numbers.view

    catalog_idinteger · required
    Argument schema and validation
    catalog_idintegerrequired
    The catalog_id from search_available_numbers.
  • search_available_numbers va86e1247 Search phone numbers available to buy right now, with their monthly price. Prices here are indicative — quote_number gives the binding total including any deposit.

    Required permissions: numbers.view

    prefixstring number_type_idinteger limitinteger
    Argument schema and validation
    prefixstringoptional
    E.164 prefix, e.g. "+255".
    number_type_idintegeroptional
    Restrict to one number type.
    limitintegeroptional
    Default 25, max 100.
  • request_number v032f4711 writes Ask for a phone number that is not in the available list — a specific prefix, a country, a vanity number. An administrator prices it and the user pays the quoted deposit. Nothing is reserved and nothing is charged by this call.

    Required permissions: numbers.purchase

    business_use_casestring · required preferred_numberstring notesstring
    Argument schema and validation
    business_use_casestringrequired
    What the business will use the number for.
    preferred_numberstringoptional
    A specific number or prefix they would like.
    notesstringoptional
    Anything else the administrator should know.
  • start_number_payment v827723c5 writes Begin paying for a number. You never move money: this hands back a prompt on the customer's phone, a lipa number, or a checkout link, and a PERSON completes it. Requires a live quote_id, so the price the user agreed is the price they pay.

    Required permissions: numbers.purchase

    quote_idstring · required methodstring · required payer_msisdnstring idempotency_keystring
    Argument schema and validation
    quote_idstringrequired
    A live quote_id from quote_number.
    methodstringrequired
    push (PIN prompt on their phone), lipa_namba (short number they pay to), or link (checkout page). Ask the user which they prefer.
    payer_msisdnstringoptional
    Required for push: the phone that gets the prompt.
    idempotency_keystringoptional
    Send the same key when retrying, so a retry never starts a second payment.
WhatsApp groups /mcp/v1/groups 2 read · 5 write

Groups the business runs from its WhatsApp number: create, invite, post, approve joins, remove members.

  • get_group v5005fc01 One WhatsApp group in full: members and their state, pending join requests, the invite link, and recent activity.

    Required permissions: communications.groups.view

    group_idinteger · required
    Argument schema and validation
    group_idintegerrequired
    The group id from list_groups.
  • list_groups v1849d872 List the WhatsApp groups this business runs: subject, status, how many of the 8 seats are taken, pending join requests, and the invite link.

    Required permissions: communications.groups.view

    phone_number_idstring statusstring limitinteger
    Argument schema and validation
    phone_number_idstringoptional
    Only groups on this business number.
    statusstringoptional
    creating | active | suspended | failed | deleted | all. Default: everything except deleted.
    limitintegeroptional
    At most this many, max 100.
  • approve_join_request vc7b8593f writes Approve or reject people waiting to join an approval-required WhatsApp group. Join request ids come from get_group.

    Required permissions: communications.groups.manage

    group_idinteger · required join_request_idsstring · required decisionstring
    Argument schema and validation
    group_idintegerrequired
    The group id from list_groups.
    join_request_idsstringrequired
    One or more join request ids, comma-separated.
    decisionstringoptional
    approve (default) or reject.
  • create_group v5c6a4fac writes Create a WhatsApp group from a business number. WhatsApp confirms it a moment later; invitees, if given, get the invite template once it does. Needs an Official Business Account.

    Required permissions: communications.groups.manage

    subjectstring · required descriptionstring join_approval_modestring phone_number_idstring inviteesstring
    Argument schema and validation
    subjectstringrequired
    The group name, up to 128 characters.
    descriptionstringoptional
    Optional, up to 2048 characters.
    join_approval_modestringoptional
    auto_approve (anyone with the link joins) or approval_required (the business approves each request).
    phone_number_idstringoptional
    The business number to create it from; the default number when omitted.
    inviteesstringoptional
    Phone numbers to invite once the group is live, comma-separated, at most 7.
  • remove_group_participant vcfad2be8 writes Remove people from a WhatsApp group. They can only come back through a fresh invite.

    Required permissions: communications.groups.manage

    group_idinteger · required phonesstring · required
    Argument schema and validation
    group_idintegerrequired
    The group id from list_groups.
    phonesstringrequired
    Phone numbers or wa_ids to remove, comma-separated, at most 8.
  • send_group_invite v6a2b9f1d writes Invite people into a WhatsApp group by sending each one the approved invite-link template. Joining is their choice; the roster updates when they tap the link.

    Required permissions: communications.groups.manage, communications.send

    group_idinteger · required phonesstring · required
    Argument schema and validation
    group_idintegerrequired
    The group id from list_groups.
    phonesstringrequired
    Phone numbers in international format, comma-separated.
  • send_group_message vfaab165e writes Post a message into a WhatsApp group: text, a media link, or an approved template. Text and media only work within 24 hours of a member's last message; a template always works. Every member delivered to is billed.

    Required permissions: communications.groups.view, communications.send

    group_idinteger · required textstring media_urlstring media_typestring templatestring
    Argument schema and validation
    group_idintegerrequired
    The group id from list_groups.
    textstringoptional
    The message, or the caption when media_url is given.
    media_urlstringoptional
    A public URL to an image, video, audio file or document.
    media_typestringoptional
    image | video | audio | document. Default document.
    templatestringoptional
    An approved template name, for when the 24-hour window is closed.
Agents /mcp/v1/agents 7 read · 6 write

Your own AI specialists: see the roster and ask one a question.

  • get_agent ve92d40e2 One AI agent in full: its persona and instructions, the greeting it opens with, which model and voice it runs on, the tools it can call, the groups it belongs to, and whether the phone system has it yet.

    Required permissions: agents.ai.view

    agent_idinteger · required
    Argument schema and validation
    agent_idintegerrequired
    The agent to read. list_agents gives the ids.
  • get_engine_run v0d6517f9 Inside one AI thinking run: what it was asked, every step it took in order, which tools it called and what came back, what it answered, and where the time and the money went. This is how you find out why an agent said something odd.

    Required permissions: agents.engine.view

    runstring · required
    Argument schema and validation
    runstringrequired
    The run reference from list_engine_runs.
  • list_agent_groups vf872cfd7 How this business groups its agents — a support desk, a sales team, a legal panel — with who is in each one, AI and human alike. Grouping is organisational only: it does not decide who gets a call or who can see a conversation.

    Required permissions: agents.ai.view

    limitinteger
    Argument schema and validation
    limitintegeroptional
    Max groups to return (default 25, max 100).
  • list_agent_tools vbca548b4 What one agent can actually do on a call: every tool attached to it, what kind each is (an HTTP call, an MCP server, one of this platform's own servers), where it points and what it is for. Read this before attaching another.

    Required permissions: agents.ai.view

    agent_idinteger · required limitinteger
    Argument schema and validation
    agent_idintegerrequired
    The agent whose tools to list.
    limitintegeroptional
    Max tools to return (default 25, max 100).
  • list_agents v6b127ddb The AI specialists this business has set up — what each one is for and whether it is available. Ask one a question with ask_agent when it knows something you do not.

    Required permissions: agents.ai.view

    limitinteger
    Argument schema and validation
    limitintegeroptional
    Default 25, max 100.
  • list_engine_runs vb4502bcc What the AI has actually been doing: every thinking run on this account with what set it off, whether it succeeded, which model answered, how many steps it took, how long it took and what it cost. Filter by agent or by status to find the failures.

    Required permissions: agents.engine.view

    agent_idinteger statusstring triggerstring limitinteger
    Argument schema and validation
    agent_idintegeroptional
    Only this agent's runs.
    statusstringoptional
    queued, running, succeeded, failed, denied, timed_out, handoff or awaiting_human.
    triggerstringoptional
    What set the run off, e.g. call_consult or inbound_message.
    limitintegeroptional
    Max runs to return (default 25, max 100).
  • list_knowledge v9788f874 What the AI agents on this account have been taught: the knowledge collections, the documents in each, and whether each one has finished indexing. A document that is not "ready" is not being used to answer anybody yet.

    Required permissions: agents.ai.view

    collection_idinteger limitinteger
    Argument schema and validation
    collection_idintegeroptional
    Read one collection only.
    limitintegeroptional
    Max collections to return (default 25, max 100).
  • add_knowledge v7064da41 writes Teach the AI agents something: add a titled piece of writing — a policy, a price list, an FAQ answer — to a knowledge collection. It is queued for indexing and only starts answering questions once indexing finishes.

    Required permissions: agents.ai.edit

    titlestring · required bodystring · required collection_idinteger collection_namestring
    Argument schema and validation
    titlestringrequired
    What this piece is about, e.g. "Refund policy". Agents retrieve by it.
    bodystringrequired
    The text itself.
    collection_idintegeroptional
    An existing collection to add it to. list_knowledge gives the ids.
    collection_namestringoptional
    A collection by name, created if it does not exist yet.
  • attach_tool_to_agent va2ead8a6 writes Give an agent a new tool it can call during a conversation: an HTTP endpoint, or an external MCP server. The agent keeps every tool it already had. Nothing reaches live calls until a person applies changes.

    Required permissions: agents.ai.tools.manage

    agent_idinteger · required namestring · required descriptionstring · required urlstring · required typestring methodstring parametersobject
    Argument schema and validation
    agent_idintegerrequired
    The agent to give the tool to.
    namestringrequired
    What the agent calls it, e.g. "check_order_status". Letters, numbers and underscores.
    descriptionstringrequired
    What it does and when to use it. This is what the agent reads to decide.
    urlstringrequired
    The endpoint to call.
    typestringoptional
    "http_call" (default) or "mcp_server" for an external MCP server.
    methodstringoptional
    HTTP method for an http_call tool. Default GET.
    parametersobjectoptional
    JSON-schema properties for the arguments the agent should supply.
  • create_agent v05236291 writes Create a new AI agent from a name and its instructions. It is saved locally and does NOT answer calls until a person applies changes. Refuses up front, and says why, if no AI model can be resolved for it — an agent without a model is rejected by the phone system.

    Required permissions: agents.ai.create

    namestring · required instructionsstring · required modestring model_idstring greetingstring first_speakerstring
    Argument schema and validation
    namestringrequired
    What to call the agent. Spaces are fine, e.g. "Customer Support".
    instructionsstringrequired
    What this agent is for, how it should behave, and what it must not do.
    modestringoptional
    "realtime" (speech to speech, the default) or "pipeline" (separate ear, brain and voice).
    model_idstringoptional
    Optional catalogue model id. Left out, the platform default for this mode is used; if there is none, this call is refused rather than making an agent the phone system will reject.
    greetingstringoptional
    The line it opens with, when it speaks first.
    first_speakerstringoptional
    "agent" or "caller" — who talks first.
  • set_group_members ved45d6d5 writes Set exactly who is in an agent group. This REPLACES the membership rather than adding to it: anybody you leave out is removed, so read list_agent_groups first and send the full list you want.

    Required permissions: agents.groups.manage

    group_idinteger · required agent_idsarray client_user_idsarray
    Argument schema and validation
    group_idintegerrequired
    The group to set. list_agent_groups gives the ids.
    agent_idsarray<any>optional
    The AI agents that should be in the group, by id. Anything omitted is removed.
    client_user_idsarray<any>optional
    The people who should be in the group, by client user id. Anything omitted is removed.
  • simulate_agent v3ada3197 writes Try an agent out: say something to it as if you were a caller and see exactly what it would answer, which tools it would reach for and how it would use them. No real call is placed and nobody is contacted; it runs against the agent's real configuration and uses a small amount of AI credit.

    Required permissions: agents.ai.edit

    agent_idinteger · required caller_saysstring · required history_jsonstring directionstring caller_numberstring caller_namestring languagestring tool_mocksobject
    Argument schema and validation
    agent_idintegerrequired
    The agent to try.
    caller_saysstringrequired
    What the pretend caller says this turn.
    history_jsonstringoptional
    Earlier turns as JSON: [{"role":"caller|agent","content":"…"}], oldest first.
    directionstringoptional
    "inbound" (default) or "outbound".
    caller_numberstringoptional
    The number the pretend caller is calling from.
    caller_namestringoptional
    The pretend caller's name.
    languagestringoptional
    "auto" (default), "en" or "sw".
    tool_mocksobjectoptional
    Canned results for tools, keyed by tool name, so a rehearsal never hits a real endpoint.
  • update_agent v0ffddcdb writes Change an existing AI agent: its name, its instructions, its greeting, or who speaks first. Only the fields you pass change. The edit is saved locally and reaches real calls only when a person applies changes.

    Required permissions: agents.ai.edit

    agent_idinteger · required namestring instructionsstring greetingstring first_speakerstring
    Argument schema and validation
    agent_idintegerrequired
    The agent to change.
    namestringoptional
    New display name. Spaces are fine.
    instructionsstringoptional
    Replacement instructions. This replaces the whole prompt, so send the full text.
    greetingstringoptional
    The opening line. Send an empty string to clear it.
    first_speakerstringoptional
    "agent" or "caller".
Orders /mcp/v1/orders 3 read · 3 write

Customer orders across every platform: find, read, move status, request payment.

  • find-chats-for-order-tool vbe029874 Find the chats an order could belong to, on any platform, best guess first. Use it before linking an order that arrived without a chat — from the storefront, over the counter, or by import. Each result says whether it can actually be linked and why not. order_idinteger · required searchstring
    Argument schema and validation
    order_idintegerrequired
    The order to find a chat for.
    searchstringoptional
    Optional name, number or username to narrow the search.
  • get-order-tool v4ac4e174 Get one order in full: the items ordered, the total, the current status with its history, and every payment attempt against it including whether it has been paid. order_idinteger · required
    Argument schema and validation
    order_idintegerrequired
    The order id, as returned by the order list.
  • list-orders-tool v9ed96b10 List orders, newest first. Filter by status, platform, date, or who the customer is — a phone number, a username, an email or a name. Use it to answer "where is my order" and "has my payment gone through" when the customer cannot quote an order number. statusstring customerstring customer_phonestring platformstring sincestring limitinteger
    Argument schema and validation
    statusstringoptional
    Only orders in this status.
    enum
    ["pending","confirmed","processing","shipped","delivered","cancelled","refunded"]
    customerstringoptional
    Who the customer is: a phone number in any format, a username, an email, or a name.
    customer_phonestringoptional
    Deprecated alias for `customer`. Phone in any format; the last 9 digits are matched.
    platformstringoptional
    Only orders that came from this platform, e.g. whatsapp, storefront, instagram, manual.
    sincestringoptional
    Only orders placed on or after this ISO 8601 date.
    limitintegeroptional
    Maximum orders to return (1-25, default 10).
    default
    10
  • link-order-to-chat-tool vb2abde88 writes Tie an order to a customer chat on any platform, so status updates and payment requests can actually reach them. Find the chat with find-chats-for-order first; never guess a conversation id. order_idinteger · required conversation_idinteger · required
    Argument schema and validation
    order_idintegerrequired
    The order to tie to a chat.
    conversation_idintegerrequired
    The chat, from find-chats-for-order.
  • request-order-payment-tool vfacafa01 writes Ask the customer to pay for an order — a mobile-money push to their phone, or a checkout link. Returns the payment reference and, for card or link methods, the URL to send them. Only use when the customer has agreed to pay now. order_idinteger · required methodstring
    Argument schema and validation
    order_idintegerrequired
    The order to collect payment for.
    methodstringoptional
    Payment method, e.g. mobile_money or card. Defaults to mobile money.
    default
    mobile_money
  • update-order-status-tool v3f904c65 writes Move an order to a new status (confirmed, processing, shipped, delivered, cancelled, refunded) and optionally tell the customer. Use only when the business has actually decided — never to guess or reassure. order_idinteger · required statusstring · required notify_customerboolean
    Argument schema and validation
    order_idintegerrequired
    The order to move.
    statusstringrequired
    The new status.
    enum
    ["pending","confirmed","processing","shipped","delivered","cancelled","refunded"]
    notify_customerbooleanoptional
    Message the customer about the change (default: true).
    default
    true
Shop /mcp/v1/shop 5 read · 2 write

Products, brands and categories, plus the order tools.

  • get-order-tool v4ac4e174 Get one order in full: the items ordered, the total, the current status with its history, and every payment attempt against it including whether it has been paid. order_idinteger · required
    Argument schema and validation
    order_idintegerrequired
    The order id, as returned by the order list.
  • get-product-tool v5b267bc9 Get the full detail of one product by its SKU: description, price, sale price, stock count, condition, brand, category and image. skustring · required
    Argument schema and validation
    skustringrequired
    The product SKU, as returned by the product search.
  • list-brands-and-categories-tool v8df75782 List the brands and categories this shop sells, with how many products each holds. Use it to answer "what brands do you carry" or to offer a customer somewhere to start. kindstring
    Argument schema and validation
    kindstringoptional
    Which list to return (default: both).
    enum
    ["brands","categories","both"]
    default
    both
  • list-orders-tool v9ed96b10 List orders, newest first. Filter by status, platform, date, or who the customer is — a phone number, a username, an email or a name. Use it to answer "where is my order" and "has my payment gone through" when the customer cannot quote an order number. statusstring customerstring customer_phonestring platformstring sincestring limitinteger
    Argument schema and validation
    statusstringoptional
    Only orders in this status.
    enum
    ["pending","confirmed","processing","shipped","delivered","cancelled","refunded"]
    customerstringoptional
    Who the customer is: a phone number in any format, a username, an email, or a name.
    customer_phonestringoptional
    Deprecated alias for `customer`. Phone in any format; the last 9 digits are matched.
    platformstringoptional
    Only orders that came from this platform, e.g. whatsapp, storefront, instagram, manual.
    sincestringoptional
    Only orders placed on or after this ISO 8601 date.
    limitintegeroptional
    Maximum orders to return (1-25, default 10).
    default
    10
  • search-products-tool va566062e Search the shop for products by name, SKU, brand, category or description. Use this to answer "do you have…", "how much is…" and "what do you sell" questions. Returns price, stock and brand for each match. querystring brandstring in_stock_onlyboolean limitinteger
    Argument schema and validation
    querystringoptional
    What the customer asked for — a product name, SKU, brand or keyword.
    brandstringoptional
    Restrict results to one brand.
    in_stock_onlybooleanoptional
    Only return products currently in stock.
    limitintegeroptional
    Maximum products to return (1-25, default 10).
    default
    10
  • request-order-payment-tool vfacafa01 writes Ask the customer to pay for an order — a mobile-money push to their phone, or a checkout link. Returns the payment reference and, for card or link methods, the URL to send them. Only use when the customer has agreed to pay now. order_idinteger · required methodstring
    Argument schema and validation
    order_idintegerrequired
    The order to collect payment for.
    methodstringoptional
    Payment method, e.g. mobile_money or card. Defaults to mobile money.
    default
    mobile_money
  • update-order-status-tool v3f904c65 writes Move an order to a new status (confirmed, processing, shipped, delivered, cancelled, refunded) and optionally tell the customer. Use only when the business has actually decided — never to guess or reassure. order_idinteger · required statusstring · required notify_customerboolean
    Argument schema and validation
    order_idintegerrequired
    The order to move.
    statusstringrequired
    The new status.
    enum
    ["pending","confirmed","processing","shipped","delivered","cancelled","refunded"]
    notify_customerbooleanoptional
    Message the customer about the change (default: true).
    default
    true
Tickets /mcp/v1/tickets 6 read · 11 write

Support tickets: create, update, assign, reply, labels and notifications.

  • get-ticket-notifications-tool vf16b5547 Get the authenticated user's ticket notifications including mentions, assignments, replies, and status changes. Returns the most recent 30 notifications with unread count.
  • get-ticket-stats-tool v23fc50fc Get ticket statistics including counts by status and urgent ticket count. Respects the user's view permissions.
  • get-ticket-tool v2f90de0e Get full details of a specific ticket including description, customer info, replies, labels, and linked conversation/call/contact. ticket_idstring · required
    Argument schema and validation
    ticket_idstringrequired
    The UUID of the ticket to retrieve.
  • list-team-members-tool ve55ac862 List team members (agents) for the current tenant. Use this to discover agent IDs for ticket assignment. searchstring rolestring
    Argument schema and validation
    searchstringoptional
    Search by name or email.
    rolestringoptional
    Filter by role (e.g. owner, manager, agent).
  • list-ticket-labels-tool v5396c988 List ticket labels for the current tenant. Optionally filter by name search query. searchstring
    Argument schema and validation
    searchstringoptional
    Optional search query to filter labels by name.
  • list-tickets-tool v5b51ef9e List and filter support tickets. Supports filtering by status, priority, assigned agent, creator, label, channel, date range, and free-text search. Returns paginated results with sort options. statusstring prioritystring assigned_to_idinteger created_by_idinteger label_idinteger channelstring created_afterstring created_beforestring searchstring sort_bystring sort_orderstring pageinteger per_pageinteger
    Argument schema and validation
    statusstringoptional
    Filter by ticket status.
    enum
    ["open","in_progress","waiting","resolved","closed"]
    prioritystringoptional
    Filter by ticket priority.
    enum
    ["low","medium","high","urgent"]
    assigned_to_idintegeroptional
    Filter by assigned agent ID. Use list-team-members to discover IDs.
    created_by_idintegeroptional
    Filter by the agent who created the ticket.
    label_idintegeroptional
    Filter by label ID.
    channelstringoptional
    Filter by channel.
    enum
    ["whatsapp","sms","phone","email","web","internal"]
    created_afterstringoptional
    Filter tickets created on or after this ISO 8601 date (e.g. 2026-03-01).
    created_beforestringoptional
    Filter tickets created on or before this ISO 8601 date (e.g. 2026-03-31).
    searchstringoptional
    Free-text search across subject, description, customer name, email, phone, and ticket number.
    sort_bystringoptional
    Sort field (default: created_at).
    enum
    ["created_at","updated_at","priority","ticket_number"]
    default
    created_at
    sort_orderstringoptional
    Sort direction (default: desc).
    enum
    ["asc","desc"]
    default
    desc
    pageintegeroptional
    Page number for pagination (default: 1).
    default
    1
    per_pageintegeroptional
    Results per page (1-50, default: 20).
    default
    20
  • add-ticket-reply-tool v81b2708b writes Add a reply or an internal note to a ticket. With a channel of sms, whatsapp or email this SENDS to the customer on the ticket's linked conversation — that needs the send tick, and the answer tells you whether it actually left the building. Without a channel it only writes to the thread. Use type "note" for internal notes only agents see. ticket_idstring · required bodystring · required typestring is_internalboolean channelstring
    Argument schema and validation
    ticket_idstringrequired
    The UUID of the ticket to reply to.
    bodystringrequired
    The reply message body. Supports @mentions to notify agents.
    typestringoptional
    Type of reply: "reply" for customer-visible response, "note" for internal agent note.
    enum
    ["reply","note"]
    default
    reply
    is_internalbooleanoptional
    Whether this reply is internal (only visible to agents).
    default
    false
    channelstringoptional
    Leave this out to write in the thread only. Set to sms, whatsapp or email to SEND to the customer — this reaches a real person, needs the send tick, and only works on the channel the ticket's linked conversation is already on (a whatsapp thread cannot be answered by email). On WhatsApp outside the 24-hour window nothing is delivered and the call comes back as an error. `internal` means the same as leaving it out.
    enum
    ["internal","sms","whatsapp","email"]
  • assign-ticket-tool v8f384043 writes Assign a ticket to an agent. Automatically changes status from "open" to "in_progress" when assigning. Pass null to unassign. ticket_idstring · required assigned_to_idinteger
    Argument schema and validation
    ticket_idstringrequired
    The UUID of the ticket to assign.
    assigned_to_idintegeroptional
    The ID of the agent to assign to. Pass null or omit to unassign.
  • change-ticket-status-tool v73be4700 writes Change a ticket's status. Automatically manages SLA timestamps: sets resolved_at when resolving, closed_at when closing, and clears both when reopening. ticket_idstring · required statusstring · required
    Argument schema and validation
    ticket_idstringrequired
    The UUID of the ticket.
    statusstringrequired
    The new status: open, in_progress, waiting, resolved, or closed.
    enum
    ["open","in_progress","waiting","resolved","closed"]
  • create-ticket-label-tool vb54e0a6a writes Create a new ticket label with a name, hex color, and optional description. Label names must be unique per tenant. namestring · required colorstring · required descriptionstring
    Argument schema and validation
    namestringrequired
    Label name (must be unique per tenant).
    colorstringrequired
    Hex color code, e.g. "#FF5733".
    descriptionstringoptional
    Optional label description.
  • create-ticket-tool v4d709f2f writes Create a new support ticket. Requires subject, priority, and channel. Optionally attach customer details, labels, and link to a conversation, call, or contact. subjectstring · required prioritystring · required channelstring · required descriptionstring assigned_to_idinteger conversation_idinteger call_idinteger contact_idinteger customer_namestring customer_emailstring customer_phonestring customer_companystring label_idsarray
    Argument schema and validation
    subjectstringrequired
    Ticket subject line.
    prioritystringrequired
    Ticket priority level.
    enum
    ["low","medium","high","urgent"]
    channelstringrequired
    The channel through which the ticket was created.
    enum
    ["whatsapp","sms","phone","email","web","internal"]
    descriptionstringoptional
    Detailed ticket description.
    assigned_to_idintegeroptional
    ID of the agent to assign the ticket to.
    conversation_idintegeroptional
    ID of a linked conversation.
    call_idintegeroptional
    ID of a linked call.
    contact_idintegeroptional
    ID of a linked contact.
    customer_namestringoptional
    Customer name.
    customer_emailstringoptional
    Customer email address.
    customer_phonestringoptional
    Customer phone number.
    customer_companystringoptional
    Customer company name.
    label_idsarray<any>optional
    Array of label IDs to attach to the ticket.
  • delete-ticket-label-tool v2be5f484 writes Delete a ticket label. Removes the label from all tickets that have it. label_idinteger · required
    Argument schema and validation
    label_idintegerrequired
    The ID of the label to delete.
  • delete-ticket-tool v129446bb writes Permanently delete a ticket and all its replies. ticket_idstring · required
    Argument schema and validation
    ticket_idstringrequired
    The UUID of the ticket to delete.
  • mark-ticket-notifications-read-tool v3384c0d1 writes Mark ticket notifications as read. Provide specific notification IDs or omit to mark all unread notifications as read. idsarray
    Argument schema and validation
    idsarray<any>optional
    Specific notification IDs to mark as read. Omit to mark all unread notifications.
  • sync-ticket-labels-tool v7707327b writes Sync labels on a ticket. Replaces all existing labels with the provided set. Pass an empty array to remove all labels. ticket_idstring · required label_idsarray · required
    Argument schema and validation
    ticket_idstringrequired
    The UUID of the ticket.
    label_idsarray<any>required
    Array of label IDs to set on the ticket. Pass empty array to remove all.
  • update-ticket-label-tool v4e11cc28 writes Update a ticket label's name, color, or description. Only provided fields are updated. label_idinteger · required namestring colorstring descriptionstring
    Argument schema and validation
    label_idintegerrequired
    The ID of the label to update.
    namestringoptional
    Updated label name (must be unique within tenant).
    colorstringoptional
    Updated hex color code (e.g. #FF5733).
    descriptionstringoptional
    Updated description.
  • update-ticket-tool vb2de7695 writes Update an existing ticket's subject, description, priority, channel, or customer details. Only provided fields are updated. ticket_idstring · required subjectstring descriptionstring prioritystring channelstring customer_namestring customer_emailstring customer_phonestring customer_companystring label_idsarray
    Argument schema and validation
    ticket_idstringrequired
    The UUID of the ticket to update.
    subjectstringoptional
    Updated subject line.
    descriptionstringoptional
    Updated description.
    prioritystringoptional
    Updated priority level.
    enum
    ["low","medium","high","urgent"]
    channelstringoptional
    Updated channel.
    enum
    ["whatsapp","sms","phone","email","web","internal"]
    customer_namestringoptional
    Updated customer name.
    customer_emailstringoptional
    Updated customer email.
    customer_phonestringoptional
    Updated customer phone.
    customer_companystringoptional
    Updated customer company.
    label_idsarray<any>optional
    Array of label IDs to sync (replaces existing labels).
Knowledge base /mcp/v1/kb 6 read

Your knowledge base: categories, search and full article text.

  • get-article-tool v1b55e41e Get the full content of a single knowledge base article by ID or slug. Returns all metadata and the complete markdown body. This is the tool to use when you need to read an article's actual content. identifierstring · required
    Argument schema and validation
    identifierstringrequired
    Article ID (numeric) or slug (string). Example: "7" or "getting-started".
  • get-category-tool vdf59caab Get a single knowledge base category by ID or slug, including all its articles with titles and excerpts. Use this to browse all articles within a specific category. identifierstring · required published_onlyboolean
    Argument schema and validation
    identifierstringrequired
    Category ID (numeric) or slug (string). Example: "42" or "platform-guide".
    published_onlybooleanoptional
    When true (default), returns only published articles. Set to false to include drafts.
  • get-kb-overview-tool v1539aa4c Get a complete overview of the tenant knowledge base. Returns all categories with article counts, total statistics, and the most recently updated articles. Use this as the starting point to understand what content is available.
  • list-articles-tool vd4356c6b List knowledge base articles with pagination. Filter by category (ID or slug) and published status. Returns article metadata without full content — use get-article to retrieve the full markdown body. categorystring published_onlyboolean pageinteger per_pageinteger
    Argument schema and validation
    categorystringoptional
    Filter by category ID (numeric) or slug (string). Omit to list all articles.
    published_onlybooleanoptional
    When true (default), returns only published articles. Set to false to include drafts.
    pageintegeroptional
    Page number for pagination. Default: 1.
    per_pageintegeroptional
    Articles per page (1-50). Default: 25.
  • list-categories-tool v820e42ab List all knowledge base categories for this tenant with article counts. Each category has an ID, slug, name, description, and the number of published articles it contains. include_unpublishedboolean
    Argument schema and validation
    include_unpublishedbooleanoptional
    When true, includes unpublished (draft) categories. Default: false (published only).
  • search-articles-tool v683c2d7b Search knowledge base articles by keyword across titles, excerpts, and full markdown content. Supports multi-word queries with AND logic — all words must match. Returns matching articles ranked by relevance (title matches first, then excerpt, then body). Use get-article to read the full content of any result. querystring · required published_onlyboolean limitinteger
    Argument schema and validation
    querystringrequired
    Search keywords. Multiple words use AND logic — all must match. Example: "billing setup" finds articles containing both "billing" and "setup".
    published_onlybooleanoptional
    When true (default), searches only published articles. Set to false to include drafts.
    limitintegeroptional
    Maximum results to return (1-30). Default: 20.
Platform content /mcp/v1/content 13 read

Public help articles, changelog, roadmap and system status.

  • get-article-tool get_help_article at /mcp v679a00b6 Get the full content of a published knowledge base article by its slug or ID. Returns the complete markdown content. slugstring · required
    Argument schema and validation
    slugstringrequired
    Article slug or numeric ID.
  • get-changelog-entry-tool v0e5d5cfa Get the full content of a published changelog entry by its ID. idinteger · required
    Argument schema and validation
    idintegerrequired
    Changelog entry ID.
  • get-incident-tool vd602dc26 Get full details of an incident or scheduled maintenance by ID. Includes the complete timeline of status updates and affected services. idinteger · required
    Argument schema and validation
    idintegerrequired
    Incident ID.
  • get-roadmap-item-tool v70356c5b Get full details of a published roadmap item by its slug or ID. Returns the complete description and timeline. slugstring · required
    Argument schema and validation
    slugstringrequired
    Roadmap item slug or numeric ID.
  • get-service-metrics-tool v2b4e73a3 Get performance metrics for a specific service: response times, uptime percentages, and availability over a time period (default: 24 hours, max: 90 days). service_slugstring · required hoursinteger
    Argument schema and validation
    service_slugstringrequired
    Service slug. Use list-services to discover slugs.
    hoursintegeroptional
    Lookback period in hours (1-2160, default: 24).
    default
    24
  • get-status-overview-tool vad40c530 Get the overall system status: all services grouped, active incidents count, scheduled maintenance, and an aggregate health indicator. Use this first to understand current system health.
  • list-articles-tool list_help_articles at /mcp v6a76cf0d List published knowledge base articles. Optionally filter by category slug. Returns titles and excerpts — use get-article for full content. category_slugstring pageinteger per_pageinteger
    Argument schema and validation
    category_slugstringoptional
    Filter by category slug. Use list-kb-categories to discover slugs.
    pageintegeroptional
    Page number (default: 1).
    default
    1
    per_pageintegeroptional
    Results per page (1-50, default: 25).
    default
    25
  • list-changelog-tool v6ca0c75f List published changelog entries, newest first. Optionally filter by version string. Returns titles and versions — use get-changelog-entry for full content. versionstring searchstring pageinteger per_pageinteger
    Argument schema and validation
    versionstringoptional
    Filter by version string (partial match). Example: "2.1"
    searchstringoptional
    Free-text search across title and content.
    pageintegeroptional
    Page number (default: 1).
    default
    1
    per_pageintegeroptional
    Results per page (1-50, default: 20).
    default
    20
  • list-incidents-tool v45b0cc8c List incidents and scheduled maintenance. Filter by type (incident/maintenance), status (active/resolved), or recency. Returns summaries — use get-incident for full timeline. typestring filterstring daysinteger pageinteger per_pageinteger
    Argument schema and validation
    typestringoptional
    Filter by type.
    enum
    ["incident","maintenance"]
    filterstringoptional
    Filter by resolution status.
    enum
    ["active","resolved"]
    daysintegeroptional
    Only show incidents from the last N days (1-365).
    pageintegeroptional
    Page number (default: 1).
    default
    1
    per_pageintegeroptional
    Results per page (1-50, default: 20).
    default
    20
  • list-kb-categories-tool vab96ba0e List all published knowledge base categories with article counts. Use the category slug or ID to filter articles with list-articles.
  • list-roadmap-tool vd3cb3107 List published roadmap items. Optionally filter by status (planned, in_progress, released). Returns summaries — use get-roadmap-item for full details. statusstring searchstring
    Argument schema and validation
    statusstringoptional
    Filter by status: planned, in_progress, released.
    enum
    ["planned","in_progress","released"]
    searchstringoptional
    Free-text search across title and summary.
  • list-services-tool v9a273025 List all visible services with their current status, uptime, and response time. Optionally filter by group name or status. groupstring statusstring
    Argument schema and validation
    groupstringoptional
    Filter by service group name.
    statusstringoptional
    Filter by status: operational, degraded_performance, partial_outage, major_outage, under_maintenance.
    enum
    ["operational","degraded_performance","partial_outage","major_outage","under_maintenance"]
  • search-articles-tool search_help_articles at /mcp v3ee3b2d3 Search published knowledge base articles by keyword. Searches across title, excerpt, and content. Supports multi-word AND queries. querystring · required
    Argument schema and validation
    querystringrequired
    Search keywords (space-separated, AND logic).
Calls /mcp/v1/calls 6 read · 2 write

Call history, recordings, transcripts, events and Call Studio scripts.

  • get_call v6ad948bc One call in full: both legs, when it started and ended, how long it lasted, the outcome and hang-up reason, which agent or person handled it, and what recordings exist. Take the call_id from list_calls, or pass the room_name if that is what you have.

    Required permissions: calls.view

    call_idinteger room_namestring
    Argument schema and validation
    call_idintegeroptional
    The call to read, from list_calls.
    room_namestringoptional
    Alternative to call_id: the PBX room name, if that is the identifier you were given.
  • get_call_recording v674ff9f6 A time-limited link to listen to a call recording. It hands back a URL for a person to open, never the audio itself, and the link expires — so give it to the user rather than storing it. Call get_call first to see which recordings a call has.

    Required permissions: calls.recordings.view

    call_idinteger room_namestring recording_idstring expires_in_secondsinteger
    Argument schema and validation
    call_idintegeroptional
    The call, from list_calls.
    room_namestringoptional
    Alternative to call_id: the PBX room name.
    recording_idstringoptional
    Which recording, from get_call. Omit for the most recent one on the call.
    expires_in_secondsintegeroptional
    How long the link should stay valid (60 to 604800, default 900).
  • get_call_scripts v5f9fc765 The Call Studio scripts agents follow on a live call: the core block asked on every call, plus each category with its questions in order, both English and Kiswahili labels, types, options and conditions. Read this before proposing any change to what agents say.

    Required permissions: calls.studio.view

  • get_call_transcript v7231efb5 What was actually said on a call, in order, labelled by speaker. Sensitive lines — card details, anything a node marked PII or PCI — come back masked and cannot be unmasked through this connection. Use it to answer "what did the customer ask for" rather than guessing from the outcome.

    Required permissions: calls.transcripts.view

    call_idinteger room_namestring speakerstring limitinteger
    Argument schema and validation
    call_idintegeroptional
    The call to read, from list_calls.
    room_namestringoptional
    Alternative to call_id: the PBX room name.
    speakerstringoptional
    Only one side: user or agent.
    limitintegeroptional
    Max lines to return (default 100, max 100).
  • list_call_events vc5823b31 The event timeline for one call, oldest first — ringing, dispatch, forward, answer, hang-up and every failure in between. This is the tool that answers "why did this call drop": look for a *_failed event and read its reason before offering a theory.

    Required permissions: calls.events.view

    call_idinteger room_namestring event_typestring limitinteger
    Argument schema and validation
    call_idintegeroptional
    The call, from list_calls.
    room_namestringoptional
    Alternative to call_id: the PBX room name.
    event_typestringoptional
    Only events whose type contains this, e.g. "forward" or "fail".
    limitintegeroptional
    Max events to return (default 50, max 100).
  • list_calls v7b6f154e The call history for this business, newest first: who called whom, how long it lasted, how it ended and whether it was recorded. Filter by direction, status, outcome, phone number, agent or date range. Start here before asking about any individual call.

    Required permissions: calls.history.view

    directionstring statusstring outcomestring numberstring agent_config_idstring date_fromstring date_tostring limitinteger
    Argument schema and validation
    directionstringoptional
    inbound, outbound or internal.
    statusstringoptional
    ringing, in_progress, ended, missed, failed or rejected.
    outcomestringoptional
    The settled outcome recorded for the call, e.g. answered, no_answer, busy.
    numberstringoptional
    Match either leg of the call against this phone number or fragment.
    agent_config_idstringoptional
    Only calls handled by this AI agent configuration.
    date_fromstringoptional
    Earliest call date, YYYY-MM-DD.
    date_tostringoptional
    Latest call date, YYYY-MM-DD.
    limitintegeroptional
    Max calls to return (default 25, max 100).
  • place_call v41146353 writes Ring a real phone. This dials a live handset immediately — there is no draft, no preview and no undo — so read the number back to the user and get their agreement before calling it. Accepts a phone number, or a colleague's username for an internal call.

    Required permissions: calls.place

    tostring · required from_numberstring use_agentboolean agent_config_idstring record_callboolean
    Argument schema and validation
    tostringrequired
    Who to ring: a phone number (international format preferred; a local number is completed from the caller ID's country) or a colleague's username for an internal call.
    from_numberstringoptional
    Which of the account's own numbers to show as caller ID. Omit for the first one this person may dial from. Ignored for internal calls.
    use_agentbooleanoptional
    Let an AI agent take the call instead of a person (default false).
    agent_config_idstringoptional
    Which AI agent, when use_agent is true.
    record_callbooleanoptional
    Record this call. Leave unset to follow the number's own recording policy.
  • update_call_script v44936ba5 writes Replace one Call Studio category's question list in a single change — reorder, edit, add and retire together. This changes what agents ask on LIVE calls the moment it saves. Send the complete list you want: any question you leave out is retired, and past answers keep resolving to it.

    Required permissions: calls.scripts.manage

    category_keystring · required questions_jsonstring · required
    Argument schema and validation
    category_keystringrequired
    Which category to replace, from get_call_scripts. "core" is the block asked on every call.
    questions_jsonstringrequired
    A JSON object string {"questions":[ ... ]}, max 40. Each question is {"key":"lowercase_snake","labelEn":"...","labelSw":"...","type":"text|number|money|date|enum|boolean|phone","options":["..."] (enum only, at least two),"required":true|false,"conditionKey":"...","conditionValue":"...","source":"agent|auto|either"}. Order in the array is the order agents are asked. Include every question you want to KEEP.
Call routing /mcp/v1/routing 6 read · 8 write

Routing rules, ring groups, working hours and forwarding targets.

  • get_dispatch_rule vec329386 One routing rule in full: every condition it matches on, the action it takes, its fallback, and — when it forwards — whether the destination is actually on the forwarding allow-list. A forward that is not on the allow-list drops callers silently, so this check is part of reading the rule.

    Required permissions: call-routing.view

    rule_idstring · required
    Argument schema and validation
    rule_idstringrequired
    The rule to read, from list_dispatch_rules.
  • get_ring_group vac41a15c One ring group in full: its members in ringing order, the phone numbers pointed at it, and what happens on overflow. If it overflows to a forward, this also says what can be checked about the two forwarding gates from here.

    Required permissions: ring-groups.view

    ring_group_idinteger · required
    Argument schema and validation
    ring_group_idintegerrequired
    The ring group to read, from list_ring_groups.
  • get_work_hours v0e7547d0 The working-hours schedule this business runs on: the account default, any number that overrides it, and every schedule available to choose from. Routing rules use "during work hours" and "outside work hours", so this is what decides which of them fires.

    Required permissions: call-routing.view

    limitinteger
    Argument schema and validation
    limitintegeroptional
    Max schedules to list (default 25, max 100).
  • list_dispatch_rules v9815ace9 The call routing rules for this account, in the order they are evaluated: what each one matches on and where it sends the call. First match wins, so read the whole list before concluding a rule is unreachable or adding another.

    Required permissions: call-routing.view

    limitinteger
    Argument schema and validation
    limitintegeroptional
    Max rules to return (default 25, max 100).
  • list_forwarding_targets v1ac7b4f0 The forwarding allow-list for one of this account's phone numbers — the only destinations a call on that line may be sent to. A forward to a number that is NOT on this list is hung up with no announcement and no error, so check here before trusting any forwarding rule.

    Required permissions: numbers.forwarding.manage

    numberstring · required limitinteger
    Argument schema and validation
    numberstringrequired
    One of this account's phone numbers, in international format (+255...).
    limitintegeroptional
    Max targets to return (default 25, max 100).
  • list_ring_groups v6f0e2a92 The ring groups on this account: who a call rings, whether it rings everyone at once or one after another, how long it waits, and what happens when nobody picks up. Read this before changing who is on call.

    Required permissions: ring-groups.view

    limitinteger
    Argument schema and validation
    limitintegeroptional
    Max ring groups to return (default 25, max 100).
  • add_forwarding_target v39b2b873 writes Allow one destination to receive calls forwarded from one of this account's numbers. This is the first of the two gates every forward must pass; without it the caller is hung up with no announcement. Adding a destination does not by itself forward anything — a routing rule still has to send calls there.

    Required permissions: numbers.forwarding.manage

    numberstring · required targetstring team_member_idinteger namestring
    Argument schema and validation
    numberstringrequired
    Which of this account's numbers, in international format (+255...). The allow-list is per number.
    targetstringoptional
    The destination phone number calls may be forwarded to, in international format. Use this or team_member_id.
    team_member_idintegeroptional
    A colleague to allow instead: their own phone number, name and role are used. Use this or target.
    namestringoptional
    A label for the destination, so the allow-list reads as people rather than numbers.
  • create_dispatch_rule vd27b7c3f writes Add a call routing rule. It takes effect on the very next inbound call — there is no draft state here. If the rule forwards, the destination is checked against the forwarding allow-list first and the rule is REFUSED rather than saved half-working, because a forward that is not allow-listed hangs callers up with no error anywhere.

    Required permissions: call-routing.edit

    namestring rule_jsonstring · required
    Argument schema and validation
    namestringoptional
    A short name for the rule. Overrides any name inside rule_json.
    rule_jsonstringrequired
    A JSON object string. {"name":"...","priority":10,"enabled":true,"conditions":[{"type":"phone_number|phone_number_prefix|caller_prefix|phone_number_set|caller_list|caller_not_in_list|time_schedule|outside_schedule|during_work_hours|outside_work_hours|all_numbers","value":"+255...","callerListId":"uuid","scheduleId":"uuid"}],"actionType":"dispatch_agent|dispatch_ivr|ring_group|ring_user|forward|reject|voicemail","actionConfig":{"agentConfigId":"...","ivrFlowId":"...","ringGroupId":"...","clientUserId":"...","forwardTo":"+255...","timeoutSeconds":30},"fallbackActionType":"...","fallbackActionConfig":{...}}. Rules are evaluated by priority and the first match wins.
  • create_ring_group v94d7bd79 writes Create a ring group — a set of people a call rings, either all at once or one after another, with a rule for what happens when nobody answers. The group is created empty: set_ring_group_members decides who is in it, and it rings nobody until you do.

    Required permissions: ring-groups.create

    namestring · required descriptionstring strategystring · required timeout_secondsinteger · required overflow_actionstring · required overflow_targetstring enabledboolean queue_wait_secondsinteger queue_timeout_actionstring queue_timeout_agent_config_idstring
    Argument schema and validation
    namestringrequired
    What to call the group, e.g. "Sales" or "After hours".
    descriptionstringoptional
    A sentence saying who this group is for.
    strategystringrequired
    simultaneous rings everyone at once; sequential rings them one after another in member order.
    timeout_secondsintegerrequired
    How long to ring before overflow, 5 to 120 seconds.
    overflow_actionstringrequired
    What happens when nobody answers: hangup, forward, voicemail or queue.
    overflow_targetstringoptional
    Required when overflow_action is forward: the phone number in international format (+255...) to send the caller to.
    enabledbooleanoptional
    Whether the group is in service (default true).
    queue_wait_secondsintegeroptional
    Queue only: how long a caller waits before the queue times out.
    queue_timeout_actionstringoptional
    Queue only: hangup, dispatch_agent, forward or voicemail when the queue times out.
    queue_timeout_agent_config_idstringoptional
    Queue only: which AI agent takes over, required when queue_timeout_action is dispatch_agent.
  • delete_dispatch_rule vc03421af writes Delete a call routing rule permanently. Callers that used to match it fall through to the next rule, or to the number's own settings if none matches — which can silently change where every call goes. Read the rule with get_dispatch_rule and get an explicit yes before calling this.

    Required permissions: call-routing.delete

    rule_idstring · required
    Argument schema and validation
    rule_idstringrequired
    The rule to delete, from list_dispatch_rules.
  • set_ring_group_members v7fe24b44 writes Set exactly who is in a ring group, in ringing order. Send the complete list you want: anybody not in it is removed, and for a sequential group the order you send is the order phones ring. This changes who is called on the very next inbound call.

    Required permissions: ring-groups.members.manage

    ring_group_idinteger · required client_user_idsarray · required
    Argument schema and validation
    ring_group_idintegerrequired
    The ring group to change, from list_ring_groups.
    client_user_idsarray<integer>required
    The complete list of team member ids who should be in the group, in ringing order. Anybody not listed is removed. Ids come from get_ring_group or the team directory.
  • set_work_hours vcb4af38d writes Point the account, or one phone number, at a working-hours schedule. This takes effect immediately and changes which routing rules fire: every "during work hours" and "outside work hours" rule starts answering differently on the next call. Pass no schedule_id to clear it.

    Required permissions: call-routing.edit

    schedule_idstring numberstring
    Argument schema and validation
    schedule_idstringoptional
    The schedule to use, from get_work_hours. Omit to clear the schedule.
    numberstringoptional
    Set it for one phone number in international format (+255...). Omit to set the account default.
  • update_dispatch_rule vdb66bee6 writes Change an existing call routing rule. The change reaches live callers on the very next call. Read the rule with get_dispatch_rule first and send back the fields you want changed; if you make it forward somewhere, the destination is checked against the forwarding allow-list and the change is refused rather than saved half-working.

    Required permissions: call-routing.edit

    rule_idstring · required changes_jsonstring · required
    Argument schema and validation
    rule_idstringrequired
    The rule to change, from list_dispatch_rules.
    changes_jsonstringrequired
    A JSON object string with only the fields you are changing — same shape as create_dispatch_rule's rule_json (name, priority, enabled, conditions, actionType, actionConfig, fallbackActionType, fallbackActionConfig). Sending `conditions` REPLACES the whole condition list, so include the ones you are keeping.
  • update_ring_group vd4ff89e5 writes Change how a ring group behaves: whether it rings everyone at once or in order, how long it waits, and what happens when nobody answers. The change applies to the next call that reaches the group. Fields you leave out keep their current value.

    Required permissions: ring-groups.edit

    ring_group_idinteger · required namestring descriptionstring strategystring timeout_secondsinteger overflow_actionstring overflow_targetstring enabledboolean queue_wait_secondsinteger queue_timeout_actionstring queue_timeout_agent_config_idstring
    Argument schema and validation
    ring_group_idintegerrequired
    The group to change, from list_ring_groups.
    namestringoptional
    What to call the group, e.g. "Sales" or "After hours".
    descriptionstringoptional
    A sentence saying who this group is for.
    strategystringoptional
    simultaneous rings everyone at once; sequential rings them one after another in member order.
    timeout_secondsintegeroptional
    How long to ring before overflow, 5 to 120 seconds.
    overflow_actionstringoptional
    What happens when nobody answers: hangup, forward, voicemail or queue.
    overflow_targetstringoptional
    Required when overflow_action is forward: the phone number in international format (+255...) to send the caller to.
    enabledbooleanoptional
    Whether the group is in service (default true).
    queue_wait_secondsintegeroptional
    Queue only: how long a caller waits before the queue times out.
    queue_timeout_actionstringoptional
    Queue only: hangup, dispatch_agent, forward or voicemail when the queue times out.
    queue_timeout_agent_config_idstringoptional
    Queue only: which AI agent takes over, required when queue_timeout_action is dispatch_agent.
Meetings /mcp/v1/meetings 2 read · 4 write

See and schedule meetings, and invite people to them.

  • get_meeting v0648f40e One meeting in full: its title, state, schedule, whether it records itself, who is currently in the room, and the link people use to join. Use it before inviting anybody, so the link you hand out belongs to the meeting you mean.

    Required permissions: calls.view

    meeting_idstring · required
    Argument schema and validation
    meeting_idstringrequired
    The meeting to read, from list_meetings.
  • list_meetings v06aa87dd The meetings on this account: what they are called, when they are scheduled, and whether each one is still to come, live now, or finished. Filter by status to answer "what is coming up" without reading the whole history.

    Required permissions: calls.view

    statusstring limitinteger
    Argument schema and validation
    statusstringoptional
    Only meetings in this state: scheduled, ready, live, ended or cancelled.
    limitintegeroptional
    Max meetings to return (default 25, max 100).
  • cancel_meeting vd5447f24 writes End a meeting. If it is live, everybody in the room is disconnected immediately; if it has not started, its link stops working. Nobody is told, so check with the user before calling this on a meeting other people are in.

    Required permissions: calls.end

    meeting_idstring · required
    Argument schema and validation
    meeting_idstringrequired
    The meeting to end, from list_meetings.
  • dial_out_to_meeting vc01e161d writes Ring a phone and put the person into a meeting when they answer, so they join without a link. NOTE: the call platform has not shipped this yet and it will tell you so — that is a missing platform feature, not a bad number, and the honest answer is to send the join link instead.

    Required permissions: calls.place, calls.participants.manage

    meeting_idstring · required tostring · required from_numberstring · required participant_namestring transportstring
    Argument schema and validation
    meeting_idstringrequired
    The meeting to ring them into, from list_meetings.
    tostringrequired
    The phone number to ring, in international format (+255...).
    from_numberstringrequired
    Which of this account's own numbers to ring from — a meeting has no caller ID of its own.
    participant_namestringoptional
    What to call them in the room.
    transportstringoptional
    sip for a normal call, whatsapp for a WhatsApp voice call (the number must be WhatsApp-enabled).
  • invite_to_meeting v11149934 writes Open a meeting for guests and hand back the link to invite them with. It marks a still-scheduled meeting ready so the link actually works, then returns it. It does NOT send anything to anybody — the user shares the link themselves.

    Required permissions: calls.participants.manage

    meeting_idstring · required namesarray
    Argument schema and validation
    meeting_idstringrequired
    The meeting to open, from list_meetings.
    namesarray<string>optional
    Who the link is for, so the reply names them back. Only a reminder for the user — nobody is contacted.
  • schedule_meeting vca2e0291 writes Create a meeting — either starting now or booked for a moment in the future — and hand back the link people join with. Nobody is told about it: this creates the room, and sending the link to anyone is a separate, human step.

    Required permissions: calls.place

    titlestring · required descriptionstring scheduled_atstring auto_recordboolean
    Argument schema and validation
    titlestringrequired
    What the meeting is called, as attendees will see it.
    descriptionstringoptional
    A sentence about what the meeting is for.
    scheduled_atstringoptional
    When it starts, as an ISO 8601 time WITH a time zone (2026-09-10T14:30:00+03:00 or ...Z). Omit to start it now.
    auto_recordbooleanoptional
    Record the meeting from the moment it starts (default false).
Messaging /mcp/v1/messaging 8 read · 5 write

Templates, sender IDs, campaigns, message history — and sending SMS and WhatsApp.

  • draft_message vc1215803 Compose a message and check it against everything that decides whether it would actually be delivered — the 24-hour WhatsApp window, template approval, the sender ID, the do-not-contact list and the SMS segment cost. SENDS NOTHING: it hands back the finished text for a human to send.

    Required permissions: communications.view

    channelstring tostring bodystring templatestring variablesarray senderstring
    Argument schema and validation
    channelstringoptional
    Which channel the message is for. Default whatsapp.
    enum
    ["sms","whatsapp"]
    tostringoptional
    The recipient, so the window, the do-not-contact list and the thread can be checked. Optional — omit for a generic draft.
    bodystringoptional
    The message text. Ignored when a template is given, since the template body is what Meta sends.
    templatestringoptional
    An approved template name or id, from list_templates. Required outside the 24-hour window.
    variablesarray<string>optional
    Values for the template placeholders, in order: the first fills {{1}}.
    senderstringoptional
    An SMS sender ID to send from. Checked for approval.
  • get_campaign vf7611799 Read one campaign in full: its audience, message, schedule, recurrence, and the live delivery breakdown — how many were delivered, are still queued, and failed, with the commonest failure reason.

    Required permissions: communications.campaigns.view

    campaignstring · required
    Argument schema and validation
    campaignstringrequired
    The campaign uid or id, from list_campaigns.
  • get_message_history v42b91418 What was sent and what happened to it: delivery states, failure reasons and billed segments across SMS and WhatsApp, filtered by direction, status, channel, contact or date. Use it to answer "did my message arrive" with the real status instead of a guess.

    Required permissions: communications.reports.view

    directionstring statusstring channelstring contactstring sincestring untilstring limitinteger
    Argument schema and validation
    directionstringoptional
    Only messages sent by the business, or only ones received.
    enum
    ["inbound","outbound"]
    statusstringoptional
    queued, sent, delivered, read, failed or received.
    channelstringoptional
    sms or whatsapp.
    contactstringoptional
    A phone number in any format, or part of one — the last nine digits are matched.
    sincestringoptional
    Only messages on or after this ISO 8601 date.
    untilstringoptional
    Only messages on or before this ISO 8601 date.
    limitintegeroptional
    Max messages to return (default 25, max 100). The summary counts every match, not just these.
  • get_template v7829b667 Read one message template in full: its body, header, footer, buttons, the variables it expects, its Meta approval state and — when it was rejected — why. Use it before sending so the variables you supply match the ones the template declares.

    Required permissions: communications.templates.view

    templatestring · required
    Argument schema and validation
    templatestringrequired
    The template name or id, from list_templates.
  • list_campaigns v926b34cf List bulk messaging campaigns, newest first, with how many recipients each has reached and how many failed. Filter by status or channel. Shows the campaigns this person may see — a personal-scope member sees the ones they created.

    Required permissions: communications.campaigns.view

    statusstring channelstring searchstring limitinteger
    Argument schema and validation
    statusstringoptional
    Filter by state: draft, scheduled, running, paused, completed or cancelled.
    channelstringoptional
    Filter by channel: sms or whatsapp.
    searchstringoptional
    Filter by campaign name.
    limitintegeroptional
    Max campaigns to return (default 25, max 100).
  • list_sender_ids vc3f017dc List the SMS sender IDs on this account with their per-country approval state. Only an APPROVED sender ID puts the business name on an SMS in that country; a pending one is not usable yet, and saying otherwise sends the user to chase a delivery that never happens.

    Required permissions: communications.sender-ids.view

    statusstring limitinteger
    Argument schema and validation
    statusstringoptional
    Filter by state: pending, approved or rejected.
    limitintegeroptional
    Max sender IDs to return (default 25, max 100).
  • list_templates v38bfd699 List the message templates on this account with their Meta approval state and the variables each one takes. Read this before drafting or sending WhatsApp: outside the 24-hour window an APPROVED template is the only thing that gets delivered.

    Required permissions: communications.templates.view

    channelstring approved_onlyboolean searchstring limitinteger
    Argument schema and validation
    channelstringoptional
    Filter to templates usable on one channel: whatsapp or sms.
    approved_onlybooleanoptional
    Only WhatsApp templates Meta has approved — the ones that will actually deliver outside the 24-hour window.
    searchstringoptional
    Filter by name or body text.
    limitintegeroptional
    Max templates to return (default 25, max 100).
  • list_whatsapp_senders vc5ea703e The WhatsApp numbers this business can send from, with the name each shows to customers. Read this before send_whatsapp when the account has more than one — passing the wrong `from`, or omitting it and letting the default apply, sends from a number the customer may not recognise.

    Required permissions: communications.view

  • create_template v1e25b0f1 writes Write a new message template and, for WhatsApp, submit it to Meta for review. Review takes minutes to a day and Meta may reject it — the template cannot be sent to anyone until it comes back approved, so tell the user that rather than implying it is ready.

    Required permissions: communications.templates.manage

    display_namestring · required bodystring · required channelsarray categorystring languagestring namestring header_textstring footerstring whatsapp_business_account_idstring
    Argument schema and validation
    display_namestringrequired
    What a human calls this template, e.g. "Order shipped".
    bodystringrequired
    The message text. Use {{1}}, {{2}} for the parts that change per recipient.
    channelsarray<string>optional
    Which channels it is for: ["whatsapp"], ["sms"], or both. Default whatsapp.
    categorystringoptional
    Meta's category. Utility for transactional notices, marketing for promotions.
    enum
    ["marketing","utility","authentication"]
    languagestringoptional
    Language code, e.g. en or sw. Default en.
    namestringoptional
    The machine handle. Derived from display_name when omitted.
    header_textstringoptional
    An optional one-line text header.
    footerstringoptional
    An optional footer, max 60 characters.
    whatsapp_business_account_idstringoptional
    Which WhatsApp business account to submit to. Required only when the account has more than one.
  • request_sender_id va0048540 writes Open a request for an SMS sender ID in one country — the business name that shows as the sender. This only files the request: an administrator reviews it against the operator rules, and nothing can be sent from the name until it is approved.

    Required permissions: communications.sender-ids.view

    sender_idstring · required countrystring · required notesstring
    Argument schema and validation
    sender_idstringrequired
    The sender name, 3-11 characters, letters digits and spaces only.
    countrystringrequired
    Where it will be used: ISO code (TZ) or country name.
    notesstringoptional
    Anything the reviewer should know — what the business is, what these messages are for.
  • send_bulk_sms v8b3d1d59 writes Send one SMS to many real phones at once. It will not send until you pass confirm_recipient_count matching the number of recipients exactly — a bulk send is irreversible, costs one message per segment per person, and a mistyped list is the expensive kind of mistake.

    Required permissions: communications.campaigns.manage, communications.send

    toarray · required bodystring · required confirm_recipient_countinteger · required senderstring interval_secondsinteger
    Argument schema and validation
    toarray<string>required
    The recipient numbers in international format. A comma-separated string is also accepted.
    bodystringrequired
    The message text, sent identically to everyone.
    confirm_recipient_countintegerrequired
    How many recipients you are sending to. Must equal the list length exactly, or nothing is sent.
    senderstringoptional
    An approved sender ID to send from. The account default is used when omitted.
    interval_secondsintegeroptional
    Seconds between each send, 0-10. Default 1, which keeps gateways happy.
  • send_sms vf38275ee writes Send one SMS to one real phone. This is irreversible and it costs money from the account wallet — an SMS over 160 characters is billed as several. Use draft_message first if the user has not approved the exact wording.

    Required permissions: communications.send

    tostring · required bodystring templatestring variablesarray senderstring
    Argument schema and validation
    tostringrequired
    The recipient phone number in international format, e.g. +255755123456.
    bodystringoptional
    The message text. Required unless a template is given.
    templatestringoptional
    An active SMS template name or id to send instead of free text.
    variablesarray<string>optional
    Values for the template placeholders, in order.
    senderstringoptional
    An approved sender ID to send from. The account default is used when omitted.
  • send_whatsapp ve26b8e53 writes Send a WhatsApp message to one real person: free text inside the 24-hour customer-service window, or an approved template at any time. Outside that window free text is DROPPED by Meta and never arrives, so this refuses it rather than reporting a send that did not happen.

    Required permissions: communications.send

    tostring · required bodystring templatestring variablesarray fromstring
    Argument schema and validation
    tostringrequired
    The recipient WhatsApp number in international format, e.g. +255755123456.
    bodystringoptional
    The message text. Only delivered inside the 24-hour window; outside it, use a template.
    templatestringoptional
    An APPROVED WhatsApp template name or id. The only thing that delivers outside the 24-hour window.
    variablesarray<string>optional
    Values for the template placeholders, in order: the first fills {{1}}.
    fromstringoptional
    Which of the account's WhatsApp numbers to send from, from list_whatsapp_senders. When the account has more than one, ask which rather than letting the default apply.
Inbox /mcp/v1/inbox 3 read · 4 write

Customer conversations across WhatsApp, SMS, social and email — read, assign, reply, and send new mail.

  • get_conversation vec38ed35 Read one customer conversation: the recent messages in order, who owns it, whether the AI is answering it, and whether a free-text reply would still be delivered. Read this before replying so the answer fits what was already said.

    Required permissions: communications.inbox.view

    conversation_idinteger · required limitinteger
    Argument schema and validation
    conversation_idintegerrequired
    The conversation id, from list_conversations.
    limitintegeroptional
    How many recent messages to return (default 30, max 100).
  • list_conversations v0d1e43e5 List customer conversations across WhatsApp, SMS, Instagram, Messenger, TikTok and email — newest activity first, with who is waiting, who owns the thread and whether the reply window is still open. Shows only the threads this person may see.

    Required permissions: communications.inbox.view

    channelstring statusstring unread_onlyboolean assigned_to_meboolean searchstring limitinteger
    Argument schema and validation
    channelstringoptional
    whatsapp, sms, instagram, messenger, tiktok or email.
    statusstringoptional
    active, archived or closed.
    unread_onlybooleanoptional
    Only threads with unread customer messages.
    assigned_to_mebooleanoptional
    Only threads assigned to the person this connection acts for.
    searchstringoptional
    Match a contact name, number, username or the last message.
    limitintegeroptional
    Max conversations to return (default 25, max 100).
  • search_messages v01eb2aab Search the words inside customer conversations — "refund", an order number, a place name — and get the matching messages with the thread each belongs to. Searches only the conversations this person may open, so it cannot be used to read somebody else's inbox.

    Required permissions: communications.inbox.view

    querystring · required channelstring directionstring sincestring limitinteger
    Argument schema and validation
    querystringrequired
    The words to look for inside message bodies.
    channelstringoptional
    Restrict to one channel: whatsapp, sms, instagram, messenger, tiktok or email.
    directionstringoptional
    Only what the customer wrote, or only what the business replied.
    enum
    ["inbound","outbound"]
    sincestringoptional
    Only messages on or after this ISO 8601 date.
    limitintegeroptional
    Max matches to return (default 25, max 100).
  • assign_conversation veddb12de writes Hand a customer conversation to a colleague or a team, or take the owner off it. The change shows immediately in everyone's inbox and holds against the routing rules for a few hours, because a person's choice should outrank a rule.

    Required permissions: communications.inbox.manage

    conversation_idinteger · required assign_to_idinteger assign_to_team_idinteger unassignboolean
    Argument schema and validation
    conversation_idintegerrequired
    The conversation to hand over.
    assign_to_idintegeroptional
    The team member id to give it to.
    assign_to_team_idintegeroptional
    A team id, when the whole team should pick it up rather than one person.
    unassignbooleanoptional
    Take the current owner off it and leave it unowned.
  • reply_to_conversation v68d82a23 writes Reply to a customer in an existing conversation. The message reaches a real person on their phone and cannot be unsent. On WhatsApp outside the 24-hour window only an approved template is delivered, so this refuses free text there instead of reporting a send that never lands.

    Required permissions: communications.send

    conversation_idinteger · required bodystring templatestring variablesarray
    Argument schema and validation
    conversation_idintegerrequired
    The conversation to reply in, from list_conversations.
    bodystringoptional
    What to say. Write in the language the customer is using.
    templatestringoptional
    An approved template name or id — required on WhatsApp once the 24-hour window has closed.
    variablesarray<string>optional
    Values for the template placeholders, in order.
  • send_email v5ecd8b8e writes Send a NEW email from one of this business's connected mailboxes to any address — the one channel you can start rather than only reply to. To answer an email already in the inbox use reply_to_conversation instead, which keeps it in the same thread. list_connected_accounts shows which mailboxes exist and their "email:41" keys.

    Required permissions: communications.send, communications.mailbox.view

    toarray · required subjectstring bodystring · required ccarray bccarray fromstring
    Argument schema and validation
    toarray<string>required
    Who it goes to, as email addresses. A single address or a comma-separated string is also accepted.
    subjectstringoptional
    The subject line. Leave it out only if you mean to send one without.
    bodystringrequired
    The message. Plain text is fine — line breaks are kept. HTML is sent as written.
    ccarray<string>optional
    Copied addresses.
    bccarray<string>optional
    Blind-copied addresses. The other recipients do not see these.
    fromstringoptional
    Which mailbox to send from — its address, its id, or the "email:41" key from list_connected_accounts. Required when the account has more than one.
  • set_conversation_ai_mode v42861bc8 writes Decide who answers one conversation: the AI agent, a chat flow, nobody automatic, or whatever the channel normally does. Switching to off is how a person takes a chat back from the AI mid-conversation; it changes live behaviour on the next customer message.

    Required permissions: communications.engine.manage

    conversation_idinteger · required modestring · required flow_idinteger
    Argument schema and validation
    conversation_idintegerrequired
    The conversation to change.
    modestringrequired
    on = the AI answers it; off = nobody automatic does, a person must; flow = a chat flow drives it; inherit = the channel default.
    enum
    ["on","off","flow","inherit"]
    flow_idintegeroptional
    Which published flow to pin, when mode is flow. Without one the flow is chosen by trigger as usual.
Comments /mcp/v1/comments 2 read · 4 write

Comments on your Facebook, Instagram and TikTok posts.

  • get_comment_thread vbaf5728a Read the whole comment conversation under one post: what was posted, every comment in order including the business's own replies, and what this platform actually allows you to do to a comment. Read it before replying, so you do not answer a point somebody already answered.

    Required permissions: comments.view

    comment_idinteger · required limitinteger
    Argument schema and validation
    comment_idintegerrequired
    Any comment on the post, from list_comments. The whole thread comes back around it.
    limitintegeroptional
    Max comments in the thread to return (default 50, max 100).
  • list_comments v8e87c9c1 List public comments on the business's Facebook, Instagram and TikTok posts — newest first, defaulting to the ones still needing an answer. Filter by platform, post, label or free text. Shows only the accounts this person is assigned to.

    Required permissions: comments.view

    viewstring platformstring post_idinteger labelstring searchstring limitinteger
    Argument schema and validation
    viewstringoptional
    attention (default) = still needs an answer; handoff = what the AI could not answer and is waiting on a person for.
    enum
    ["attention","all","hidden","resolved","mine","handoff"]
    platformstringoptional
    Only comments from one platform.
    enum
    ["facebook","instagram","tiktok"]
    post_idintegeroptional
    Only comments on one post.
    labelstringoptional
    Only comments carrying this label.
    searchstringoptional
    Match the comment text or the author.
    limitintegeroptional
    Max comments to return (default 25, max 100).
  • assign_comment v21d1dead writes Give a comment to a colleague to answer, or take the owner off it. Nothing is posted publicly — this only decides whose queue it lands in, and a person's choice outranks the routing rules for a few hours afterwards.

    Required permissions: comments.manage

    comment_idinteger · required assign_to_idinteger unassignboolean
    Argument schema and validation
    comment_idintegerrequired
    The comment to hand over.
    assign_to_idintegeroptional
    The team member id to give it to.
    unassignbooleanoptional
    Take the current owner off it and leave it unowned.
  • hide_comment vb51cae1d writes Hide a comment so the public can no longer see it under the post. It is not deleted and the author is not told — they still see their own comment, which is what makes hiding the calm option. unhide_comment puts it back.

    Required permissions: comments.manage

    comment_idinteger · required
    Argument schema and validation
    comment_idintegerrequired
    The comment to hide, from list_comments.
  • reply_to_comment v52fbb635 writes Reply publicly to a comment on the business's Facebook, Instagram or TikTok post. Everyone can read this reply, it is posted in the business's own name, and it cannot be quietly unsent — show the user the exact wording before calling this.

    Required permissions: comments.manage

    comment_idinteger · required messagestring · required
    Argument schema and validation
    comment_idintegerrequired
    The comment to answer, from list_comments.
    messagestringrequired
    The public reply. Match the language the commenter used.
  • unhide_comment vea372744 writes Put a hidden comment back in public view under the post. Use it when a comment was hidden by mistake, or once the thing it complained about has been sorted out and the answer belongs where everyone can read it.

    Required permissions: comments.manage

    comment_idinteger · required
    Argument schema and validation
    comment_idintegerrequired
    The hidden comment to restore.
Posts /mcp/v1/posts 3 read · 4 write

Social posts to Facebook, Instagram, TikTok, YouTube and LinkedIn: what is drafted, scheduled and sent; drafting a new one; scheduling or publishing it.

  • get_post v9690b11b Read one post in full: the whole text, every attached picture or video, the per-platform options, each target's state and public link, the pre-flight warnings, and any pending approval. Read it before changing or sending it.

    Required permissions: posts.view

    post_uidstring · required
    Argument schema and validation
    post_uidstringrequired
    The post id (pub_…), from list_posts.
  • list_posts v0ccc225e List this business's social posts — drafts, what is scheduled, what went out and what failed — newest first, scheduled ones first. Filter by status or platform, or search the text. Start here before scheduling anything, so you do not post the same thing twice.

    Required permissions: posts.view

    statusstring platformstring qstring limitinteger
    Argument schema and validation
    statusstringoptional
    Only posts in this state. `sent` is published or partially published; `scheduled` includes ones publishing right now.
    enum
    ["draft","scheduled","sent","failed","cancelled"]
    platformstringoptional
    Only posts with a target on this platform.
    enum
    ["facebook","instagram","tiktok","youtube","linkedin"]
    qstringoptional
    Text to search for in the post body.
    limitintegeroptional
    Max rows (default 20, max 50).
  • list_publish_targets v93db0fed The social accounts a post can go to — Facebook Pages, Instagram accounts, TikTok, YouTube channels, LinkedIn — with whether each can publish right now and why not when it cannot. Pass their ids as targets to create_post_draft.

    Required permissions: posts.view

  • cancel_post vcca78ef5 writes Stop a scheduled post before it goes out: it is marked cancelled and will not be sent. A post already publishing may only stop the targets that have not started. Nothing already published on a platform is removed by this — that is done from the app. To reuse the text, duplicate the post in the app.

    Required permissions: posts.manage

    post_uidstring · required
    Argument schema and validation
    post_uidstringrequired
    The scheduled post to stop, from list_posts.
  • create_post_draft vcb3b5740 writes Create a post as a DRAFT: the text, the accounts it goes to (from list_publish_targets), optional media asset uids the business uploaded, per-platform options, and a first comment. Nothing is published by this tool — read the draft back with get_post, show the user, then schedule_post or publish_post_now. Options are validated against each platform: YouTube needs a title and "made for kids"; TikTok needs "who can view".

    Required permissions: posts.manage

    bodystring · required target_account_idsarray · required media_uidsarray first_commentstring optionsstring notesstring idempotency_keystring
    Argument schema and validation
    bodystringrequired
    The text of the post. Match the language the business uses with its customers.
    target_account_idsarray<integer>required
    The accounts it goes to, from list_publish_targets.
    media_uidsarray<string>optional
    Media asset uids to attach, in order (uploaded by the business in the composer or through the API). Leave empty for text only.
    first_commentstringoptional
    A comment the business posts under its own post right after it goes up — hashtags, a link — where the platform allows it.
    optionsstringoptional
    Per-platform options as a JSON object keyed by platform, e.g. {"youtube":{"title":"…","made_for_kids":false},"tiktok":{"privacy_level":"PUBLIC_TO_EVERYONE"}}. Omitted keys take the account's saved defaults.
    notesstringoptional
    An internal note on the post, for the team.
    idempotency_keystringoptional
    Any string of yours; sending the same key again returns the same post instead of a second one.
  • publish_post_now v152b2195 writes Publish a draft right now, publicly, in the business's name, on every target it has. This cannot be quietly unsent — show the user the exact text and the accounts before calling it. Pre-flight warnings block it until listed in accept_warnings; errors block it outright.

    Required permissions: posts.manage

    post_uidstring · required accept_warningsarray
    Argument schema and validation
    post_uidstringrequired
    The draft to publish, from list_posts or create_post_draft.
    accept_warningsarray<string>optional
    Warning codes from get_post's verification that the user has seen and accepts.
  • schedule_post v109fd491 writes Schedule a draft to go out publicly at a given time, in the business's name, on every target it has. Once the time comes it cannot be quietly unsent — confirm the exact text and time with the user first. Pre-flight warnings (a caption over a platform's limit, a picture the platform will crop) block scheduling until they are listed in accept_warnings; errors block it outright.

    Required permissions: posts.manage

    post_uidstring · required atstring · required tzstring accept_warningsarray
    Argument schema and validation
    post_uidstringrequired
    The draft to schedule, from list_posts or create_post_draft.
    atstringrequired
    When it goes out, ISO-8601 (e.g. 2026-09-20T09:00:00). Read in tz when given, else in the workspace timezone.
    tzstringoptional
    IANA timezone for `at`, e.g. Africa/Dar_es_Salaam.
    accept_warningsarray<string>optional
    Warning codes from get_post's verification that the user has seen and accepts.
Contacts /mcp/v1/contacts 4 read · 4 write

The contact book and groups.

  • get_contact vabce7916 One contact in full: name, the full phone number to dial or message, the group they belong to, whether they are subscribed, and every custom field their group defines. Takes either the numeric id or the ctc_ reference.

    Required permissions: contacts.view

    contact_idstring · required
    Argument schema and validation
    contact_idstringrequired
    The contact id, or its ctc_ reference.
  • list_contact_groups v4d49b7f6 The contact groups this account keeps: name, how many contacts are in each, how many still accept messages, and the custom fields each group defines. Read this before creating or importing contacts — every contact belongs to a group.

    Required permissions: contacts.view

    limitinteger
    Argument schema and validation
    limitintegeroptional
    Max groups to return (default 25, max 100).
  • list_contacts v545d7238 The contact book on this account: name, phone number, which group each one is in and whether they still accept messages. Narrow it to one group with group_id, or use search_contacts when you have a name or a number.

    Required permissions: contacts.view

    group_idinteger subscribed_onlyboolean limitinteger
    Argument schema and validation
    group_idintegeroptional
    Only contacts in this group. list_contact_groups gives the ids.
    subscribed_onlybooleanoptional
    Only contacts who still accept messages.
    limitintegeroptional
    Max contacts to return (default 25, max 100).
  • search_contacts v3f1497f0 Find a contact by name or by phone number, across every group on the account. Handles a number written any way — with or without the country code, with spaces or a leading zero — so "who is 0712 345 678" resolves to a person.

    Required permissions: contacts.view

    querystring · required limitinteger
    Argument schema and validation
    querystringrequired
    A name, part of a name, or a phone number in any format.
    limitintegeroptional
    Max matches to return (default 25, max 100).
  • add_to_group v3ef14d5c writes Put an existing contact in a different group. A contact belongs to exactly one group on this platform, so this MOVES them — they leave the group they are in now, and any campaign aimed at the old group stops including them.

    Required permissions: contacts.edit

    contact_idstring · required group_idinteger · required
    Argument schema and validation
    contact_idstringrequired
    The contact id, or its ctc_ reference.
    group_idintegerrequired
    The group to move them into.
  • create_contact v93d7601c writes Add one contact to a group: a name and a phone number, plus any custom fields that group defines. Refuses a number the group already holds rather than creating a second copy of the same person.

    Required permissions: contacts.create

    group_idinteger · required namestring · required phonestring · required country_codestring subscribedboolean custom_fieldsobject
    Argument schema and validation
    group_idintegerrequired
    Which group to add them to. list_contact_groups gives the ids.
    namestringrequired
    The person's name.
    phonestringrequired
    Their phone number. Full international form is safest, e.g. +255712345678.
    country_codestringoptional
    Optional country code when the number is written nationally, e.g. "255".
    subscribedbooleanoptional
    Whether they accept messages. Default true.
    custom_fieldsobjectoptional
    Values for the custom fields this group defines, keyed by field key.
  • import_contacts vaa0abaa3 writes Add many contacts to one group in a single call. Bounded and deliberately awkward: you must state how many rows you are importing and the count must match, because an import that quietly adds the wrong number of people is discovered weeks later on a bill.

    Required permissions: contacts.import

    group_idinteger · required rows_jsonstring · required confirm_countinteger · required
    Argument schema and validation
    group_idintegerrequired
    The group to import into. list_contact_groups gives the ids.
    rows_jsonstringrequired
    A JSON array of {"name":"…","phone":"…","country_code":"…","subscribed":true,"custom_fields":{…}} objects. At most 500.
    confirm_countintegerrequired
    How many contacts you are importing. Must equal the number of rows, or nothing is imported.
  • update_contact va403030f writes Change a contact: their name, their phone number, their custom fields, or whether they still accept messages. Only the fields you pass change. Unsubscribing here stops campaigns reaching them.

    Required permissions: contacts.edit

    contact_idstring · required namestring phonestring country_codestring subscribedboolean custom_fieldsobject
    Argument schema and validation
    contact_idstringrequired
    The contact id, or its ctc_ reference.
    namestringoptional
    New name.
    phonestringoptional
    New phone number.
    country_codestringoptional
    Country code, when the new number is written nationally.
    subscribedbooleanoptional
    Whether they accept messages. False stops campaigns reaching them.
    custom_fieldsobjectoptional
    Custom field values to set, keyed by field key. Merged with what is already there.
Overview /mcp/v1/overview 6 read

The dashboard, business analytics, call stats and spend — how the business is doing.

  • get_business_analytics v15b81d8c The results of the AI analysis of recorded calls over a period: satisfaction, complaints, churn-risk flags, resolution and escalation rates, and the caller journey funnel. Reads finished analyses only — it never starts a new analysis run.

    Required permissions: calls.business-analytics.view

    rangestring numberstring
    Argument schema and validation
    rangestringoptional
    Period to read: 24h, 7d (default) or 30d.
    numberstringoptional
    Optional. One phone number in E.164, to read that line only.
  • get_call_stats v1691744e Call volume over a period, split by direction and by what happened: answered, missed, still live, total talk time and the daily shape. Use this when someone asks whether calls are up, or when the busy days are.

    Required permissions: dashboard.stats.view

    daysinteger
    Argument schema and validation
    daysintegeroptional
    How many days back to count, ending today. Default 7, max 60.
  • get_dashboard vbfd76a36 How the business is doing right now, in one call: calls today, message delivery, wallet, who is on duty, and an "attention" list of things that are actually wrong with the page that fixes each one. Start here before any other overview tool.

    Required permissions: dashboard.view

  • get_live_calls vc2e4bde7 What is happening on the phones this second: the calls currently ringing or connected, who is on each one, how long it has been running. Answers "is anybody waiting right now".

    Required permissions: dashboard.live-calls.view

    limitinteger
    Argument schema and validation
    limitintegeroptional
    Max live calls to return (default 25, max 100).
  • get_spend_summary v4d660775 Where the money went over a period: total spent, broken down by category (calls, messages, AI usage, numbers), what was topped up, and the balance left. Answers "why is my balance down".

    Required permissions: billing.view

    daysinteger
    Argument schema and validation
    daysintegeroptional
    How many days back to total, ending today. Default 30, max 92.
  • get_subscription v4510fba1 How the workspace pays: pay-as-you-go, or the package it is on with what is left of each allowance (inbound and outbound minutes, WhatsApp messages, IVR runs, flow sessions, comments, AI credits), the per-message SMS price the package sets, the renewal date and any shortfall. Answers "how many minutes do I have left", "what does an SMS cost me" and "when does my package renew". Read-only: subscribing is done by a person at /app/billing/plans; Enterprise terms are agreed with us.

    Required permissions: billing.subscription.view

Connected accounts /mcp/v1/accounts 1 read

The WhatsApp numbers, social profiles, mailboxes and SMS routes this business has connected, and what each can actually do.

  • list_connected_accounts v1378108b Every account connected to this business — WhatsApp numbers, Facebook pages, Instagram and TikTok profiles, LinkedIn, YouTube, email and SMS — with what each can actually do right now and whether it needs attention. Start here when asked what accounts exist, or to pick which one to send, post or reply from.

    Required permissions: communications.accounts.view

    kindstring needs_attentionboolean limitinteger
    Argument schema and validation
    kindstringoptional
    Only one kind: whatsapp, facebook, instagram, tiktok, linkedin, youtube, email or sms.
    needs_attentionbooleanoptional
    Only accounts that are not healthy — expired tokens, missing permissions, disconnections.
    limitintegeroptional
    Max accounts to return (default 25, max 100).
Finding things /mcp/v1/navigate 4 read

Where pages and settings live in the app, and what each form asks for.

  • describe_action va736d516 One form in full: the page it is on, every field with what it actually asks for in plain words, what pressing save does, and whether it costs money or reaches a customer. Call it before walking someone through a form so you ask for the right things in the right order. action_idstring · required
    Argument schema and validation
    action_idstringrequired
    The id from list_actions — e.g. billing.topup, contacts.group.create.
  • find_page v89fb7b08 Answer "where do I change X" with a real path. Ask it in the words the user used — "where do I top up", "change what callers hear first", "reply templates" — and it returns the best pages with what each is for, filtered to what this connection can actually open. querystring · required limitinteger
    Argument schema and validation
    querystringrequired
    What the user is trying to do, in their words. "where do I change my greeting", "top up", "delivery reports".
    limitintegeroptional
    How many pages to return. Default 5, max 20.
  • list_actions v1c81ecfc The forms in the app this connection could walk someone through: what each one is called, which page it is on, the fields it asks for, and how serious pressing save is (navigate, reversible, or critical). Use it to prepare someone before they open the page, not to submit anything. pagestring commitstring
    Argument schema and validation
    pagestringoptional
    Only forms on this page, by path — e.g. /app/billing.
    commitstringoptional
    Only forms of this seriousness: navigate, reversible, or critical. Default: all of them.
  • list_pages vb7b57d51 The map of the app: every page this connection can actually open, with its path, its name in the sidebar, and what it is for. Read it once to learn where things live, then say "Settings → Integrations → API keys" instead of guessing a URL. Narrow it with section to keep the answer small. sectionstring limitinteger
    Argument schema and validation
    sectionstringoptional
    Only pages in this part of the app — Calls, Messaging, Marketplace, Settings, and so on. The full list of section names comes back with every answer.
    limitintegeroptional
    At most this many pages. Default and maximum 200.
Account /mcp/v1/account 13 read · 3 write

A cross-domain starting point: overview, search, fetch, and the most-used read tools.

  • fetch vf9389191 Read one record in full, using an id returned by search exactly as search returned it: "ivr:12", "flow:4" and "asset:41" carry a number, a ticket id carries the ticket's uuid. A flow too big to return whole comes back as an overview naming the call that reads the rest. idstring · required
    Argument schema and validation
    idstringrequired
    An id from search, passed through verbatim — "ivr:12", "flow:4" and "asset:41" carry a number, "ticket:…" carries the ticket's uuid.
  • get_account_overview v5f2016cd A one-call picture of this account: what it is called, how many call flows and message flows it has, how many are live, how many phone numbers, how much audio. Good opening move when you do not yet know what you are working with.
  • get_ivr_catalog vff05c216 The IVR building reference: every node kind you may use, the exact fields each one allows, which action dialect it speaks, and the resource ids that actually exist on this account (agents, models, voices, SMS senders, audio assets). ALWAYS call this before your first apply_ivr_ops — inventing a field or an id is the most common way a batch is rejected.

    Required permissions: ivr.view

  • get_ivr_flow v87fd2010 Read one call flow: every node, where each one sends the caller, the entry point, the current version number, and what the IVR engine validator says about it right now. A flow too big to answer in one call comes back as an overview that names the sections to ask for next — pass section and cursor to walk it. Read this before proposing edits, and pass the version back to apply_ivr_ops.

    Required permissions: ivr.view

    flow_idinteger · required sectionstring cursorstring summaryboolean node_idsstring expected_versioninteger
    Argument schema and validation
    flow_idintegerrequired
    The flow id, from list_ivr_flows.
    sectionstringoptional
    Which piece to read: overview, nodes, transitions, errors or warnings. Leave it out to get the whole flow in one answer, which is what happens whenever it fits.
    cursorstringoptional
    Where to resume a section: the next_cursor the previous page returned, verbatim. It carries the graph snapshot with it, so a flow that is edited or rolled back mid-read is refused rather than stitched together. Omit it, or pass "0", for the first page — any LATER page must carry the snapshot, so build the cursor from next_cursor rather than from page.from.
    summarybooleanoptional
    On section "nodes", return id/type/name/targets/fields instead of whole nodes — the whole graph in far fewer pages.
    node_idsstringoptional
    Node ids to read in full, comma-separated ("entry,menu_malipo") or as a list. Ids that are not on the flow come back in not_found. This pages like any section: if the nodes asked for do not fit one answer, send the SAME node_ids back with the next_cursor.
    expected_versionintegeroptional
    Optional guard: refuse if the draft counter is not this. Paging does not need it — the cursor already pins the graph — and the counter also moves on a rename or a canvas drag, so passing it on every page can abandon a walk for an edit that changed no node.
  • get_message_flow v04f29c16 Read one message flow: nodes, edges, triggers, its version number, and what the validator currently says. A flow too big to answer in one call comes back as an overview that names the sections to ask for next — pass section and cursor to walk it, and expected_version to be sure every page is the same draft. Pass the version back to apply_flow_ops so you do not overwrite somebody else.

    Required permissions: flows.view

    flow_idinteger · required sectionstring cursorstring summaryboolean node_idsstring expected_versioninteger
    Argument schema and validation
    flow_idintegerrequired
    The flow id, from list_message_flows.
    sectionstringoptional
    Which piece to read: overview, nodes, edges, variables, issues or triggers. Leave it out to get the whole flow in one answer, which is what happens whenever it fits.
    cursorstringoptional
    Where to resume a section: the next_cursor the previous page returned, verbatim. It carries the graph snapshot with it, so a flow that is edited or rolled back mid-read is refused rather than stitched together. Omit it, or pass "0", for the first page.
    summarybooleanoptional
    On section "nodes", return id/kind/name/outs/exits instead of full config — the whole graph in far fewer pages.
    node_idsstringoptional
    Comma-separated node ids to read in full, e.g. "start,pay_collect_v12". Ids that are not on the flow come back in not_found.
    expected_versionintegeroptional
    Optional guard: refuse if the draft counter is not this. Paging does not need it — the cursor already pins the graph — and the counter also moves on a rename or a canvas drag, so passing it on every page can abandon a walk for an edit that changed no node.
  • list_agents v6b127ddb The AI specialists this business has set up — what each one is for and whether it is available. Ask one a question with ask_agent when it knows something you do not.

    Required permissions: agents.ai.view

    limitinteger
    Argument schema and validation
    limitintegeroptional
    Default 25, max 100.
  • list_assets v55ed7b79 List the audio already in this account's Asset Studio. Check here before generating — the clip you need may exist.

    Required permissions: asset-studio.view

    searchstring statusstring limitinteger
    Argument schema and validation
    searchstringoptional
    Filter by name.
    statusstringoptional
    ready, processing or failed.
    limitintegeroptional
    Default 25, max 100.
  • list_ivr_flows vd1ab45b8 List the call (IVR) flows on this account: name, status, size, whether it has unpublished changes, and when it last changed. Start here before editing anything.

    Required permissions: ivr.view

    statusstring searchstring limitinteger
    Argument schema and validation
    statusstringoptional
    Filter by status: draft, active or paused.
    searchstringoptional
    Filter by name.
    limitintegeroptional
    Max flows to return (default 25, max 100).
  • list_message_flows v91dff075 List the WhatsApp conversation flows on this account, with status, priority, how many triggers each has and whether it is actually live for customers.

    Required permissions: flows.view

    statusstring searchstring limitinteger
    Argument schema and validation
    statusstringoptional
    draft, active, paused or archived.
    searchstringoptional
    Filter by name.
    limitintegeroptional
    Default 25, max 100.
  • list_my_numbers v7b8bfcd0 The phone numbers this business already owns.

    Required permissions: numbers.view

    limitinteger
    Argument schema and validation
    limitintegeroptional
    Default 25, max 100.
  • list_voices vd9762d10 List the voices available for generating speech, with language, gender, style and a preview URL. Pick from here rather than generating candidates — previews already exist and cost nothing, generation costs money.

    Required permissions: asset-studio.view

    languagestring genderstring providerstring limitinteger
    Argument schema and validation
    languagestringoptional
    e.g. "sw" for Kiswahili, "en" for English.
    genderstringoptional
    male or female.
    providerstringoptional
    Filter to one provider.
    limitintegeroptional
    Default 25, max 100.
  • search v6665f820 Search across everything in this account — call flows, message flows, audio and support tickets — and get back ids you can pass to fetch. Use it when you know roughly what you are looking for but not where it lives. querystring · required limitinteger
    Argument schema and validation
    querystringrequired
    What to look for.
    limitintegeroptional
    Default 20, max 50.
  • search_available_numbers va86e1247 Search phone numbers available to buy right now, with their monthly price. Prices here are indicative — quote_number gives the binding total including any deposit.

    Required permissions: numbers.view

    prefixstring number_type_idinteger limitinteger
    Argument schema and validation
    prefixstringoptional
    E.164 prefix, e.g. "+255".
    number_type_idintegeroptional
    Restrict to one number type.
    limitintegeroptional
    Default 25, max 100.
  • apply_flow_ops v65b46727 writes Build or edit a WhatsApp conversation flow by applying graph operations to its draft. Checked by the real flow validator before anything is written, and the user's canvas updates immediately. The flow stays a DRAFT — you cannot make it reach customers.

    Required permissions: flows.edit

    flow_idinteger · required ops_jsonstring · required expected_versioninteger auto_layoutboolean
    Argument schema and validation
    flow_idintegerrequired
    The flow to edit.
    ops_jsonstringrequired
    A JSON object string {"ops":[...]}. Ops: add_node, update_node, remove_node, set_edge {from,out,to}, remove_edge {from,out}, set_entry. Max 40. Call get_flow_catalog first — an edge "out" must be one the node kind actually has.
    expected_versionintegeroptional
    The version from get_message_flow. Stops you overwriting somebody else.
    auto_layoutbooleanoptional
    Arrange the canvas after applying (default true). Set false only if you are placing nodes yourself.
  • apply_ivr_ops v370cd99e writes Build or edit a call flow by applying graph operations to its draft. The whole batch is checked by the real IVR engine validator before anything is written, and the result appears immediately on the canvas if the user has it open. The flow stays a DRAFT — publishing is the user's.

    Required permissions: ivr.edit

    flow_idinteger · required ops_jsonstring · required expected_versioninteger auto_layoutboolean
    Argument schema and validation
    flow_idintegerrequired
    The flow to edit, from list_ivr_flows.
    ops_jsonstringrequired
    A JSON object string {"ops":[...]}. Each op is {"op":"add_node","node":{...}} | {"op":"update_node","id":"...","set":{...}} | {"op":"remove_node","id":"..."} | {"op":"set_entry","id":"..."}. Max 30. Call get_ivr_catalog first for the node kinds and fields.
    expected_versionintegeroptional
    The version you read in get_ivr_flow. Strongly recommended: it is what stops you overwriting a change somebody else made in the meantime.
    auto_layoutbooleanoptional
    Arrange the canvas as a tidy tree after applying (default true). Set false only if you are placing nodes yourself with format_ivr_layout.
  • generate_speech v5dd9ab56 writes Turn text into spoken audio using one of the account's voices, and put it in Asset Studio. Use it for IVR greetings, menu prompts and voicemail messages. Generation costs money, so pick the voice with list_voices first and do not generate variations speculatively.

    Required permissions: asset-studio.manage

    textstring · required voice_idstring · required namestring · required wait_msinteger confirm_longboolean
    Argument schema and validation
    textstringrequired
    What to say. Write it in the language the caller will hear.
    voice_idstringrequired
    A voice_id from list_voices.
    namestringrequired
    A name for the clip, e.g. "greeting_sw".
    wait_msintegeroptional
    How long to wait for it to finish before returning a handle. Default 8000, max 20000.
    confirm_longbooleanoptional
    Required for text over 1200 characters, after checking with the user.

The same ground, both ways

REST group MCP server
Authentication Account /mcp/v1/account
SMS Messaging /mcp/v1/messaging
WhatsApp Messaging /mcp/v1/messaging, Inbox /mcp/v1/inbox
WhatsApp groups WhatsApp groups /mcp/v1/groups
WhatsApp templates Messaging /mcp/v1/messaging
Contacts Contacts /mcp/v1/contacts
Catalogue Shop /mcp/v1/shop, Orders /mcp/v1/orders
Profile & Balance Overview /mcp/v1/overview, Account /mcp/v1/account
Webhooks

Webhooks have no MCP equivalent, and will not: MCP is request and response with the model asking, while Momo Business calling you when something happens stays an HTTP callback.

What you can grant

You pick the areas when you connect. The ones that reach your customers or your money are separate, and off unless you turn them on.

  • Overview and analyticsHow the business is doing — calls, messages, spend, and what needs attention.
  • CallsCall history, recordings, transcripts and Call Studio scripts.
  • Call routingRouting rules, ring groups, working hours and forwarding targets.
  • Phone numbersWhat you own, what is available, what one costs, and how it is configured.
  • MeetingsSee and schedule meetings, and invite people to them.
  • Call flows and chat flowsBuild and edit your IVRs and WhatsApp conversation flows — as drafts.
  • Data tablesThe tables your business defined for itself — read records, save them, shape fields, run reports — and the business rules (limits, fees, eligibility, opening hours) your flows enforce. Flows and IVRs read the same tables and the same rules.
  • Voice and audioVoices, and generating spoken prompts for your call flows.
  • ContactsYour contact book and groups.
  • AI agentsYour AI agents, what they know, how they behave, and what they have done.
  • Orders and shopCustomer orders, products, brands and categories.
  • Support ticketsTickets and your knowledge base.
  • Connected accountsWhich WhatsApp numbers, social profiles and mailboxes are connected, and what each can do.
  • ApprovalsDecisions people in your business are waiting on — what is pending, what was decided, and why. Answering one is separate.
  • PaymentsMoney your customers pay you: what has been asked for, what arrived, and each payment's history. Asking for money and refunding it need the spending tick as well.
  • AutomationsWhat your business has set up to happen on its own — what reacts to an event, what runs on a rhythm, and a log of what actually fired. Changing any of it is separate.
  • Alerts and service levelsHow your business watches itself: what it has asked to be told about, how quickly it promises to do things, what it checks before letting something through, and a log of everything that fired — including anything that reached nobody. Changing any of it is separate.
  • OperationsThe named things your business can do — create a booking, register a customer, process a refund. Seeing what they are is included; DOING one needs the ticks its own steps call for.
  • Finding thingsWhere pages and settings live in the app, so it can point you to them.
  • MessagingTemplates, sender IDs, campaigns and your message history. Sending is separate.
  • InboxRead your customer conversations across WhatsApp, SMS, social and email.
  • CommentsRead comments on your Facebook, Instagram and TikTok posts.
  • PostsRead what the business has drafted, scheduled and published on its social accounts. Drafting, scheduling and publishing are separate.
  • WhatsApp groupsGroups your business runs from its WhatsApp number.

Going further

  • Publish thingsMake a call flow answer real calls, a chat flow reach real customers, or a routing change go live.
  • Send messages and place callsSend an SMS, WhatsApp or email to a real person, reply to a customer, or ring a phone.
  • Start purchases and ask customers to payBegin buying a number or topping up, and ask your customers to pay you. You still approve every payment yourself, on your phone, and a refund still waits for somebody in your business to say yes.
  • Delete thingsPermanently remove flows, audio, contacts and tickets.
  • Save and change recordsCreate, update and upsert rows in your data tables, and save reports.
  • Change tables and fieldsCreate tables, add, rename, retype or remove fields, put indexes and "unique together" rules on them, and arrange tables into groups. This changes what every screen and flow sees.
  • Set up things that run without youCreate or change an automation: something that reacts to an event on its own, or runs on a rhythm — including sending your business's data to an address outside it.
  • Answer approvals for youApprove or reject a request somebody is waiting on — releasing a discount, a refund or a payout that was deliberately held for a person to sign off.

Machine-readable: /api-docs/mcp.json carries every tool with a full JSON Schema for its arguments, and /api-docs/openapi.json carries the transport itself under the MCP tag — the endpoints, the JSON-RPC envelope and the OAuth handshake. Both are generated from the same code.