Response shape
{ "message_uuid": "...", "wamid": "..." }
{
"code": "WINDOW_CLOSED",
"message": "The 24-hour messaging window is closed. Send an approved template to reopen it."
}
{ code, message } — both fields are guaranteed.
Switch on
code: it is stable and part of the contract. The message is
helpful human text and may be refined between API versions without notice.
Messages are delivered in English (industry standard, same pattern as
Stripe, GitHub or Linear). If you need to display them translated to the
end user, map them in your client by code.Status families
| HTTP | Meaning | Retry? |
|---|---|---|
2xx | Success — 200 read, 201 created, 202 accepted (async), 207 multi-status. | — |
4xx | Request error — auth, validation, or missing resource. | No, fix it first. |
429 | Rate limit exceeded. | Yes, after the Retry-After. |
5xx | Server error. | Yes, with backoff. |
Code catalog
The HTTP status of each code is typical: a specific endpoint may return a
different status if context warrants it. The authoritative per-endpoint status
lives in its OpenAPI specification. The table here is a general reference, not a
binding per-endpoint contract.The conversation-resolution ambiguity codes (
PHONE_NUMBER_AMBIGUOUS,
USERNAME_AMBIGUOUS) can surface on any
endpoint that accepts a conversation reference by
phone/username/whatsapp_user_id, even if its specification lists other
409 codes.Auth and tenant
| Code | HTTP | Description |
|---|---|---|
INVALID_API_TOKEN | 401 | The token does not exist or has been revoked. Generate a new one in Settings → API Token. |
TOKEN_BUSINESS_MISMATCH | 403 | The token does not belong to the business slug in the URL. Verify they refer to the same business. |
Rate limit
| Code | HTTP | Description |
|---|---|---|
RATE_LIMIT_EXCEEDED | 429 | You exceeded the 60 req/min bucket. Wait the seconds from the Retry-After header. See Rate limits. |
Validation
| Code | HTTP | Description |
|---|---|---|
INVALID_REQUEST | 400 | The body failed schema validation (missing field, wrong type, value out of range). |
INVALID_CURSOR | 400 | The pagination cursor is invalid or expired. Restart the listing without a cursor. |
BATCH_LIMIT_EXCEEDED | 400 | The action group exceeds the maximum allowed in a single request. The message carries the actual cap. |
Conversation, contact, and channel
| Code | HTTP | Description |
|---|---|---|
CONVERSATION_NOT_FOUND | 404 | The conversation identifier (uuid, whatsapp_user_id, username, or phone) does not resolve to any conversation in the business. |
CONTACT_NOT_FOUND | 404 | The contact identifier does not resolve to any contact in the business. |
PHONE_NUMBER_AMBIGUOUS | 409 | Two distinct contacts share that phone number in the given channel (recycled numbers / separate identities). Identify the conversation by whatsapp_user_id or uuid. The same number on different channels is not ambiguous: channel resolves it. |
USERNAME_AMBIGUOUS | 409 | Two distinct contacts share that username in the given channel. Identify the conversation by whatsapp_user_id or uuid. The same username on different channels is not ambiguous: channel resolves it. |
CHANNEL_NOT_FOUND | 404 | The channel (channel public key) sent in the reference does not resolve to any channel in the business. It is required in every request; copy it under Settings → Channels or list your channels with GET /channels. |
CHANNEL_NOT_CONFIGURED | 422 | The conversation has no WhatsApp channel configured. Connect one in the dashboard before sending. |
TESTER_CONVERSATION_NOT_WRITABLE | 422 | Tester conversations don’t accept real media: send_message with a file (file_uuid) is rejected. Text, quick replies and templates do work (simulated). |
TRANSCRIPTION_REQUIRED_TO_RESOLVE | 422 | The mark_resolved action cannot close the conversation: it has an AI employee assigned and inbound media awaiting transcription. The AI builds its prompt from the transcribed text, so without it the AI would be blind to what the customer sent. Complete the manual transcription from the dashboard and retry. Human-attended conversations do not trigger this error. |
TRANSCRIPTION_REQUIRED | 422 | The send_message action carries media (file_uuid) the vision pipeline cannot transcribe (unsupported type or larger than 20 MB) and does not include the transcription field. Provide the file transcription in the action and retry. This is a per-action error: evaluated during background execution. |
Tags
| Code | HTTP | Description |
|---|---|---|
TAG_NOT_FOUND | 404 | The tag does not exist for this business. |
TAG_ALREADY_ASSIGNED | 409 | The tag is already assigned to this conversation. |
TAG_NOT_ASSIGNED | 404 | The tag is not assigned, so it cannot be removed. |
TAG_AMBIGUOUS | 409 | Multiple tags match that name. Use tag_uuid to disambiguate. |
TAG_BUSINESS_MISMATCH | 400 | The tag belongs to a different business. |
CANNOT_ASSIGN_DEFAULT_TAG | 422 | The tag is system-managed (handled by the backend, e.g. the pending-conversion tag) and cannot be assigned via the API. |
CANNOT_REMOVE_DEFAULT_TAG | 422 | The tag is system-managed (handled by the backend, e.g. the pending-conversion tag) and cannot be removed via the API. |
Mailboxes and categories
| Code | HTTP | Description |
|---|---|---|
CATEGORY_NOT_FOUND | 404 | The mailbox category does not exist for this business. |
CATEGORY_BUSINESS_MISMATCH | 403 | The category belongs to a different business. |
AMBIGUOUS_CATEGORY | 409 | Multiple categories match that name. Use category: { uuid } to disambiguate. |
MAILBOX_NOT_FOUND | 404 | The mailbox does not exist for this business. |
MAILBOX_ALREADY_ASSIGNED | 409 | The conversation is already in that mailbox. |
AMBIGUOUS_MAILBOX | 409 | Multiple mailboxes match that name. Use mailbox: { uuid } to disambiguate. |
NO_MAILBOXES_IN_CATEGORY | 422 | The category has no mailboxes configured. Add at least one in the dashboard. |
Precondition guards
| Code | HTTP | Description |
|---|---|---|
WINDOW_CLOSED | 422 | The 24h window is closed. Send an approved template to reopen it. |
WALLET_BLOCKED | 402 | The token wallet for the required AI model is empty or blocked. Top up to continue. |
VISION_WALLET_BLOCKED | 402 | The vision token wallet is empty. Vision processing requires top-up. |
VISION_BLOCKED | 402 | The thread is blocked by vision (vision tokens exhausted or a failed token top-up), or its inbound media ended up without a transcription after vision finished. Top up, or add the media transcription in the dashboard, before retrying. |
NO_AI_EMPLOYEE_ASSIGNED | 422 | No AI employee is assigned to this conversation. Assign one before running the action. |
Schedules (scheduled messages)
| Code | HTTP | Description |
|---|---|---|
OWNER_MISMATCH | 422 | The scheduled message’s executor does not match the conversation’s current owner. Reassign or cancel the schedule first. |
SCHEDULE_CONFLICT | 409 | The schedule could not be saved due to a concurrent change or a duplicate request on the conversation: another request is creating, replacing, or cancelling a schedule at the same time, or this request reuses a scheduled_uuid that already exists. If the conversation had a previous schedule, this same request already cancelled it, so a GET reflects the real current state (usually no pending schedule). To retry, omit scheduled_uuid (the server generates one) or use a new one: reusing the scheduled_uuid that caused the conflict returns a permanent 409. |
AI employees and execution
| Code | HTTP | Description |
|---|---|---|
AI_EMPLOYEE_NOT_FOUND | 404 | The AI employee does not exist for this business. |
AMBIGUOUS_AI_EMPLOYEE | 409 | Multiple employees match that name. Use ai_employee: { uuid } to disambiguate. |
AI_EMPLOYEE_ALREADY_ASSIGNED | 409 | That AI employee is already assigned to the conversation. |
INVALID_ASSIGNEE_FOR_RUN_AI | 409 | The conversation is human-owned and no ai_employee was specified. Pass one or reassign an AI first. |
AI_EXECUTION_FAILED | 500 | The AI execution failed unexpectedly. Retry; if it persists, check the employee’s dashboard. |
ACTIONS_DEPTH_EXCEEDED | 422 | Chained AI actions exceed the maximum depth allowed in a single request. |
Scheduled message action
| Code | HTTP | Description |
|---|---|---|
SCHEDULED_MESSAGE_ACTION_REQUIRED | 400 | There is an active scheduled message on the conversation. Specify scheduled_message_action (reassign or cancel). |
SCHEDULED_MESSAGE_ACTION_INVALID | 400 | The scheduled_message_action value is invalid. Must be reassign or cancel. |
Uploads
| Code | HTTP | Description |
|---|---|---|
INVALID_FILE_TYPE | 400 | The file type is not allowed. See the documentation for the list of accepted MIME types. |
FILE_TOO_LARGE | 400 | The file exceeds the maximum size for its category (image 5 MB, video/audio 16 MB, document 100 MB; text/plain has its own 5 MB cap). |
STORAGE_QUOTA_EXCEEDED | 413 | The business is over its storage quota, or its storage is disabled (quota_bytes = 0). Free up space or upgrade the plan; if storage is administratively disabled, contact support. |
STORAGE_QUOTA_CLEANUP_INSUFFICIENT | 507 | Storage is full and automatic cleanup could not free enough space for this upload. Free up space or upgrade the plan. |
FILE_NOT_FOUND | 404 | The file_uuid does not resolve for the requested operation: it does not exist, does not belong to this business, or is not in the state that endpoint requires (on /files/confirm the file must still be pending — one already confirmed or rejected also returns 404; on receipt downloads it must be a confirmed receipt of the requested paymentUuid). Per-endpoint detail lives in its OpenAPI specification. |
FILE_NOT_UPLOADED | 404 | The binary never reached Storage, or an empty/partial blob occupies the path. Request a new /request-upload and PUT again — retrying the same upload_url fails with Duplicate if the path is already occupied. |
MIME_MISMATCH | 400 | The actual MIME type does not match the declared one. Re-upload with the correct Content-Type. |
UPLOAD_FAILED | 500 | The upload could not be completed due to a storage error. Retry. |
TOO_MANY_PENDING_UPLOADS | 429 | The business has too many unconfirmed uploads. Confirm or let your pending uploads expire before requesting more. Retry after the seconds in the Retry-After header. |
Messages
| Code | HTTP | Description |
|---|---|---|
PREPARE_FAILED | 500 | The message could not be prepared (DB or upstream error). Retry. |
SEND_FAILED | 502 | Sending to WhatsApp failed. Check the channel’s health and retry. |
BSUID_SEND_DISABLED | 409 | The contact has no phone number, and sending via BSUID (business-scoped user ID) is not enabled on the platform. This is not a per-account setting, so there is nothing to enable on your side. |
PAYMENT_MARKER_UNKNOWN | 422 | The text contains a payment marker that is not in the catalog (usually a typo). The whole message is blocked: fix the marker name and send again. |
PAYMENT_MARKER_DATA_UNAVAILABLE | 422 | The conversation has no value for the requested marker — for example, a link-only payment profile has no CLABE. Repair the conversation’s payment details before sending again. |
PAYMENT_MARKER_UNSUPPORTED_FIELD | 422 | The marker sits in a field that does not accept substitution: the header, footer or button title of an interactive message, or the text header / button parameters of a template. In those fields the length is validated against the text you send — measuring the marker, not the value — so substituting could overflow Meta’s limit on delivery. Markers only work in the message body, or in a template’s body variables. |
PAYMENT_MARKER_NOT_ALLOWED_IN_TEMPLATE | 400 | Reserved. Template body variables do substitute payment markers as of v0.3.0; what rejects them is the header and the buttons, and that returns PAYMENT_MARKER_UNSUPPORTED_FIELD. No endpoint emits this code today. |
TEMPLATE_BODY_TOO_LONG | 422 | The template body, once the variables are interpolated, exceeds Meta’s 1024-character limit. Any long variable can overflow it; the typical case is a marker replaced by a value longer than itself (a CLABE is 18 characters; ==clabe_spei== is 14). Shorten the template text or the variable value. |
PAYMENT_PROFILE_GENERATION_FAILED | 500 | This conversation had no payment details and generating them failed. Retry in a few moments. |
Quick replies and templates
| Code | HTTP | Description |
|---|---|---|
QUICK_REPLY_NOT_FOUND | 404 | The quick reply does not exist for this business. |
QUICK_REPLY_AMBIGUOUS | 409 | Multiple quick replies match that name. Use quick_reply: { uuid } to disambiguate. |
QUICK_REPLY_EMPTY | 422 | The quick reply has no actions configured. Add at least one in the dashboard. |
TEMPLATE_NOT_FOUND | 404 | The template does not exist for this business. |
TEMPLATE_NOT_APPROVED | 422 | The template is not approved by Meta. Approval is required before sending. |
AUTH_TEMPLATE_REQUIRES_PHONE | 409 | An authentication template cannot be sent to a contact without a phone number: Meta requires it. Send to a contact with a phone number. |
TEMPLATE_AMBIGUOUS | 409 | The same name and language exist on 2 or more connected WhatsApp channels. The request cannot select which channel the template belongs to, so the ambiguity cannot be resolved from the integration. Contact support to resolve the collision. |
TEMPLATE_VARIABLES_INVALID | 422 | The body variables do not match the template (wrong count, missing index, or empty value). |
TEMPLATE_HEADER_MISMATCH | 400 | The header type does not match what the template declares (e.g. template expects an image but you sent text). |
TEMPLATE_HEADER_TRANSCRIPTION_MISSING | 422 | The template media header (in send_message with template, or in the deprecated send_quick_reply_or_template) cannot be transcribed by the vision pipeline (unsupported type or larger than 20 MB) and the header.transcription field is missing. Provide the header transcription and retry. This is a per-action error: evaluated during background execution. |
TEMPLATE_FAKE_REQUIRES_TESTER_CONVERSATION | 422 | This template is a fake (test-only) template and can only be sent to tester conversations. |
Payments
| Code | HTTP | Description |
|---|---|---|
PAYMENT_NOT_FOUND | 404 | No payment matches that paymentUuid for this business. |
Generic operations
| Code | HTTP | Description |
|---|---|---|
FETCH_FAILED | 500 | Could not load the data required to complete this request. Retry. |
UPDATE_FAILED | 500 | The change could not be applied (DB error). Retry. |
UNKNOWN_ERROR | 500 | Unexpected error. Contact support if it persists. |