WhatsApp

Start WhatsApp conversations with a template, send messages, read conversations and manage templates.

The WhatsApp API works on your workspace’s own WhatsApp Business account (connect it in the console → WhatsApp → Settings). It needs the whatsapp:read / whatsapp:write scopes. Starting a conversation with an agent also needs WhatsApp agents to be enabled for the workspace.

Every send goes through the same checks as the console: a contact who opted out of WhatsApp (or is on your do-not-call list, or was erased) is never messaged, a template that is not approved is never sent, and a marketing template needs a recorded WhatsApp opt-in for that person (pass optIn when you have one). An opt-in you record never overrides a customer’s own STOP.

A business-initiated message outside the customer’s 24-hour window must be an approved template. Text and media are only accepted inside the window.

Test keys (ohk_test_…) run every check and return a simulated result with livemode: false; nothing is sent to WhatsApp and nothing is stored or changed. That covers starting a conversation and sending a message (a wamid.TEST_… id), ending a conversation (it stays open), and creating, editing, deleting, syncing templates and uploading header samples (the template that would result, a TEST_… handle; your templates at Meta are untouched). The same errors as a live key come back.

The message object (also the data of every message.* webhook):

{
  "messageId": "wamsg_…",
  "wamid": "wamid.HBgM…",
  "chatId": "chatsess_…",
  "threadId": "wathr_…",
  "agentId": "agt_…",
  "contactId": "con_…",
  "externalRef": null,
  "direction": "outbound",
  "source": "api",
  "type": "template",
  "text": "Hi Asha, we missed you.",
  "template": { "id": "watpl_…", "name": "missed_call_follow_up", "language": "en", "category": "UTILITY" },
  "media": null,
  "from": "+918050000000",
  "to": "+919811111111",
  "customerUserId": null,
  "customerUsername": null,
  "phoneNumberId": "1234567890",
  "status": "delivered",
  "timestamp": "2026-10-07T06:00:03Z",
  "statuses": { "sentAt": "2026-10-07T06:00:01Z", "deliveredAt": "2026-10-07T06:00:03Z", "readAt": null, "failedAt": null }
}

customerUserId and customerUsername are the customer’s WhatsApp user id (such as IN.13491208655302741918) and @username, or null. For a customer who hides their number, the customer side of the message (from on a received message, to on a sent one) is null until WhatsApp shares the number.

A failed message also carries error: { code, title, detail }: Meta’s error code, or one of ours for a message refused before it reached Meta (see Local error codes). Inbound media carries media: { type, mimeType, messageId, available }; download it with Download media.

status is one of:

status Meaning
queued Waiting to be sent (paced by a campaign or the number’s throughput).
sent WhatsApp accepted it.
delivered It reached the customer’s phone.
read The customer opened it (when they share read receipts).
failed Not delivered; see error.
delivery_unknown WhatsApp did not confirm the send in time. It may still arrive and is never sent again.
received An inbound message from the customer.
deleted The customer deleted it, or it was erased (DPDP).

Local error codes

error.code error.title Meaning
-1001 contact_opted_out The person opted out of WhatsApp, is on your do-not-call list or was erased.
-1003 text_too_long Over WhatsApp’s 4,096 characters.
-1004 window_closed Free text / media outside the 24-hour window; send a template.
-1005 invalid_message Not a valid WhatsApp message (too many buttons, an empty list…).
-1006 opt_in_required A marketing template and no recorded opt-in.
-1007 template_unavailable The template is not approved.
-1008 template_not_found No template with that name and language on your account.

Meta’s own codes and what to do about each are listed in the WhatsApp troubleshooting guide.


Start a conversation

POST /v1/whatsapp/conversations  ·  scope whatsapp:write  ·  rate limited  ·  optional Idempotency-Key

Sends an approved template to a customer and opens a conversation with your WhatsApp agent: when the customer replies, the agent answers, with your variables available to its flow.

curl -X POST https://api.oyehello.ai/v1/whatsapp/conversations \
  -H "Authorization: Bearer ohk_live_…" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: lead-1042" \
  -d '{
    "agentId": "agt_…",
    "to": "+919811111111",
    "template": { "id": "watpl_…", "params": { "body": { "customer_name": "Asha" } } },
    "variables": { "lead_id": "L-1042" },
    "contact": { "name": "Asha" },
    "onActiveSession": "reject"
  }'
  • template - id, or name (+ language). params.body is an object for named templates ({{customer_name}}) and an array for numbered ones ({{1}}, {{2}}); params.header is { "type": "text", "text" } or { "type": "image" | "video" | "document", "url" } (https); params.buttons is [{ "index", "text" | "payload" | "code" }]. Omit params to fill the template from the agent’s template-tool mapping. For a template those fields cannot fill (carousel, catalog, multi-product or flow buttons), pass params.components instead: Meta’s own template components in its send format, used as they are. See Carousel and other raw templates.
  • from - optional; must be the number bound to the agent.
  • contact - optional { "name", "externalRef" }. The customer’s contact is found by number (or created). externalRef (your own id for the person) is stored the same way as POST /v1/contacts sets it: only on a contact that has no reference of its own yet. A contact that already has a different reference keeps it, and a reference another contact already holds is ignored; the conversation is started either way.
  • metadata - optional; your own key/value data (up to 20 keys of 1-40 characters; string values up to 500 characters, numbers, booleans or null; 4 KB in all). It is stored on the conversation and returned on it, on List conversations / Get a conversation and in its chat.* webhooks. The agent never reads it. Sending into an open conversation (send_in_session) keeps that conversation’s metadata.
  • optIn - { "source", "capturedAt" }: the customer’s WhatsApp opt-in you collected (required for marketing templates unless already recorded).
  • onActiveSession - reject (default: 409 active_session_exists with the open chatId) or send_in_session (the template goes into the open conversation, 200).
  • priorMessages - optional; up to 200 earlier messages with this customer, for a conversation your helpdesk or another bot hands to the agent: [{ "role": "customer" | "agent" | "bot", "text", "agentName"?, "sentAt"?, "wamid"? }] (text up to 4096 characters, 256 KB in all). The agent reads them as background, never as instructions: messages with the same wamid count once, control characters and leading “System:”-style labels are removed, and when they are long only the newest are kept. They are not sent to the customer.
  • handoverNote - optional; up to 1000 characters for the agent (“VIP customer, refund already approved”). Not sent to the customer.
  • priorMessages and handoverNote only seed a new conversation, so they cannot be combined with onActiveSession: "send_in_session" (400).

Response 201 (200 when sent into an open conversation):

{ "data": { "livemode": true, "chatId": "chatsess_…", "threadId": "wathr_…", "contactId": "con_…", "sentInSession": false, "metadata": { "crmId": "L-1042" },
  "message": { "id": "wamsg_…", "wamid": "wamid.…", "status": "sent" } } }

Errors (every error body is { error, code, meta? }):

Status code When
400 (none) The body is not valid (an unknown field, a bad value); error names the field.
400 invalid_idempotency_key The Idempotency-Key header is longer than 255 characters.
400 template_not_found No template with that id (or name + language) on your WhatsApp account.
402 insufficient_balance The wallet cannot pay for a conversation.
403 whatsapp_disabled WhatsApp is turned off for the workspace.
403 whatsapp_agents_off WhatsApp agents are not enabled for the workspace.
403 contact_opted_out The person opted out of WhatsApp, is on your do-not-contact list, or was erased (meta.reason).
403 opt_in_required A marketing template and no recorded WhatsApp opt-in (pass optIn).
404 agent_not_found No such agent in this workspace.
409 active_session_exists The customer already has an open conversation (meta.chatId); use onActiveSession: "send_in_session" to send into it.
409 agent_not_active The agent is not published.
409 agent_number_missing No WhatsApp number is bound to the agent.
409 agent_number_mismatch from is not the agent’s number (meta.phoneNumberId).
409 whatsapp_not_connected The workspace’s WhatsApp connection is missing.
409 idempotency_in_progress A request with the same Idempotency-Key is still running, or it ended in an unexpected 500. After a 500 the key stays held for 24 hours, because the message may have gone out: check GET /v1/whatsapp/messages (filter by contactId or templateName) before you retry with a new key.
422 agent_not_whatsapp The agent is not a WhatsApp agent.
422 invalid_number to is neither an international number nor a WhatsApp user id.
422 invalid_opt_in optIn.capturedAt is in the future.
422 template_unavailable The template exists but is not approved (paused, rejected, disabled…).
422 template_params_invalid A value is missing, extra or too long (every issue in meta.issues: { field, code, message }).
422 idempotency_key_reused The Idempotency-Key was used with a different body.
429 tier_limit_reached Your messaging limit for the last 24 hours is used up (also the per-minute rate limit: rate_limited).
502 delivery_failed WhatsApp rejected the message (meta.error has Meta’s code; the conversation ends unbilled).
502 delivery_failed (recipient_unsupported) WhatsApp can’t deliver this message type to a user id (for example authentication templates).
503 provider_unavailable WhatsApp did not confirm in time (meta.chatId); it may still arrive and is never sent again.

With an Idempotency-Key, a retry returns the first response - including a 502 or 503 - and never sends the template twice.

End a conversation

POST /v1/whatsapp/conversations/{id}/end  ·  scope whatsapp:write

Ends an open conversation (endCause: completed); ending an ended one returns its final state.

Take over a conversation

POST /v1/whatsapp/conversations/{id}/takeover  ·  scope whatsapp:write

Your helpdesk (Zendesk, Freshdesk, your own inbox) takes the conversation from the AI agent: the agent stops replying until you hand it back. Your helpdesk replies with Send a message (free text inside the 24-hour window). The console inbox shows the conversation as handled by your helpdesk.

curl -X POST https://api.oyehello.ai/v1/whatsapp/conversations/chatsess_…/takeover \
  -H "Authorization: Bearer ohk_live_…" -H "Content-Type: application/json" \
  -d '{ "agentName": "Priya", "note": "Refund dispute" }'

Body (optional): agentName (up to 80 characters) and note (up to 1000), both echoed in the chat.taken_over webhook.

{ "data": { "id": "chatsess_…", "status": "taken_over", "alreadyTakenOver": false, "livemode": true } }

Safe to retry: taking over a conversation your helpdesk already holds returns alreadyTakenOver: true. A test key (ohk_test_) validates the request and answers without changing the conversation.

Hand a conversation back

POST /v1/whatsapp/conversations/{id}/handback  ·  scope whatsapp:write

Gives the conversation back to the AI agent. Pass what your helpdesk told the customer so the agent knows: the customer’s own messages are already in the conversation.

curl -X POST https://api.oyehello.ai/v1/whatsapp/conversations/chatsess_…/handback \
  -H "Authorization: Bearer ohk_live_…" -H "Content-Type: application/json" \
  -d '{ "messages": [{ "text": "Your refund is approved.", "agentName": "Priya" }], "note": "Refund approved, close politely" }'

Body (optional): messages (up to 200 of { "text", "agentName"?, "sentAt"? }, 256 KB in all) - added to the conversation history as your team’s replies; note (up to 1000 characters) - added for the agent only, never sent to the customer.

{ "data": { "id": "chatsess_…", "status": "with_agent", "alreadyWithAgent": false, "livemode": true } }

Safe to retry: a conversation already with the agent returns alreadyWithAgent: true.

Status code When (take over and hand back)
400 (none) The body is not valid; error names the field.
403 api_scope_missing The key lacks whatsapp:write.
404 not_found No WhatsApp conversation with that id in this workspace.
409 conversation_ended The conversation has ended. Start a new one.
409 held_by_teammate A teammate is handling it in the console inbox; they hand it back there first.

Assign a conversation

POST /v1/whatsapp/conversations/{id}/assign  ·  scope whatsapp:write

Gives the conversation to one of your teammates in the console inbox, as if they had taken it over: the AI agent stops replying and the teammate replies from the inbox. Use it to route conversations from your own system (by skill, language or customer value). Send exactly one of userId or email:

curl -X POST https://api.oyehello.ai/v1/whatsapp/conversations/chatsess_…/assign \
  -H "Authorization: Bearer ohk_live_…" -H "Content-Type: application/json" \
  -d '{ "email": "[email protected]" }'
{ "data": { "id": "chatsess_…", "assignedTo": "usr_…" } }

The teammate must be able to reply in the inbox (edit access to agents). GET /v1/whatsapp/assignees (scope whatsapp:read) lists them: { "data": [{ "userId", "name", "email" }] }. The API may move a conversation a teammate holds; it never takes one your helpdesk holds through Take over. Each assignment sends a chat.assigned webhook event.

Status code When
400 assignee_not_allowed That person is not a teammate who can reply in the inbox.
403 test_mode_read_only A test key: assigning changes a live conversation, so use a live key.
404 not_found No WhatsApp conversation with that id in this workspace (web and API chats are not assigned here).
409 conversation_ended The conversation has ended.
409 held_by_helpdesk Your helpdesk holds it; hand it back first.

List conversations

GET /v1/whatsapp/conversations  ·  scope whatsapp:read

Newest first, limit (1-100, default 50) with nextCursor. Filters: agentId, phoneNumberId, contactId, customer (E.164 number or WhatsApp user id), status (active / ended), endCause, origin (inbound, template_start, call_followup, campaign, ai_unavailable), startedAfter, startedBefore, includeTest (test chats are left out by default).

{ "data": [{ "id": "chatsess_…", "agentId": "agt_…", "contactId": "con_…", "externalRef": null,
  "customer": { "phone": "+919811111111", "name": "Asha", "userId": null, "username": null }, "business": { "phone": "+918050000000", "phoneNumberId": "1234567890" },
  "status": "ended", "endCause": "idle_timeout", "origin": "template_start", "messageCount": 4, "isTest": false,
  "startedAt": "2026-10-07T06:00:00Z", "endedAt": "2026-10-07T09:10:00Z", "outcome": { "disposition": "interested", "boardColumn": "Interested" } }],
  "nextCursor": null }

Get a conversation

GET /v1/whatsapp/conversations/{id}  ·  scope whatsapp:read

?include=messages,analysis adds the conversation’s messages (oldest first) and analysis: { summary, extracted, disposition }.

adReferral (list and detail) is the click-to-WhatsApp ad or post that started the conversation, or null: { sourceId, sourceType ("ad" or "post"), sourceUrl, headline, body, mediaType, imageUrl, videoUrl, thumbnailUrl, ctwaClid } as Meta reported it. Use sourceId to match Meta Ads Manager and ctwaClid for Meta’s Conversions API. It is also on every chat.* webhook payload.

Every conversation (list and get) also carries labels ([{ "id", "name", "color" }], the labels your team applied in the inbox) and assignedTo (the teammate’s user id, or null).

Send a message

POST /v1/whatsapp/messages  ·  scope whatsapp:write  ·  rate limited  ·  optional Idempotency-Key

One message without an agent conversation. Send exactly one of:

  • template (same shape as above), to anyone.
  • text (up to 4096 characters).
  • media: { "type": "image" | "video" | "document" | "audio", "url": "https://…", "caption", "filename" }. Audio (AAC, AMR, MP3, M4A or OGG/Opus, up to 16 MB) takes no caption.
  • interactive: reply buttons, a list or a link button (below).
  • location: { "latitude", "longitude", "name"?, "address"? }, a map pin.

to is the customer’s international number (+919811111111) or, for a customer who hides their number, their WhatsApp user id from a received message (customerUserId, for example IN.13491208655302741918). Templates and in-window messages both accept it. A response for a user id has to: null and customerUserId set; a test key answers in the same shape.

Everything except a template needs the customer’s 24-hour window on that number to be open (409 window_closed otherwise). A session message to a user id that has never written to that number is 409 window_closed.

interactive follows WhatsApp’s limits, checked before anything is sent (a 400 names the field):

type Fields Limits
buttons body, buttons: [{ "id", "title" }] 1-3 buttons, titles up to 20 characters and all different, ids up to 256 and all different, body up to 1024
list body, button, sections: [{ "title", "rows": [{ "id", "title", "description"? }] }] button text up to 20, 1-10 sections, at most 10 rows in all, row title up to 24, description up to 72, id up to 200 (row ids all different), section title up to 24, body up to 4096
cta_url body, displayText, url a link button: displayText up to 20, an https:// url, body up to 1024

Each can add a text header and a footer (up to 60 characters each). The customer’s tap on a reply button or a list row arrives as a message.received with its id.

{ "to": "+919811111111", "interactive": { "type": "cta_url", "body": "Your bill for October is ready.", "displayText": "Pay now", "url": "https://pay.example.com/inv/123" } }

from (optional) picks which of your live WhatsApp numbers sends it: the number’s id, its phoneNumberId, or its E.164 number (spaces allowed), as listed by List numbers. Without from, your default number (isDefault: true) sends. A customer’s 24-hour window is per number: reply from the number they wrote to.

Response 201 with the message object (plus livemode). Errors as for Start a conversation (without the agent and conversation codes), plus 409 window_closed (text or media outside the 24-hour window) and 409 from_number_mismatch (from is not one of your live WhatsApp numbers). A test key gets the same errors; only the send itself is simulated.

Some templates can’t be filled with header, body and buttons: carousels, catalog and multi-product templates, and flow buttons. Your console marks them as read-only. Send them on POST /v1/whatsapp/messages and POST /v1/whatsapp/conversations with template.params.components. This is Meta’s template components array in its send format, and it replaces header / body / buttons (passing both is refused):

"template": { "id": "watpl_…", "params": { "components": [
  { "type": "body", "parameters": [{ "type": "text", "text": "Asha" }] },
  { "type": "carousel", "cards": [
    { "card_index": 0, "components": [
      { "type": "header", "parameters": [{ "type": "image", "image": { "link": "https://cdn.example.com/1.png" } }] },
      { "type": "button", "sub_type": "quick_reply", "index": "0", "parameters": [{ "type": "payload", "payload": "more-1" }] } ] },
    { "card_index": 1, "components": [ … ] } ] } ] } }

We check the shape before anything is sent:

  • Each component is a header (exactly one parameter), a body, a button (sub_type and index 0-9) or a carousel.
  • A carousel has 1-10 cards, numbered by card_index from 0. Each card has its own header, body and button components.
  • Every parameter has a known type.
  • There are at most 20 components and the array is at most 16 KB.

A bad shape answers 422 template_params_invalid, with the path of each problem in meta.issues (code invalid_component). Meta checks the values themselves against the template, so a wrong card count or media type comes back as a failed message. A carousel template sent without components is refused. A template that header / body / buttons can fill refuses components.

Campaigns and agent template tools fill templates from per-person values, so they don’t take raw components. Send a carousel to a list from your own code, one POST /v1/whatsapp/messages per person.

List messages

GET /v1/whatsapp/messages  ·  scope whatsapp:read

Newest first with nextCursor. Filters: chatId, threadId, contactId, direction, status, source, templateName, after, before.

Get a message

GET /v1/whatsapp/messages/{id}  ·  scope whatsapp:read

Download media

GET /v1/whatsapp/media/{messageId}  ·  scope whatsapp:read

Redirects (302) to a short-lived link for media a customer sent you. Outbound media is the link you sent; 404 for it, and 503 media_unavailable when the deployment stores no media.

List numbers

GET /v1/whatsapp/numbers  ·  scope whatsapp:read

Every live number of the workspace (several can be live at once, each answered by its own agent).

{ "data": [{ "id": "wanum_…", "phoneNumberId": "1234567890", "phone": "+918050000000", "verifiedName": "Acme", "status": "registered",
  "qualityRating": "GREEN", "tier": "TIER_2K", "agentId": "agt_…", "isDefault": true }] }

isDefault marks the number Send a message uses without from. tier is your business’s messaging limit (new customers per 24 hours), shared by all your numbers: TIER_50, TIER_250, TIER_2K, TIER_10K, TIER_100K or TIER_UNLIMITED.

List templates

GET /v1/whatsapp/templates  ·  scope whatsapp:read

Filters: status, category, language, quality, q (name contains), with limit and nextCursor. Each template carries its components, variables, status, qualityScore, any announced recategorisation (pendingCategory, categoryChangeAt), editable and Meta’s edit allowance.

Get a template

GET /v1/whatsapp/templates/{id}  ·  scope whatsapp:read

Create a template

POST /v1/whatsapp/templates  ·  scope whatsapp:write

Body { "spec": { name, language, category, parameterFormat, header, body, footer, buttons }, "submit": true } - the same template builder as the console. submit: false saves a local draft. Returns 201 with the template (pending once submitted to Meta).

Errors: 409 name_taken (the name and language already exist - Meta also blocks a name for 30 days after an approved template with it was deleted), 409 not_connected (connect WhatsApp with your WABA ID first), 422 invalid_template (every problem in meta), 502 meta_rejected (Meta refused it; its details in meta).

Edit a template

PATCH /v1/whatsapp/templates/{id}  ·  scope whatsapp:write

Body { "spec": … }. Meta allows one edit of an approved template per 24 hours and ten per 30 days: past that, 429 edit_limit. When we refused it from the edits we recorded, meta.nextEditAt is the earliest time the next edit is allowed; when Meta itself refused it (an edit made elsewhere, e.g. in WhatsApp Manager), meta.nextEditAt is null and Meta’s code / subcode / detail are in meta.

Other errors: 400 immutable_field (the variable style of a submitted template cannot change), 404 not_found, 409 not_editable (only approved, rejected or paused templates can be edited), 422 category_locked (an approved template’s category cannot change), 422 invalid_template (every problem in meta), 502 meta_rejected (Meta refused the edit; its details in meta).

Delete a template

DELETE /v1/whatsapp/templates/{id}  ·  scope whatsapp:write

Deletes this language; ?allLanguages=true deletes every language of the name. A template used by an agent, a tool, a board action or a running campaign is refused with 409 template_in_use (the users in meta).

Sync templates

POST /v1/whatsapp/templates/sync  ·  scope whatsapp:write

Reads every template from Meta now (202 with { synced, changed, deleted }). The mirror also syncs on its own and on every template webhook.

Upload a header sample

POST /v1/whatsapp/templates/media  ·  scope whatsapp:write

A multipart upload (field file, JPEG / PNG / MP4 / PDF, up to 16 MB) for a media header’s example. Returns 201 { "data": { "handle", "mimeType", "size" } }; put the handle in the header of the template you create.

Webhook events

WhatsApp activity reaches your webhooks as:

Event When Delivery
message.received A customer message arrived. Only to endpoints that list it
message.sent WhatsApp accepted a message you sent. Only to endpoints that list it
message.delivered A message reached the phone. Only to endpoints that list it
message.read The customer read it. Only to endpoints that list it
message.failed A message failed or was refused (with error). Only to endpoints that list it
chat.started A conversation with your agent started. All-events endpoints too
chat.completed It ended (with the transcript and endCause). All-events endpoints too
chat.analyzed Its summary, extracted fields, sentiment and outcome are ready. All-events endpoints too
chat.evaluated It was graded against the agent’s eval criteria (again after a re-run). Only to endpoints that list it
chat.taken_over Your helpdesk took it over through the API. All-events endpoints too
chat.handed_back Your helpdesk handed it back. All-events endpoints too
chat.assigned It was assigned to a teammate (in the console, by the API, or by round-robin), with assignedTo, assignedBy and reason. All-events endpoints too
contact.opted_out A customer opted out (channel: "whatsapp"). All-events endpoints too
whatsapp.template.updated A template’s status, quality or category changed at Meta. All-events endpoints too

message.* events carry the message object above; chat.* events the conversation (see Webhooks).

Errors

Every error body is { error, code, meta? }. The codes the WhatsApp API returns:

Status code Endpoints When
400 (none) all The body or query is not valid; error names the field.
400 invalid_cursor lists The cursor is not one we returned.
400 invalid_idempotency_key start, send The Idempotency-Key is over 255 characters.
400 template_not_found start, send No such template (or name + language) on your account.
400 immutable_field edit template The variable style of a submitted template can’t change.
401 (none) all The key is missing, unknown, revoked or expired.
402 insufficient_balance start The wallet can’t pay for a conversation.
403 api_scope_missing all The key lacks whatsapp:read / whatsapp:write.
403 whatsapp_disabled / whatsapp_agents_off start, send WhatsApp, or WhatsApp agents, are off for the workspace.
403 contact_opted_out start, send The person opted out, is on your do-not-call list, or was erased.
403 opt_in_required start, send A marketing template and no recorded opt-in.
404 not_found / agent_not_found all No such object in this workspace.
409 active_session_exists start The customer already has an open conversation (meta.chatId).
409 agent_not_active / agent_number_missing / agent_number_mismatch start Publish the agent / bind a number / from is not its number.
409 whatsapp_not_connected / not_connected start, send, templates Connect WhatsApp first.
409 from_number_mismatch send from is not one of your live numbers.
409 window_closed send Text or media outside the 24-hour window.
409 conversation_ended / held_by_teammate takeover, handback It ended, or a teammate holds it in the console.
409 idempotency_in_progress start, send The same key is still running (or ended in a 500).
409 name_taken / not_editable / template_in_use templates See Create, Edit, Delete.
413 file_too_large header sample Over 16 MB.
422 agent_not_whatsapp / invalid_number / invalid_opt_in start The agent isn’t a WhatsApp agent / to is neither an international number nor a WhatsApp user id / optIn.capturedAt is in the future.
422 template_unavailable / template_params_invalid start, send The template isn’t approved / a value is missing, extra or too long (meta.issues).
422 idempotency_key_reused start, send The key was used with a different body.
422 invalid_template / category_locked templates Every problem in meta / an approved template’s category can’t change.
429 tier_limit_reached start, send Your WhatsApp messaging limit (new customers per 24 hours) is used up.
429 rate_limited start, send Over the API rate limit (120 requests per minute per workspace by default); retry after the time in error.
429 edit_limit edit template One edit per 24 hours and ten per 30 days for an approved template (meta.nextEditAt).
502 delivery_failed / meta_rejected start, send, templates WhatsApp refused it (Meta’s details in meta). recipient_unsupported (in meta.error.title) means that message type can’t go to a customer known only by a user id.
502 meta_unavailable templates WhatsApp did not return your templates; retry.
503 provider_unavailable start, send WhatsApp did not confirm in time; never sent twice.
503 media_unavailable media Stored media isn’t available on this deployment.

Exports (CSV of conversations) are in the console (WhatsApp → Inbox → History → Export CSV); the API reads conversations and messages page by page.

Bulk campaigns

To message a whole list with a template - paced within your messaging limit, inside a daily send window, with opt-in filtering, automatic pauses and per-person delivery, read and reply status - create a campaign with channel: "whatsapp": see Campaigns.