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

70 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

SMS5 operationsWhatsApp3 operationsWhatsApp groups13 operationsContacts5 operationsProfile & Balance2 operationsCatalogue14 operationsData tables12 operationsPayments2 operationsAutomations3 operationsAgent tasks2 operationsOperations1 operationsMCP8 operationsWebhooks2 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.

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 a business phone identity belonging to the account; omitting it uses configured defaults. Use the provider phone-number identity shown by your connection, not a local database channel ID.

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

Contacts belong to a contact group. Obtain the group ID from the account's contact management interface or an authorized MCP contact-group tool. This REST family operates inside a known group; it does not include a group-list or group-create endpoint.

Compatibility routes

Method and path Action
POST /api/v3/contacts/{group_id}/store Create a contact
POST /api/v3/contacts/{group_id}/search/{uid} Read one contact
PATCH /api/v3/contacts/{group_id}/update/{uid} Update a contact
DELETE /api/v3/contacts/{group_id}/delete/{uid} Delete a contact
POST /api/v3/contacts/{group_id}/all List contacts

The group identifier may be a numeric ID or group UUID. Contact lookup accepts its public UID or numeric ID, but only within the requested group and account. Missing group/contact returns 404. The names search and all do not change their required HTTP method.

Create with explicit phone information

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

PHONE is required and limited to 64 characters; phone_number is an alias. If both are supplied, PHONE wins. Country code is optional, at most eight characters, and should be sent explicitly when you need split country/national fields. With the example above the stored national number is 712345678 and the country code is 255.

Normalization removes nondigits and a matching supplied country code, then removes a national trunk zero when a country code is present. It does not infer country_code merely because PHONE begins with +255; if no country_code is supplied, the digits remain in phone_number. This contact parser differs from the data-table phone field's regional normalization.

Name selection uses name or NAME, then joins FIRST_NAME and LAST_NAME, then falls back to the normalized phone. The normal name field is limited to 160 characters. is_subscribed defaults true at creation.

Every nonreserved request field is saved under custom_field_values. Reserved fields are PHONE, phone_number, country_code, name, NAME, FIRST_NAME, LAST_NAME, is_subscribed and _token. Send custom values at the top level, as in the example; wrapping them inside a custom_field_values input would store that wrapper as a custom key.

PATCH is replacement-like

This endpoint still requires PHONE or phone_number on an update. It recalculates name from the submitted payload and replaces the complete custom-field map. Omitted is_subscribed retains its previous value, but omitted name/custom fields do not follow that same rule.

To retain data, read the contact first, merge the changes in your application, and send the phone, desired name and all custom fields you intend to preserve. This differs from data-record PATCH, which merges only submitted keys.

Listing and campaign use

The contact list accepts search across name and national phone_number, with newest updated contacts first. Use page/limit pagination, default 20, maximum 100. The response contains stable UID, group_id/group_uid, name, country_code, phone_number, full_phone_number, subscription flag, custom fields and timestamps. Empty custom fields are serialized as {}.

Campaigns can use {{name}} and {{cf:order_ref}} from these records. The subscription flag records your data but is not automatically applied by the current campaign dispatch loop. Maintain your intended audience and blacklist deliberately, and avoid creating duplicate contact rows through untracked repeated POST requests.

Business data

Catalogues, products and orders

The commerce API manages existing catalogues, their products and customer orders. Start by listing /api/v3/catalogues, then retain the local catalogue ID for resource operations and the distinct meta_catalogue_id for provider catalogue messages. Creating a shop and connecting it to Meta are account setup tasks outside this REST family.

Product identity and money

Value Meaning
Catalogue id Numeric local shop ID used in REST paths
Product id Numeric local product ID used in REST paths
retailer_id Your product/SKU identifier, unique within the catalogue
meta_catalogue_id / meta_product_id Provider identifiers
price / sale_price Nonnegative integer hundredths of the stated currency

For these product fields, 12500 represents 125.00; do not submit a formatted amount or assume the data-table currency field uses the same scale. currency is a three-character code. The account wallet response is another distinct representation, so name money variables with their units in your integration.

{"retailer_id":"BAG-001","name":"Canvas bag","description":"Reusable canvas shopping bag","price":1250000,"currency":"TZS","image_url":"https://assets.example.com/products/bag.jpg","availability":"in stock","condition":"new","inventory":24,"visibility":"published"}

POST to /api/v3/catalogues/{catalogue}/products. Retailer ID and name are required, maximum 100 characters; description maximum 5,000; image URL required, maximum 2,048. Price and currency are required. Inventory and sale_price are optional nonnegative integers. Brand/category maximum 255; product_type maximum 750.

Availability values are in stock, out of stock, preorder, available for order, discontinued. Condition is new, refurbished, used; create visibility is staging or published. Product lists search name/retailer_id and can filter availability, sorted by name, default 25 rows.

Updates, batches and synchronization

PUT /catalogues/{catalogue}/products/{product} applies recognized submitted fields. Its editable fields differ from creation: retailer_id, product_type and visibility are not accepted update inputs. A duplicate retailer_id on creation returns 422.

POST /catalogues/{catalogue}/products/batch takes products, 1–3,000 entries. Each requires retailer_id, name, price, currency and image_url. It upserts local products by retailer_id and initiates publication through configured shop channels. Response imported counts processed entries, and syncing indicates whether the shop has channels. This response does not establish successful publication of every product at every provider.

Single product operations may synchronize a linked Meta product when credentials are available. A provider refusal on create/update can produce 502. An unconnected catalogue keeps products locally. Local deletion and remote synchronization are not an atomic distributed transaction; reconcile important inventory changes by reading current state.

Sending products into WhatsApp

The three send endpoints are /catalogues/send-product, /catalogues/send-product-list and /catalogues/send-catalogue. They use to for recipient and optional from for sending identity, unlike regular message sends.

Single product requires provider catalogue_id and product_retailer_id; optional body maximum 1,024 and footer 60. Product list requires header_text≤60, body≤1,024 and 1–10 sections; each section needs title≤24 and product_items containing product_retailer_id. Whole catalogue requires body and can include thumbnail_product_retailer_id.

These operations call the provider directly and return data.message_id, its gateway ID. They do not create the ordinary REST message UID through the unified send path; do not look for that ID as a local message record. Missing WhatsApp configuration returns 422 and provider refusal returns 502.

Orders and fulfilment

List /catalogues/orders with optional status, read /catalogues/orders/{order}, and PUT {order}/status with a required status. Values are pending, confirmed, processing, shipped, delivered, cancelled and refunded. The current status method accepts any enum value and records the change; it does not enforce a linear transition graph or trigger a payment refund simply because you set refunded.

Orders expose product_items, customer details, total_amount/total_currency and originating provider message ID. Stored total_amount is scaled by 100; incoming cart item_price values are major units before total calculation. Keep fulfilment status distinct from payment settlement, and use order.received/order.paid callbacks only where configured. Poll order state to recover missed callbacks.

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. Automation subscription webhooks have their own signature, event 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 message, group and order events, then reconcile important state through the corresponding read endpoints.

Configure the receiver

Open Settings → Webhooks. Viewing needs webhooks.view; creating, editing or deleting needs webhooks.manage. Supply a public HTTP(S) URL and the events you want. Private/internal targets are rejected, the hostname is checked again at delivery, and redirects are not followed. Prefer an HTTPS endpoint that responds directly at its configured URL.

There is no customer /api/v3/webhooks management endpoint. The event picker and the dispatcher are also not identical: some emitted event names are not currently offered by the picker. Confirm the stored subscription with your administrator when using events beyond those shown in the interface.

Actual signature contract

The messaging/group/order dispatcher sends JSON with this header:

X-Signature: HEX_HMAC_SHA256
Content-Type: application/json

The signature is a lowercase hexadecimal HMAC-SHA256 of the serialized JSON payload, using the endpoint's signing secret. It has no sha256= prefix. This dispatcher does not set X-Webhook-Signature or X-Event. Use the event field in the body.

Verify against the raw body before parsing or changing whitespace:

$raw = file_get_contents('php://input');
$secret = getenv('MOMO_WEBHOOK_SECRET');
$signature = $_SERVER['HTTP_X_SIGNATURE'] ?? '';
if (!is_string($raw) || !is_string($secret) || $secret === '') {
    http_response_code(503);
    exit;
}
$expected = hash_hmac('sha256', $raw, $secret);
if (!hash_equals($expected, $signature)) {
    http_response_code(401);
    exit;
}
$event = json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
// Persist or enqueue the validated event, then acknowledge it.
http_response_code(200);

Provision the matching endpoint secret in your receiver environment. Current limitation: the platform generates this secret server-side, but the customer webhook screen and REST API do not currently expose secret retrieval or rotation. Do not assume you can copy it from the API-key screen. Arrange provisioning with the administrator responsible for the endpoint before enabling signature enforcement. An API key is not the webhook secret.

Message payloads are flat

{"event":"message.delivered","message_id":101,"direction":"outbound","sender":"MyBrand","recipient":"255712345678","status":"delivered","body":"Your order is ready.","media_url":null,"channel_type":"sms","timestamp":"2030-10-12T06:01:00+00:00"}

The message callback contains event, message_id, direction, sender, recipient, status, body, media_url, channel_type and timestamp. It does not wrap a full Message object in data; it does not include public uid, gateway ID or error_message. Use numeric message_id with the SMS/WhatsApp read endpoint for the complete current record.

Emitted message names include message.received, message.sent, message.delivered, message.read, message.failed, message.echoed and message.updated. A failed event may require a follow-up read to retrieve the error. Provider echoes and edits are not new inbound customer requests merely because they carry body text.

Orders and groups

order.received includes order_id, customer_wa_id, customer_name, product_items, total_amount, total_currency, customer_note, conversation_id and created_at. order.paid includes order_id, payment_id, method, amount_minor, currency, payer_msisdn, paid_at and conversation_id. Both also carry event and timestamp.

Group callbacks contain a group summary plus event-specific details. Events cover created, create_failed, updated, deleted, suspended, suspension_cleared, participant_joined/left/removed, join_requested, join_request_revoked and invite_sent. Use IDs in the payload to fetch current group state. Campaign completion/failure labels in the picker are not a delivery guarantee: the campaign execution path currently requires polling for those outcomes.

Delivery reliability

The current dispatcher makes one attempt with a 10-second timeout. It does not retry non-2xx responses, follow redirects or provide a durable customer replay log. Respond quickly after persisting the event, and keep costly downstream processing outside the request.

Do not assume exactly-once delivery or strict ordering. Make repeated status updates harmless, retain your own event audit, and poll important message/order/group IDs to recover from missing callbacks. Route incoming events by event family and ignore unknown fields so compatible additions do not break your receiver. This contract is for the customer messaging dispatcher; other voice/provider webhook integrations can have different headers and payloads.

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

Contacts

Operate within a known contact group, using its numeric ID or UUID. Read/search and list are POST routes. Contact lookup accepts UID or numeric ID.

PHONE or phone_number is required on both create and PATCH. Supply country_code explicitly when split normalization is needed. Updates recalculate the name and replace all custom fields; only omitted is_subscribed retains its previous value. The subscription flag is stored data and is not automatically applied by the current campaign dispatch loop.

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 282 tools as 282 near-identical POST operations would validate perfectly and teach nobody anything. So what is documented under this tag is the transport — the 28 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
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, 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 ticked
mcp:numbers Phone numbers ticked
mcp:meetings Meetings ticked
mcp:builders Call flows and chat flows ticked
mcp:data Data tables ticked
mcp:studio Voice and audio ticked
mcp:contacts Contacts ticked
mcp:agents AI agents ticked
mcp:commerce Orders and shop ticked
mcp:support Support tickets ticked
mcp:accounts Connected accounts ticked
mcp:approvals Approvals ticked
mcp:payments Payments ticked
mcp:automations Automations ticked
mcp:alerts Alerts and service levels ticked
mcp:operations Operations ticked
mcp:navigate Finding things ticked
mcp:messaging Messaging off
mcp:inbox Inbox off
mcp:comments Comments 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.
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.",
            "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.
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.",
            "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.
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."
        },
        "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).

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.
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."
        },
        "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.

AuthenticationTenant API token

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

API REFERENCE / Contacts

Find a contact

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

Finds a single contact in a group by public uid.

AuthenticationTenant API token

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

API REFERENCE / Contacts

Update a contact

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

Replacement-like contact update: phone remains required; send the name and all custom fields you intend to keep. Omitted is_subscribed retains its value; omitted custom fields are removed.

AuthenticationTenant API token

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

API REFERENCE / Contacts

Delete a contact

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

Deletes one contact by uid within a contact group.

AuthenticationTenant API token

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

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.

AuthenticationTenant API token

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

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

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

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.
retailer_idstringrequired
Your SKU. Unique per catalogue, and the id WhatsApp uses to refer to the product.
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.
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

Create one product. When the shop is connected to a Meta catalogue the product is created there first and the returned meta_product_id is stored; if Meta refuses, nothing is written locally and the call answers 502.

AuthenticationTenant API token

Path parameters

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

Example: 1

Request body

application/json · required

retailer_idstringrequired
Your SKU. Must be unique inside this catalogue; WhatsApp refers to the product by it.
maxLength
100
namestringrequired
Product name as customers see it.
maxLength
100
descriptionstringoptional
Long description.
maxLength
5000
priceintegerrequired
Price in the minor unit of `currency` — 25000 is TZS 250.00 for a 2-decimal currency.
minimum
0
currencystringrequired
ISO 4217 currency code.
minLength
3
maxLength
3
sale_priceintegeroptional
Optional sale price in the minor unit.
minimum
0
image_urlstringrequired
Publicly reachable image. Meta fetches it directly, so it cannot sit behind auth.
format
uri
maxLength
2048
urlstringoptional
Link to the product on your own site.
format
uri
maxLength
2048
availabilitystringoptional
Stock state. Defaults to "in stock".
enum
["in stock","out of stock","preorder","available for order","discontinued"]
conditionstringoptional
Condition. Defaults to "new".
enum
["new","refurbished","used"]
brandstringoptional
Brand name.
maxLength
255
categorystringoptional
Category label.
maxLength
255
product_typestringoptional
Your own taxonomy string.
maxLength
750
inventoryintegeroptional
Stock count.
minimum
0
visibilitystringoptional
Hide a product from customers with "staging".
enum
["staging","published"]
Complete request schema
{
    "type": "object",
    "required": [
        "retailer_id",
        "name",
        "price",
        "currency",
        "image_url"
    ],
    "properties": {
        "retailer_id": {
            "type": "string",
            "maxLength": 100,
            "description": "Your SKU. Must be unique inside this catalogue; WhatsApp refers to the product by it."
        },
        "name": {
            "type": "string",
            "maxLength": 100,
            "description": "Product name as customers see it."
        },
        "description": {
            "type": "string",
            "maxLength": 5000,
            "description": "Long description."
        },
        "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 currency code."
        },
        "sale_price": {
            "type": "integer",
            "minimum": 0,
            "description": "Optional sale price in the minor unit."
        },
        "image_url": {
            "type": "string",
            "format": "uri",
            "maxLength": 2048,
            "description": "Publicly reachable image. Meta fetches it directly, so it cannot sit behind auth."
        },
        "url": {
            "type": "string",
            "format": "uri",
            "maxLength": 2048,
            "description": "Link to the product on your own site."
        },
        "availability": {
            "type": "string",
            "enum": [
                "in stock",
                "out of stock",
                "preorder",
                "available for order",
                "discontinued"
            ],
            "description": "Stock state. Defaults to \"in stock\"."
        },
        "condition": {
            "type": "string",
            "enum": [
                "new",
                "refurbished",
                "used"
            ],
            "description": "Condition. Defaults to \"new\"."
        },
        "brand": {
            "type": "string",
            "maxLength": 255,
            "description": "Brand name."
        },
        "category": {
            "type": "string",
            "maxLength": 255,
            "description": "Category label."
        },
        "product_type": {
            "type": "string",
            "maxLength": 750,
            "description": "Your own taxonomy string."
        },
        "inventory": {
            "type": "integer",
            "minimum": 0,
            "description": "Stock count."
        },
        "visibility": {
            "type": "string",
            "enum": [
                "staging",
                "published"
            ],
            "description": "Hide a product from customers with \"staging\"."
        }
    }
}
The five required fields
{
    "retailer_id": "ACME-002",
    "name": "Kitenge 6 yards",
    "price": 45000,
    "currency": "TZS",
    "image_url": "https://cdn.acme.co.tz/kitenge.jpg"
}
A fully described product
{
    "retailer_id": "ACME-003",
    "name": "Kanga Pair \u2014 Blue",
    "description": "Cotton kanga, 2 pieces, wax print.",
    "price": 25000,
    "sale_price": 19000,
    "currency": "TZS",
    "image_url": "https://cdn.acme.co.tz/kanga-blue.jpg",
    "url": "https://acme.co.tz/shop/kanga-blue",
    "availability": "in stock",
    "condition": "new",
    "brand": "Acme",
    "category": "Fabrics",
    "inventory": 40,
    "visibility": "published"
}
Staged — created but hidden from customers
{
    "retailer_id": "ACME-004",
    "name": "Ramadan Bundle",
    "price": 90000,
    "currency": "TZS",
    "image_url": "https://cdn.acme.co.tz/bundle.jpg",
    "visibility": "staging"
}

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.
retailer_idstringrequired
Your SKU. Unique per catalogue, and the id WhatsApp uses to refer to the product.
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.
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

Import products in bulk

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

Create or update up to 3000 products in one call, matched on retailer_id — an existing SKU is updated, a new one is created. When the shop is connected to Meta the mirror runs in the background and data.syncing says so.

AuthenticationTenant API token

Path parameters

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

Example: 1

Request body

application/json · required

productsarray<object>required
The products to import.
minItems
1
maxItems
3000
Show child properties
retailer_idstringrequired
Your SKU. Must be unique inside this catalogue; WhatsApp refers to the product by it.
maxLength
100
namestringrequired
Product name as customers see it.
maxLength
100
priceintegerrequired
Price in the minor unit of `currency` — 25000 is TZS 250.00 for a 2-decimal currency.
minimum
0
currencystringrequired
ISO 4217 currency code.
minLength
3
maxLength
3
image_urlstringrequired
Publicly reachable image. Meta fetches it directly, so it cannot sit behind auth.
format
uri
maxLength
2048
descriptionstringoptional
Long description.
maxLength
5000
sale_priceintegeroptional
Optional sale price in the minor unit.
minimum
0
availabilitystringoptional
Stock state. Defaults to "in stock".
enum
["in stock","out of stock","preorder","available for order","discontinued"]
conditionstringoptional
Condition. Defaults to "new".
enum
["new","refurbished","used"]
brandstringoptional
Brand name.
maxLength
255
categorystringoptional
Category label.
maxLength
255
inventoryintegeroptional
Stock count.
minimum
0
Complete request schema
{
    "type": "object",
    "required": [
        "products"
    ],
    "properties": {
        "products": {
            "type": "array",
            "minItems": 1,
            "maxItems": 3000,
            "description": "The products to import.",
            "items": {
                "type": "object",
                "required": [
                    "retailer_id",
                    "name",
                    "price",
                    "currency",
                    "image_url"
                ],
                "properties": {
                    "retailer_id": {
                        "type": "string",
                        "maxLength": 100,
                        "description": "Your SKU. Must be unique inside this catalogue; WhatsApp refers to the product by it."
                    },
                    "name": {
                        "type": "string",
                        "maxLength": 100,
                        "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 currency code."
                    },
                    "image_url": {
                        "type": "string",
                        "format": "uri",
                        "maxLength": 2048,
                        "description": "Publicly reachable image. Meta fetches it directly, so it cannot sit behind auth."
                    },
                    "description": {
                        "type": "string",
                        "maxLength": 5000,
                        "description": "Long description."
                    },
                    "sale_price": {
                        "type": "integer",
                        "minimum": 0,
                        "description": "Optional sale price in the minor unit."
                    },
                    "availability": {
                        "type": "string",
                        "enum": [
                            "in stock",
                            "out of stock",
                            "preorder",
                            "available for order",
                            "discontinued"
                        ],
                        "description": "Stock state. Defaults to \"in stock\"."
                    },
                    "condition": {
                        "type": "string",
                        "enum": [
                            "new",
                            "refurbished",
                            "used"
                        ],
                        "description": "Condition. Defaults to \"new\"."
                    },
                    "brand": {
                        "type": "string",
                        "maxLength": 255,
                        "description": "Brand name."
                    },
                    "category": {
                        "type": "string",
                        "maxLength": 255,
                        "description": "Category label."
                    },
                    "inventory": {
                        "type": "integer",
                        "minimum": 0,
                        "description": "Stock count."
                    }
                }
            }
        }
    }
}

Responses

200How many rows were written, and whether a Meta sync was started.
statusstringrequired
Always "success".
enum
["success"]
dataobjectrequired
What the import wrote.
Show child properties
importedintegerrequired
Rows created or updated.
syncingbooleanrequired
True when a background sync to Meta was queued.
{
    "status": "success",
    "data": {
        "imported": 1,
        "syncing": 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 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.
retailer_idstringrequired
Your SKU. Unique per catalogue, and the id WhatsApp uses to refer to the product.
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.
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}

Partial update — send only what changes. retailer_id is immutable; create a new product instead. A product already mirrored to Meta is updated there first, and a Meta refusal answers 502 with nothing written locally.

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

namestringoptional
Product name as customers see it.
maxLength
100
descriptionstringoptional
Long description.
maxLength
5000
priceintegeroptional
Price in the minor unit of `currency` — 25000 is TZS 250.00 for a 2-decimal currency.
minimum
0
currencystringoptional
ISO 4217 currency code.
minLength
3
maxLength
3
sale_priceintegeroptional
Optional sale price in the minor unit.
minimum
0
image_urlstringoptional
Publicly reachable image. Meta fetches it directly, so it cannot sit behind auth.
format
uri
maxLength
2048
urlstringoptional
Link to the product on your own site.
format
uri
maxLength
2048
availabilitystringoptional
Stock state. Defaults to "in stock".
enum
["in stock","out of stock","preorder","available for order","discontinued"]
conditionstringoptional
Condition. Defaults to "new".
enum
["new","refurbished","used"]
brandstringoptional
Brand name.
maxLength
255
categorystringoptional
Category label.
maxLength
255
inventoryintegeroptional
Stock count.
minimum
0
Complete request schema
{
    "type": "object",
    "description": "Any subset of the writable fields.",
    "properties": {
        "name": {
            "type": "string",
            "maxLength": 100,
            "description": "Product name as customers see it."
        },
        "description": {
            "type": "string",
            "maxLength": 5000,
            "description": "Long description."
        },
        "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 currency code."
        },
        "sale_price": {
            "type": "integer",
            "minimum": 0,
            "description": "Optional sale price in the minor unit."
        },
        "image_url": {
            "type": "string",
            "format": "uri",
            "maxLength": 2048,
            "description": "Publicly reachable image. Meta fetches it directly, so it cannot sit behind auth."
        },
        "url": {
            "type": "string",
            "format": "uri",
            "maxLength": 2048,
            "description": "Link to the product on your own site."
        },
        "availability": {
            "type": "string",
            "enum": [
                "in stock",
                "out of stock",
                "preorder",
                "available for order",
                "discontinued"
            ],
            "description": "Stock state. Defaults to \"in stock\"."
        },
        "condition": {
            "type": "string",
            "enum": [
                "new",
                "refurbished",
                "used"
            ],
            "description": "Condition. Defaults to \"new\"."
        },
        "brand": {
            "type": "string",
            "maxLength": 255,
            "description": "Brand name."
        },
        "category": {
            "type": "string",
            "maxLength": 255,
            "description": "Category label."
        },
        "inventory": {
            "type": "integer",
            "minimum": 0,
            "description": "Stock count."
        }
    }
}
Reprice
{
    "price": 27000
}
Mark out of stock
{
    "availability": "out of stock",
    "inventory": 0
}
Rename and re-photograph
{
    "name": "Kanga Print Deluxe",
    "image_url": "https://cdn.acme.co.tz/kanga-deluxe.jpg"
}

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.
retailer_idstringrequired
Your SKU. Unique per catalogue, and the id WhatsApp uses to refer to the product.
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.
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

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.
customer_wa_idstringrequired
The customer's WhatsApp id (their phone number in E.164 without +).
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.
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"]
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.
customer_wa_idstringrequired
The customer's WhatsApp id (their phone number in E.164 without +).
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.
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"]
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.
customer_wa_idstringrequired
The customer's WhatsApp id (their phone number in E.164 without +).
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.
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"]
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 to a customer

POST/api/v3/catalogues/send-product

Sends a single-product message: the product card with an Add to cart button. catalogue_id is the Meta catalogue id of a connected shop.

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.
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.
Complete request schema
{
    "type": "object",
    "required": [
        "to",
        "catalogue_id",
        "product_retailer_id"
    ],
    "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."
        },
        "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."
        }
    }
}

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

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

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 / 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","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 and social — read, assign, reply.
comments Comments Comments on your Facebook, Instagram and TikTok posts.
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","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: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: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

Message and order events we POST to your server

POST(your webhook URL)

Best-effort JSON POST to your configured public receiver. Verify X-Signature (plain hexadecimal HMAC-SHA256 of serialized body with endpoint secret). One attempt with 10-second timeout, no redirect following and no automatic retry/replay. Message payloads use local numeric message_id; order/group payloads have distinct fields. Secret retrieval/rotation is not currently exposed by customer UI/API.

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 serialized JSON body using the endpoint signing secret; no sha256= prefix.
pattern
^[a-f0-9]{64}$

Example: 0000000000000000000000000000000000000000000000000000000000000000

Request body

application/json · required

The event payload.

eventstringrequired
Event name; payload fields depend on this family.
enum
["message.received","message.sent","message.delivered","message.read","message.failed","message.echoed","message.updated","order.received","order.paid","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"]
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.
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.
Complete request schema
{
    "type": "object",
    "properties": {
        "event": {
            "type": "string",
            "description": "Event name; payload fields depend on this family.",
            "enum": [
                "message.received",
                "message.sent",
                "message.delivered",
                "message.read",
                "message.failed",
                "message.echoed",
                "message.updated",
                "order.received",
                "order.paid",
                "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"
            ]
        },
        "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": []
        }
    },
    "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 flat delivery callback
{
    "event": "message.delivered",
    "message_id": 101,
    "direction": "outbound",
    "sender": "MyBrand",
    "recipient": "255712345678",
    "status": "delivered",
    "body": "Your order is ready.",
    "media_url": null,
    "channel_type": "sms",
    "timestamp": "2030-10-12T06:01:00+00:00"
}
A customer submitted a cart
{
    "event": "order.received",
    "timestamp": "2030-10-12T06:01:00+00:00",
    "order_id": 201,
    "customer_wa_id": "255712345678",
    "customer_name": "Asha",
    "product_items": [
        {
            "product_retailer_id": "BAG-001",
            "quantity": 2,
            "item_price": 12500,
            "currency": "TZS"
        }
    ],
    "total_amount": 2500000,
    "total_currency": "TZS",
    "customer_note": null,
    "conversation_id": 31,
    "created_at": "2030-10-12T06:01:00+00:00"
}
A payment settled
{
    "event": "order.paid",
    "timestamp": "2030-10-12T06:05:00+00:00",
    "order_id": 201,
    "payment_id": 302,
    "method": "mobile_money",
    "amount_minor": 2500000,
    "currency": "TZS",
    "payer_msisdn": "255712345678",
    "paid_at": "2030-10-12T06:05:00+00:00",
    "conversation_id": 31
}

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

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 265 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 28 · 265 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 ffd5c552 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 vab01f7b8 Read one call flow in full: every node, the entry point, the current version number, and what the IVR engine validator says about it right now. Read this before proposing edits, and pass the version back to apply_ivr_ops.

    Required permissions: ivr.view

    flow_idinteger · required
    Argument schema and validation
    flow_idintegerrequired
    The flow id, from list_ivr_flows.
  • 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 10 read · 9 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_message_flow v30472a8c Read one message flow in full: nodes, edges, triggers, its version number, and what the validator currently says. Pass the version back to apply_flow_ops so you do not overwrite somebody else.

    Required permissions: flows.view

    flow_idinteger · required
    Argument schema and validation
    flow_idintegerrequired
    The flow id, from list_message_flows.
  • 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_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 vaf81e26e 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
    Argument schema and validation
    session_idintegerrequired
    The session to replay.
    flow_idintegeroptional
    Optional: refuse if the session is not on this flow.
  • 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 v3fbf446f 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
    Argument schema and validation
    flow_idintegerrequired
    The flow to simulate.
    inbound_jsonstringoptional
    A JSON array of the customer's messages in order, e.g. ["hi","1","Amina"].
  • 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_message_flow v7902aac8 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
    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.
  • 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.
  • 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 ve598e436 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 acknowledge_overlapboolean
    Argument schema and validation
    flow_idintegerrequired
    The flow to publish.
    acknowledge_overlapbooleanoptional
    Set true only after telling the user another live flow answers the same words and they confirmed.
  • rollback_message_flow v80775f5c 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 confirmboolean
    Argument schema and validation
    flow_idintegerrequired
    The flow to roll back.
    version_idintegerrequired
    The version id to restore, from list_flow_versions.
    confirmbooleanoptional
    Set true only after the user has agreed that this version goes live now and the draft is replaced.
  • set_flow_triggers vb85fac64 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 triggers_jsonstring · required
    Argument schema and validation
    flow_idintegerrequired
    The flow to set triggers on.
    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_message_flow v37b18937 writes Rename a message flow, change its description, or change its status and priority. Priority decides which flow wins when two match the same message — lower runs first. Pausing an active flow stops it answering customers.

    Required permissions: flows.edit

    flow_idinteger · required namestring descriptionstring statusstring priorityinteger 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.
    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 v033c866b 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 set afterwards from the Fields tab by the owner (they build in the background).

    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 v8a9fdb36 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 (drop it first from the Fields tab); 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 v8a6ca63c 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. (call is declared but not built.)
    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 · 1 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.
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 8 read · 5 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).
  • simulate_agent v3ada3197 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.
  • 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.
  • 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 5 read · 1 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.
  • link-order-to-chat-tool vb2abde88 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.
  • 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
  • update-order-status-tool v3f904c65 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
  • request-order-payment-tool v02ded816 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 payer_msisdnstring
    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
    payer_msisdnstringoptional
    Phone number to bill, if it differs from the one on the order.
Shop /mcp/v1/shop 6 read · 1 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
  • update-order-status-tool v3f904c65 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
  • request-order-payment-tool v02ded816 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 payer_msisdnstring
    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
    payer_msisdnstringoptional
    Phone number to bill, if it differs from the one on the order.
Tickets /mcp/v1/tickets 15 read · 2 write

Support tickets: create, update, assign, reply, labels and notifications.

  • add-ticket-reply-tool v10555643 Add a reply or internal note to a ticket. Replies can be sent externally via SMS or WhatsApp if a channel is specified. Use type "note" for internal notes visible only to agents. 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
    Channel to send the reply through. If set to sms/whatsapp/email, the reply will be sent externally.
    enum
    ["internal","sms","whatsapp","email"]
  • assign-ticket-tool v8f384043 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 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 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 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.
  • 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
  • mark-ticket-notifications-read-tool v3384c0d1 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 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 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 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).
  • 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.
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.participants.manage

    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

    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 · 3 write

Customer conversations across WhatsApp, SMS and social — read, assign, reply.

  • 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.
  • 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.
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 5 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.
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 v8705c421 Read one record in full, using an id returned by search (e.g. "ivr:12", "flow:4", "asset:41", "ticket:9"). idstring · required
    Argument schema and validation
    idstringrequired
    An id from search, e.g. "ivr:12".
  • 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 vab01f7b8 Read one call flow in full: every node, the entry point, the current version number, and what the IVR engine validator says about it right now. Read this before proposing edits, and pass the version back to apply_ivr_ops.

    Required permissions: ivr.view

    flow_idinteger · required
    Argument schema and validation
    flow_idintegerrequired
    The flow id, from list_ivr_flows.
  • get_message_flow v30472a8c Read one message flow in full: nodes, edges, triggers, its version number, and what the validator currently says. Pass the version back to apply_flow_ops so you do not overwrite somebody else.

    Required permissions: flows.view

    flow_idinteger · required
    Argument schema and validation
    flow_idintegerrequired
    The flow id, from list_message_flows.
  • 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
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 and social.
  • CommentsRead comments on your Facebook, Instagram and TikTok posts.
  • 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 or WhatsApp 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. 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.