Skip to main content
We are the system of record for a merchant’s conversations. Your product reads them, replies into them, and decides when a person should take over.
These routes need messages:read and messages:write. They are deliberately not covered by assistants:write — a key issued to edit a prompt should not thereby be able to message a merchant’s customers.
A conversation exists whenever a customer writes, whether or not an assistant is going to answer. conversation.created fires for a number that is still awaiting activation, for a merchant with no live assistant, and for one whose allowlist excludes the sender.A thread exists because somebody wrote to it. That is a different question from whether anything is going to reply, and you can build an inbox on the first without taking a position on the second.

Reading

Messages page oldest-first on a stable cursor.
limit is 1–100, default 50. A cursor naming nothing in this conversation is a 400, not an empty page — silently returning the first page would make a paging client loop forever.

The message shape

role has three values and the third one is the point. user is the customer, assistant is the AI, and human is a person — a salesperson replying from the WhatsApp Business App, or your own team through this API.An inbox that renders assistant and human identically is an inbox showing its operator something untrue.
Never present: prompts, retrieval evidence, token counts, internal reply-button ids, or Meta media ids — the last are useless without the merchant’s own access token anyway.

Sending

string
default:"text"
text, template, image, video, audio, document, interactive, location or reaction.
string
A channel.message_id from this thread. WhatsApp draws it as a quoted reply, which is how an answer to a question asked four messages ago stops reading like a non-sequitur.
Prefer link over media_id wherever a public URL exists: Meta fetches and caches by URL, so one catalogue photo sent to a thousand customers is fetched once instead of uploaded once and served a thousand times. 201 returns the stored message and the conversation. It is recorded as role: "human" and fires message.sent like any other outbound message.

Two failures to handle

The 24-hour window has closed and this is not a template.
Checked before Meta is called, so you get a named condition and a remedy rather than an unmapped provider error. Read window.expires_at off the conversation and show a countdown instead of discovering the limit by failing.
Meta refused the message. Their words are in the body. Common causes: a template that is not approved yet, a number that has been blocked by the recipient, or a media URL Meta could not fetch.

Messaging someone who has not written

The route above needs a thread the customer started, which is the wrong shape for a broadcast, an invoice, a quotation, or “message this lead”.
string
required
The customer’s number in any dialable form. We normalise it.
string
Which of the merchant’s numbers to send from. Defaults to their primary — worth naming explicitly once a merchant has more than one, so a support message does not leave from the marketing line.
It creates the conversation, or reuses the open one for that number, so the customer’s reply lands in the thread your CRM already knows about rather than beside it. The response carries both, exactly as the conversation route does.
The 24-hour window still applies. A non-template to somebody who has not written in 24 hours is refused with the same 422 — Meta’s rule does not soften because you addressed a number instead of a thread.In practice, first contact is always a template.

Media

Inbound photos, documents and voice notes arrive as a reference:
Returns the bytes with their content type. Store them, show them — a photo of damage, a signed PO, a voice note.
ref is our id for the message, never Meta’s media_id. Meta’s only resolves with the merchant’s own access token, so handing one to you would be both useless and a leak of the merchant’s Meta object graph. That is also why this streams the file rather than redirecting: there is no URL to redirect to that would not need the merchant’s token.
Meta expires media after about 30 days. After that this is a 502 and the file is honestly gone — fetch anything you intend to keep when the event arrives.

Read receipts and typing

Both default to the conversation’s most recent inbound message; pass {"message_id": "wamid.…"} to name one.
These are calls, not settings, and that is the point. Blue ticks should fire when a person opens the thread. A receipt sent automatically on arrival tells a customer that somebody read their message when nobody has looked at it — and a customer who works that out stops believing the ticks at all.
Meta only accepts a typing indicator on the same request that marks a message read, so typing/ marks it read too. That is their shape, not ours. The indicator clears when your next message is sent, or after about 25 seconds, and cannot be cancelled — send it when somebody is actually composing.

Taking over from the assistant

While a human holds a conversation the assistant stays silent and stops nudging. Send {"on": false} to give it back. Handover is a judgement somebody made, so only somebody releases it. The one exception is credit exhaustion: a conversation paused because the merchant ran out of credits resumes on its own when they top up, provided no human has replied in the meantime.
The merchant’s own staff can do this from their phone. Replying from the WhatsApp Business App takes over automatically, and /human, /ai and /clear pause, release and wipe a thread. Every one of those reaches you as handoff.requested — so your inbox stays correct even when nobody used your UI.

Asking the assistant to answer

202, and the turn runs asynchronously — the reply arrives as message.sent. Useful for a “draft a reply” button, or to re-run a turn that failed on a provider outage. Refused with 409 human_control while a human holds the conversation, unless you pass force. Without that, your own takeover would be silently undone by your own next call.

What a conversation looks like

whatsapp_number is the number this thread is happening on. Replies go out from it automatically — answering from a merchant’s other number would arrive, to the customer, as a message from a stranger.