Skip to content
API PlatformDevelopers
POST/v1/messagesbeta

Send a message

Needs
Send WhatsApp messages; manage templates, quick messages and WhatsApp short links (messaging:write)
Plan
Any plan with API access
Limits
60 a minute per key · 20 an hour · 100 a day · 3 per contact a day
Dry run
Yes — a full preview with ?dry_run=true
Undo
Cannot be undone: the customer receives the message.

Sends one free-form message to a customer: text, media by public https link, a location, buttons, a list, a link button or a reaction. The customer receives it and it cannot be undone.

Say who receives it with exactly one of:

TargetChannelsGoes into
conversation_idanythat conversation
contact_idanythe contact's conversation
phone (the customer's number, international format such as +15555550123)WhatsApp onlythe conversation of the contact with that number

With contact_id or phone you may also choose where it goes out from: channel_id, or from_phone (one of your connected WhatsApp numbers, in any format). When the contact has conversations on several channels and you name none, the one with an open reply window is used; when several are open you must choose (400, reason: channel_ambiguous, with the choices). Web chat and custom channels have no reply window, so they are never picked over a WhatsApp, Instagram, Messenger or TikTok conversation: name them with channel_id (they are picked by themselves only when the contact has no other kind of conversation). With conversation_id the conversation's own channel is used, so neither is accepted.

A free-form message needs an open reply window where the channel has one (on WhatsApp, 24 hours after the customer's last message). The message always goes into the customer's existing conversation: when there is none on that channel (the customer never wrote there, or no contact has that number) or its window is closed, the answer is 409 with reason: window_closed and nothing is sent. Send an approved template with Send a WhatsApp template instead (contact_id is in the answer when the contact is known). A contact with do-not-disturb on is not messaged by contact_id or phone (409, reason: contact_dnd).

The key's staff member must be allowed to reply in that conversation. Customer messages count against the workspace send limits. Try it with ?dry_run=true: every check runs and the answer shows the conversation, contact, channel and window it resolved to, but nothing is sent.

Try it

Body

Choose who receives it, then (for a contact or a number) where it goes out from. The other fields are left out of the request, so it never names two targets.

Send to

The customer's WhatsApp number in international format (+15555550123). WhatsApp only.

Target: the customer's WhatsApp number in international format, e.g. `+15555550123` (spaces and dashes are fine). WhatsApp only; the number must belong to one of your contacts.

Send from

One of your connected WhatsApp numbers, in any format.

With `contact_id` or `phone`, instead of `channel_id`: one of your connected WhatsApp numbers to send from, in any format.

What to send: `text`, `image`, `video`, `audio`, `document`, `sticker`, `location`, `interactive` or `reaction`. Send exactly the one matching block.

type `text`: the message (1–4096 characters).

Quote this message of the conversation (a message id from its message list).

media

type `image`, `video`, `audio`, `document` or `sticker`: `url`, a public https link to the file, with an optional `caption` (not for audio or stickers) and a `filename` for documents. The platform downloads the link when you send (it must answer directly, without a login or a redirect, within 60 seconds) and checks the file by its content: image JPEG or PNG up to 5 MB (WebP, GIF, HEIC and other images are converted), video MP4 or 3GP up to 16 MB, audio MP3, AAC, AMR, OGG or M4A up to 16 MB (other audio is converted), document PDF, Word, Excel or text up to 100 MB, sticker WebP up to 500 KB. The link must name its host on the default port (no IP address, port, user name, backslash or control character); a host that is private or internal, or that does not resolve, is refused (`reason: url_refused`, `private_address` or `dns_failed`). To send a file from your own system without hosting it, use **Send a file in a conversation** (`POST /v1/conversations/{conversation_id}/files`).

Public https URL of the file (the platform downloads it).

Caption (image, video, document).

document: file name shown.

location

type `location`: latitude, longitude, and an optional name and address.

Latitude.

Longitude.

Place name.

Address.

interactive

type `interactive`: `kind` `buttons` (1–3 quick replies), `list` (a menu of up to 10 rows) or `cta_url` (one link button).

buttons (1–3 quick replies), list (a menu), cta_url (one link button).

Message text (≤ 1024).

Header text (≤ 60).

kind buttons: 1–3 buttons, unique ids and titles.

kind list: the text of the button that opens the menu (≤ 20).

kind list: 1–10 sections, 10 rows in total at most.

kind cta_url: https link.

kind cta_url: button text (≤ 20).

reaction

type `reaction`: the message id to react to and one emoji (`""` removes the reaction).

Message to react to (`GET /v1/conversations/{conversation_id}/messages` id).

One emoji; "" removes the reaction.

Protects against doing it twice if you retry: a retry with the same key gets the first answer back instead of running again.

Test mode (dry run): nothing will change
Turns test mode off for this page only. It switches back when you leave the page.

Code and response

curl -X POST 'https://mcp.wa-api.cloud/v1/messages?dry_run=true' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
  "phone": "+15555550123",
  "from_phone": "+1 555-555-0100",
  "type": "text",
  "text": "Your order #1001 is ready for pickup."
}'

The code reads your key from $API_KEY.

Body

Body fields
FieldTypeWhat it is
conversation_idstringTarget: reply in this conversation. Takes neither channel_id nor from_phone.pattern ^\d{1,19}$Signed in? Pick one from your data with “My data”.
contact_idstringTarget: this contact, in their existing conversation.pattern ^\d{1,19}$Signed in? Pick one from your data with “My data”.
phonestringTarget: the customer's WhatsApp number in international format, e.g. +15555550123 (spaces and dashes are fine). WhatsApp only; the number must belong to one of your contacts.3–32 characters
channel_idstringWith contact_id or phone: send on this channel. Needed when the contact has open conversations on several channels.pattern ^\d{1,19}$Signed in? Pick one from your data with “My data”.
from_phonestringWith contact_id or phone, instead of channel_id: one of your connected WhatsApp numbers to send from, in any format.3–32 characters
typerequiredstringWhat to send: text, image, video, audio, document, sticker, location, interactive or reaction. Send exactly the one matching block.one of: text, image, video, audio, document, sticker, location, interactive, reaction, system
textstringtype text: the message (1–4096 characters).1–4096 characters
reply_to_message_idstringQuote this message of the conversation (a message id from its message list).pattern ^\d{1,19}$
mediaobjecttype image, video, audio, document or sticker: url, a public https link to the file, with an optional caption (not for audio or stickers) and a filename for documents. The platform downloads the link when you send (it must answer directly, without a login or a redirect, within 60 seconds) and checks the file by its content: image JPEG or PNG up to 5 MB (WebP, GIF, HEIC and other images are converted), video MP4 or 3GP up to 16 MB, audio MP3, AAC, AMR, OGG or M4A up to 16 MB (other audio is converted), document PDF, Word, Excel or text up to 100 MB, sticker WebP up to 500 KB. The link must name its host on the default port (no IP address, port, user name, backslash or control character); a host that is private or internal, or that does not resolve, is refused (reason: url_refused, private_address or dns_failed). To send a file from your own system without hosting it, use Send a file in a conversation (POST /v1/conversations/{conversation_id}/files).
urlrequiredstringPublic https URL of the file (the platform downloads it).0–2000 characters
captionstringCaption (image, video, document).0–1024 characters
filenamestringdocument: file name shown.0–200 characters
locationobjecttype location: latitude, longitude, and an optional name and address.
latituderequirednumberLatitude.-90–90
longituderequirednumberLongitude.-180–180
namestringPlace name.0–255 characters
addressstringAddress.0–255 characters
interactiveobjecttype interactive: kind buttons (1–3 quick replies), list (a menu of up to 10 rows) or cta_url (one link button).
kindrequiredstringbuttons (1–3 quick replies), list (a menu), cta_url (one link button).one of: buttons, list, cta_url
bodyrequiredstringMessage text (≤ 1024).1–1024 characters
headerstringHeader text (≤ 60).0–60 characters
footerstringFooter text (≤ 60).0–60 characters
buttonsarray of objectkind buttons: 1–3 buttons, unique ids and titles.1–3 items
idrequiredstringReply id you get back when tapped.1–256 characters
titlerequiredstringButton text (≤ 20).1–20 characters
list_buttonstringkind list: the text of the button that opens the menu (≤ 20).1–20 characters
sectionsarray of objectkind list: 1–10 sections, 10 rows in total at most.1–10 items
titlestringSection title (≤ 24; needed with several sections).0–24 characters
rowsrequiredarray of objectRows.1–10 items
idrequiredstringRow id you get back.1–200 characters
titlerequiredstringRow title (≤ 24).1–24 characters
descriptionstringRow description (≤ 72).0–72 characters
urlstringkind cta_url: https link.uri · 0–2000 characters
url_textstringkind cta_url: button text (≤ 20).1–20 characters
reactionobjecttype reaction: the message id to react to and one emoji ("" removes the reaction).
message_idrequiredstringMessage to react to (GET /v1/conversations/{conversation_id}/messages id).pattern ^\d{1,19}$
emojirequiredstringOne emoji; "" removes the reaction.0–16 characters

Headers

Headers
FieldTypeWhat it is
AuthorizationrequiredheaderBearer $API_KEY — your API key.
Api-VersionheaderThe API version to use, e.g. 2026-10-01. Default: the version your key is pinned to.one of: 2026-10-01 · pattern ^\d{4}-\d{2}-\d{2}$
Idempotency-KeyrequiredheaderRequired here. Any unique string (8–128 characters), e.g. your order id plus the step. Kept 24 hours. Not needed with dry_run=true.pattern ^[A-Za-z0-9._:-]+$ · 8–128 characters

Response 202

Accepted for delivery (or the dry-run check), with the conversation, contact and channel it resolved to. Delivery and read receipts show as the message's `status` in the message list.

Response fields
FieldTypeWhat it is
message_idrequiredstring or null
statusrequiredstringqueued (accepted for delivery), created (system line) or dry_run.
typerequiredstring
conversation_idrequiredstringThe conversation it went into (resolved from contactId / phone when those were given).
contact_idrequiredstring or nullThe customer it went to.
channelrequiredobject
idrequiredstring
namerequiredstring or null
typerequiredstring
windowrequiredobject
appliesrequiredbooleanfalse for channels without a window (web chat, custom) and for system messages.
openrequiredboolean or nullnull when the window does not apply.
kindrequiredstring or nullservice (24 h), free_entry (72 h after an ad), standard, tiktok (48 h), web.
closes_atrequiredstring or nullOpen window: when it closes.
closed_atrequiredstring or nullClosed window: when it closed (null = the customer never wrote on this channel, or unknown).
visible_to_customerrequiredboolean
dry_runbooleantrue when this was a dry run: every check ran and nothing changed.

Errors

Errors are application/problem+json. Branch on code.

StatusCodeWhen
400invalid_input

A field is missing or has the wrong format. errors[] points at each field. No target or more than one (reason: target_required, target_conflict; also channel_id or from_phone with conversation_id, or both of them), a phone number that is not in international format (reason: invalid_phone), a phone target on a channel that is not WhatsApp (reason: phone_needs_whatsapp), several contacts with that number (reason: contact_ambiguous: send contact_id), a number that too many other contacts' numbers contain to find it exactly (reason: lookup_incomplete: send contact_id), open conversations on several channels and none chosen (reason: channel_ambiguous), or a message that breaks a rule (missing block, media link not public https, reason: unsupported_on_channel). Also returned when the Idempotency-Key header is missing, or was used before with a different body.

401unauthenticated

The Authorization header is missing, the key is unknown, expired or revoked.

403entitlement_required

The workspace's plan does not include API access (api_access).

403forbidden

The key's staff member may not reply in the conversation: it is closed, or assigned to someone else (reason: cannot_send_here).

403insufficient_scope

The key does not have the permission this operation needs.

404not_found

The conversation, contact or channel is not in your workspace, or from_phone is not one of your connected WhatsApp numbers.

409conflict

No open reply window (reason: window_closed): there is no conversation with this customer on that channel, or its window closed. Send a template instead. Or the contact has do-not-disturb on (reason: contact_dnd), or the channel refused the message (reason: send_failed). Also returned while a request with the same Idempotency-Key is still running.

429rate_limited

The key or workspace went over its rate limit. Wait for Retry-After seconds. A send cap was reached (reason: send_cap, with window: hour, day or contact_day).

503upstream_unavailable

A service behind the API is briefly unavailable. Safe to retry with backoff.

504timeout

The change did not finish in time. Retry with the same Idempotency-Key: it never runs twice.

Examples

Message a customer by WhatsApp number, from one of your numbers

Request body

{
  "phone": "+15555550123",
  "from_phone": "+1 555-555-0100",
  "type": "text",
  "text": "Your order #1001 is ready for pickup."
}

Response 202

{
  "message_id": "5501239",
  "status": "queued",
  "type": "text",
  "conversation_id": "77410",
  "contact_id": "48213",
  "channel": {
    "id": "301",
    "name": "Main WhatsApp",
    "type": "whatsapp"
  },
  "window": {
    "applies": true,
    "open": true,
    "kind": "service",
    "closes_at": "2026-09-25T09:12:00.000Z",
    "closed_at": null
  },
  "visible_to_customer": true
}
Check a message to a contact on one channel (dry run)

Request body

{
  "contact_id": "48213",
  "channel_id": "301",
  "type": "text",
  "text": "Thanks! Your refund was issued today."
}

Response 202

{
  "message_id": null,
  "status": "dry_run",
  "type": "text",
  "conversation_id": "77410",
  "contact_id": "48213",
  "channel": {
    "id": "301",
    "name": "Main WhatsApp",
    "type": "whatsapp"
  },
  "window": {
    "applies": true,
    "open": true,
    "kind": "service",
    "closes_at": "2026-09-25T09:12:00.000Z",
    "closed_at": null
  },
  "visible_to_customer": true,
  "dry_run": true
}

Operation path

The same operation is also at POST /v1/ops/inbox_send_message, with every field in the JSON body.