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
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
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
422 outside_customer_service_window
422 outside_customer_service_window
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.502 channel_rejected
502 channel_rejected
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.
Media
Inbound photos, documents and voice notes arrive as a reference: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.502 and the file
is honestly gone — fetch anything you intend to keep when the event arrives.
Read receipts and typing
{"message_id": "wamid.…"} to name one.
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
{"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
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.
