Messages
Sending one SMS, MMS or WhatsApp template, uploading its picture, reading it back, and pictures people text you. How-to: Send a text, Send a WhatsApp template.
Generated from /openapi/v1.json; interactive version: API reference.
Endpoints
Section titled “Endpoints”| method | path | what it does | success |
|---|---|---|---|
GET | /v1/messages | List messages you sent, newest first | 200 |
POST | /v1/messages | Send one SMS or WhatsApp message | 202 |
GET | /v1/messages/{id} | Read a message’s status | 200 |
POST | /v1/media | Upload a picture or video to send as MMS | 201 |
GET | /v1/media/{id} | Get a download URL for a file someone sent in an MMS | 200 |
GET /v1/messages
Section titled “GET /v1/messages”List messages you sent, newest first.
The SMS and WhatsApp template sends of the workspace (this API’s, the dashboard’s and campaigns’), newest first by created_at to the microsecond, ties by id. Not the person’s messages and not inbox replies: those are in GET /v1/conversations/{id}/messages. Without channel, both channels in one list, each item with its channel; with it, that channel only. to, from, campaign, status and the created_* times narrow it. Each item is the object GET /v1/messages/{id} answers. A cursor belongs to the list that answered it: one from channel=sms is refused without channel, and the reverse (invalid_cursor, with a hint). A cursor from the SMS list before 2026-10-16 (when it was ordered by hand-over time) is refused once with invalid_cursor: start again without after.
Parameters
| name | in | type | description |
|---|---|---|---|
limit | query | integer 1–200 | Page size, 1 to 200 (default 50). |
after | query | string | The previous page’s next_cursor. |
channel | query | stringsms, whatsapp | Only this channel’s sends. Leave it out to list both channels together. |
to | query | string | Only messages to this recipient, E.164. |
campaign | query | string | Only this campaign’s messages: its ca-… id or UUID. On WhatsApp, the template sends the campaign made (also listed per person by GET /v1/campaigns/{id}/recipients). |
from | query | string | Only messages sent from this number: its E.164, pn-… id or the channel’s ch-… id, as GET /v1/senders lists them (on WhatsApp, the WhatsApp channel of that number). Without channel, a number that is only an SMS sender or only a WhatsApp number lists that channel’s sends. |
status | query | stringqueued, sending, sent, delivered, read, undelivered, failed, skipped, unconfirmed | Only messages with this status (SMS has no read; WhatsApp has no undelivered). Without channel, read lists WhatsApp only and undelivered SMS only. |
created_after | query | string | Only messages created at or after this, exact to the microsecond, as an ISO 8601 time (2026-10-16T09:00:00Z). Not a sync feed: created_at is stamped as a message is accepted, before it is committed, so a later page can show an older created_at; overlap your window or use the webhooks. |
created_before | query | string | Only messages created before this, as an ISO 8601 time (2026-10-16T09:00:00Z). |
Answers 200 with MessageList. Errors use the error envelope; common codes: SMS, uploads, WhatsApp, lists, every request.
POST /v1/messages
Section titled “POST /v1/messages”Send one SMS or WhatsApp message.
Sends one text (channel: sms) or one approved WhatsApp template (channel: whatsapp). Answers 202 with the message: it is on its way, not yet delivered. Follow it with GET /v1/messages/{id} or the message.status_updated webhook. Opt-outs, sender readiness, credits and sending limits are all checked before anything is sent.
Parameters
| name | in | type | description |
|---|---|---|---|
idempotency-key | header | string ≤ 255 chars | Send one with every message. A retry with the same key never sends a second message: it answers the first one as it is now. Rules: Idempotency. |
Request body: MessageCreate.
Answers 202 with Message. Errors use the error envelope; common codes: SMS, uploads, WhatsApp, lists, every request.
GET /v1/messages/{id}
Section titled “GET /v1/messages/{id}”Read a message’s status.
Parameters
| name | in | type | description |
|---|---|---|---|
id | path | string, required | The message id from the send. |
Answers 200 with Message. Errors use the error envelope; common codes: SMS, uploads, WhatsApp, lists, every request.
POST /v1/media
Section titled “POST /v1/media”Upload a picture or video to send as MMS.
Upload one file as a multipart/form-data part named file, then pass the answer’s media as media on POST /v1/messages, as content.media on POST /v1/campaigns or PATCH /v1/campaigns/{id}, or as media on POST /v1/conversations/{id}/messages. Accepted: a JPEG, PNG or GIF image up to 5 MB (sent without its metadata, such as GPS), or an MP4, MOV or WebM video up to 50 MB, which is sent as an MP4 of at most 500 KB (transcoded: true). The bytes decide the type; the declared content type and file name extension are ignored. An upload that no message, campaign or template uses is deleted after 7 days (expires_at). There is no Idempotency-Key: a retried upload stores a second copy, which the same rule deletes if it goes unused.
Request body (multipart/form-data)
| field | type | description |
|---|---|---|
file | string (binary), required | The picture or video. Any other form field is 400 unknown_field. |
Example: see Send a text.
Answers 201 with UploadedMedia. Errors use the error envelope; common codes: SMS, uploads, WhatsApp, lists, every request.
GET /v1/media/{id}
Section titled “GET /v1/media/{id}”Get a download URL for a file someone sent in an MMS.
Each message.received event lists the photos and files the person sent in media[], and each item’s url is this endpoint. We fetch every file from the carrier when the message arrives and keep our own copy, so the link keeps working after the carrier deletes theirs. The answer carries a short-lived download URL (see expires_in): fetch it when you need the bytes, rather than storing it. Scoped to the workspace that owns the API key; a file in another workspace returns 404, not 403. A file we could not fetch from the carrier is not listed in the event and has no media id.
Parameters
| name | in | type | description |
|---|---|---|---|
id | path | string, required | The att-… media id from message.received.media[].url. |
Answers 200 with the object below. Errors use the error envelope; common codes: SMS, uploads, WhatsApp, lists, every request.
| field | type | description |
|---|---|---|
object | string, required always media | |
id | string, required | |
message_id | string, required | |
conversation_id | string, required | |
content_type | string, required | |
size | integer, required | Bytes. |
url | string, required | Presigned HTTPS URL. Treat as a secret — it grants read access until it expires. |
expires_in | integer, required | Seconds until url stops working. |
Example: see Inbound pictures.
Objects
Section titled “Objects”Message
Section titled “Message”| field | type | description |
|---|---|---|
id | string, required | |
object | string, required always message | |
channel | string, requiredsms, whatsapp | |
from | string or null, required | The sending number in E.164. null when it could not be read: a WhatsApp number whose channel has no display number yet, or a sender id that no longer resolves. |
to | string, required | |
status | string, requiredqueued, sending, sent, delivered, read, undelivered, failed, skipped, unconfirmed | queued, sending, sent (the carrier or Meta has it), delivered, read (WhatsApp), undelivered, failed, skipped (opted out just before sending) or unconfirmed (we could not confirm it went; do not resend). |
conversation_id | string or null, required | |
campaign_id | string or null, required | |
segments | integer or null, required | SMS only. |
billed_credits | number or null, required | Credits this message cost. SMS: null until it is billed. WhatsApp: always null (not reported per message). |
error | object or null, required | |
error.code | string or null, required | The carrier’s, Meta’s or the call’s own code, when there is one. |
error.category | string, requiredopted_out, carrier_filtered, rate_limited, invalid_number, unreachable, account, unconfirmed, blocked_by_route, not_sent, other | Closed set: opted_out, carrier_filtered, rate_limited, invalid_number, unreachable, account, unconfirmed, blocked_by_route, not_sent, other. |
error.message | string, required | |
error.action | string or null, required | What to do about it. |
template | object or null, required | WhatsApp only. |
template.id | string, required | |
template.name | string, required | |
template.language | string, required | |
text | string or null, required | What was sent: an SMS’s text (for a long text sent in parts, this part’s), or a WhatsApp template’s text as the person got it. Null when there is none, or once the platform’s retention cleared a text’s body. |
media | array of objects, required | The files sent with it (an MMS), as a conversation message lists them; [] for none. |
media[].id | string or null, required | The file’s att-… id. |
media[].url | string or null, required | GET it with your key for a download link (/v1/media/{id}). Null for a file kept only as a link. |
media[].content_type | string or null, required | |
created_at | string or null, required | |
sent_at | string or null, required | |
delivered_at | string or null, required | |
read_at | string or null, required | |
updated_at | string or null, required | When the status last changed. SMS only: always null on WhatsApp (follow sent_at, delivered_at and read_at). |
metadata | map of string to string or null, required | The metadata you sent, as you sent it; null when you sent none (or {}). |
Example: see Send a text.
MessageCreate
Section titled “MessageCreate”One message. channel decides the rest of the body. More channels may be added later; ignore values you don’t know.
Fields for every channel:
| field | type | description |
|---|---|---|
from | string, required 1–64 chars | The number to send from, as GET /v1/senders lists it: its E.164 (+16285550100), its pn-… id or UUID, or the ch-… id of one of its channels. Every channel takes the same forms. The shared trial number sends no texts or campaigns (422 trial_sender_not_allowed). |
to | string, required 1–32 chars | The recipient, in E.164: +, the country code and the number, no spaces or punctuation. |
metadata | map of string to string ≤ 20 keys; keys 1–40 chars; values ≤ 500 chars | Your own key/value pairs, stored and returned as sent. String values only; at most 20 keys, each key 1–40 characters, each value at most 500 characters (400 invalid_request, param metadata or metadata.<key>). {} is the same as leaving it out. |
When channel is sms (SmsMessageCreate):
| field | type | description |
|---|---|---|
channel | string, required always sms | |
text | string ≤ 1,600 chars | The text, up to 1,600 characters. Optional when media is set. |
media | string ≤ 512 chars | A picture or video: the media that POST /v1/media answers. MMS goes to +1 numbers only. |
conversation | stringauto, none | auto (default) adds the message to the recipient’s conversation, so a reply lands next to it and your AI agent has the context. none records only the send. |
Example (channel: sms):
{ "channel": "sms", "from": "+16285550100", "to": "+14155550100", "text": "Your table is ready", "conversation": "auto", "metadata": { "order_id": "1042", "source": "checkout" }}When channel is whatsapp (WhatsappMessageCreate):
| field | type | description |
|---|---|---|
channel | string, required always whatsapp | |
template | object, required | |
template.id | string 1–64 chars | The template’s wt-… id or UUID. Or name it with name + language. |
template.name | string 1–512 chars | The template’s name, as approved by Meta. |
template.language | string 2–15 chars | With name: the language code, e.g. en_US. |
template.variables | map of string to string or array of string values 1–1,024 chars; ≤ 30 items; each 1–1,024 chars | The body variables. Named templates: an object ({ "first_name": "Mei" }). Positional templates ({{1}}): an array in order, or an object keyed "1", "2", …. |
template.header | object | |
template.header.type | string, requiredtext, image, video, document | |
template.header.text | string 1–1,024 chars | type: text: the header variable’s value. |
template.header.url | string (uri) ≤ 2,048 chars | image / video / document: an https URL Meta can fetch. |
template.header.filename | string 1–240 chars | document: the file name shown to the recipient. |
template.buttons | array of objects ≤ 10 items | |
template.buttons[].index | integer, required 0–9 | The button’s position in the template, from 0. |
template.buttons[].url_suffix | string 1–1,024 chars | Dynamic URL button: the value appended to the URL. |
template.buttons[].coupon_code | string 1–15 chars | Copy-code button: the code. |
template.buttons[].payload | string 1–256 chars | Quick-reply button: returned to you when the person taps it. |
template.components | array of map of string to any ≤ 12 items | Meta’s raw components array, for shapes variables, header and buttons cannot express. It is still checked against the approved template (422 template_components_mismatch). Send this OR variables/header/buttons, not both. |
Example (channel: whatsapp):
{ "channel": "whatsapp", "from": "+16285550100", "to": "+14155550100", "template": { "name": "order_ready", "language": "en_US", "variables": { "first_name": "Mei" } }}MessageList
Section titled “MessageList”| field | type | description |
|---|---|---|
object | string, required always list | |
data | array of Message, required | |
has_more | boolean, required | |
next_cursor | string or null, required | Pass as after for the next page; null on the last. |
Example: see Send a text.
UploadedMedia
Section titled “UploadedMedia”| field | type | description |
|---|---|---|
object | string, required always uploaded_media | |
media | string, required | Pass this exact string as media on POST /v1/messages, content.media on campaigns, or media on a conversation reply. |
content_type | string, required | The stored file’s type: image/jpeg, image/png, image/gif or video/mp4 (every video is stored as an MP4). |
size | integer, required | Bytes stored (after preparing). |
transcoded | boolean, required | true for a video, which is re-encoded as an MP4 of at most 500 KB. |
preview_url | string or null, required | A short-lived link to view the stored file. Treat as a secret. null when the link could not be made (the upload still succeeded). |
preview_expires_in | integer or null, required | Seconds until preview_url stops working; null with a null preview_url. |
expires_at | string (date-time), required | When the file is deleted if nothing uses it (7 days). A message, campaign or template that uses it keeps it. |
Example: see Send a text.