Sending messages
One command, six payload shapes, and how to choose between them.
Everything you send — a plain sentence, a link card, a poll — goes through one
command: POST /v1/commands/message.send.
The body is always two objects. target says where the message goes, payload
says what it is.
{
"target": {
"kind": "conversation",
"conversationId": "a0000000-0000-4000-8000-000000000001"
},
"payload": { "text": "Your table is ready." }
}A call the API accepts returns 202 with an id you can correlate later:
{ "commandId": "cmd-1", "status": "queued", "disposition": "queued" }202 means queued, not delivered
The API does not validate payload at the boundary. A malformed payload — an unknown kind, an
effect name that does not exist, a duplicated poll option — still returns 202, then settles
failed on the response stream seconds later. The 202 tells you
the command was durably queued and nothing more.
The boundary checks the envelope, not the contents. The body must be a JSON
object of at most 65,536 bytes carrying a target object and a payload
object. Those three conditions do give you a synchronous 400 —
body_too_large, body_not_object and target_and_payload_required
respectively — before anything is queued.
The six payload shapes
payload is a union discriminated by kind. Plain text is the exception: it
carries no kind at all — only text.
kind | What it sends | Target | Status |
|---|---|---|---|
| omitted | Plain text | conversation or recipient | Available |
rich_text | Text plus an effect, a threaded reply or a subject line | conversation | Available |
rich_link | A URL that Messages renders as a preview card | conversation | Available |
poll | A question with 2–12 options | conversation | Available |
attachment | A file or voice memo | conversation | Available |
sticker | A sticker, optionally pinned to a message | conversation | Unavailable — sticker sends not enabled yet |
Both attachment and sticker reference their media by transferId, which you
get from POST /v1/media. Attachment sends work today;
sticker sends are not enabled on connectors yet. See the attachments
page for the stage-and-send flow.
Two ways to address a send
message.send accepts two of the three target
kinds, and the choice changes more than the
address.
A conversation target replies into a thread you already know about. Its
id comes from an inbound message event on the stream. This is the only target
that accepts rich payloads, it honours Idempotency-Key, and it has a
30-second dispatch deadline.
A recipient target opens a new thread from an E.164 phone number — email
addresses work elsewhere in the API but not here, and one comes back
422 recipient_not_allowed. It takes plain text only — the server reads
payload.text, drops everything else without complaining, and caps the text at
16,384 bytes — and it ignores Idempotency-Key entirely, minting a fresh one
per call. A retried recipient send delivers twice.
The usual pattern is to open with a plain recipient send, wait for the conversation to appear on
the stream, then use its conversationId for everything after. See Addressing a
conversation.
Pick a payload
Text and rich text
The workhorse: plain text, byte limits, threaded replies and subject lines.
Message effects
The 13 bubble and full-screen effects, and who actually sees them.
Rich links
Send a URL as a preview card you do not control.
Polls
Create a poll — and what you cannot do with it afterwards.
Attachments and stickers
Not yet sendable, but receiving attachments works today.