Skip to content
API PlatformDevelopers
POST/v1/conversations/{conversation_id}/messagesbeta

Send a message in a conversation

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.

Replies in a conversation you already have, on the conversation's own channel (WhatsApp, Instagram, Messenger, TikTok or web chat): 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. To reach a customer by contact id or WhatsApp number instead, use Send a message (POST /v1/messages). For a line only your team sees, use Add a system message.

Free-form messages need an open reply window where the channel has one (on WhatsApp, 24 hours after the customer's last message). When it is closed the answer is 409 with reason: window_closed: send an approved template with POST /v1/messages/template instead. Some types do not exist on some channels (no stickers on Instagram, no location on TikTok): 400 with reason: unsupported_on_channel. Text is at most 4096 characters.

The key's staff member must be allowed to reply here (the conversation is open and theirs, or they lead its team or manage the inbox). Customer messages count against the workspace send limits. Try it with ?dry_run=true: every check runs (conversation, permission, window, channel) and nothing is sent.

Try it

Path

The conversation id.

Body

What to send: `text`, `image`, `video`, `audio`, `document`, `sticker`, `location`, `interactive` or `reaction`. Send exactly the one matching block. (`system` is an older alias of **Add a system message**.)

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.

system

Deprecated: use **Add a system message** (`POST /v1/conversations/{conversation_id}/system-messages`), which takes the same fields. Still accepted with `type: system`.

The line shown in the thread to staff (plain text, ≤ 4096).

High priority: moves the conversation up and notifies its assignee, a manager and the team leads (in-app + push).

What kind of event the line records, e.g. ORDER_UPDATE, PAYMENT_RECEIVED, SHIPPING_UPDATE, CRM_SYNC (upper-cased; default NOTIFICATION). Reserved for the platform: MENTION, REMINDER, REMINDER_SET, REMINDER_ACKNOWLEDGED, REMINDER_DELETED, REMINDER_UPDATED, SLA_BREACH, CALL_EVENT, CLIENT_ACTION.

0 / 4,096 bytes as JSON

Structured data kept with the line (JSON object, ≤ 4096 bytes, nesting ≤ 3 levels, ≤ 50 keys per object) for integrations that read the thread later. Shown to staff as plain text at most; `source` (text ≤ 200 characters, or a number) appears in the priority notification. Reserved keys: mentionStaffs, acknowledged, reminder_id, created_by_staff_id, idempotency_key.

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/conversations/77410/messages?dry_run=true' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
  "type": "text",
  "text": "Yes! The blue one is in stock and ships today.",
  "reply_to_message_id": "5501234"
}'

The code reads your key from $API_KEY.

Parameters

Parameters
FieldTypeWhat it is
conversation_idrequiredstring · pathThe conversation id.pattern ^\d{1,19}$Signed in? Pick one from your data with “My data”.

Body

Body fields
FieldTypeWhat it is
typerequiredstringWhat to send: text, image, video, audio, document, sticker, location, interactive or reaction. Send exactly the one matching block. (system is an older alias of Add a system message.)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
systemobjectDeprecated: use Add a system message (POST /v1/conversations/{conversation_id}/system-messages), which takes the same fields. Still accepted with type: system.
textrequiredstringThe line shown in the thread to staff (plain text, ≤ 4096).1–4096 characters
prioritybooleanHigh priority: moves the conversation up and notifies its assignee, a manager and the team leads (in-app + push).
system_typestringWhat kind of event the line records, e.g. ORDER_UPDATE, PAYMENT_RECEIVED, SHIPPING_UPDATE, CRM_SYNC (upper-cased; default NOTIFICATION). Reserved for the platform: MENTION, REMINDER, REMINDER_SET, REMINDER_ACKNOWLEDGED, REMINDER_DELETED, REMINDER_UPDATED, SLA_BREACH, CALL_EVENT, CLIENT_ACTION.pattern ^[A-Za-z][A-Za-z0-9_]*$ · 0–100 characters
metaobjectStructured data kept with the line (JSON object, ≤ 4096 bytes, nesting ≤ 3 levels, ≤ 50 keys per object) for integrations that read the thread later. Shown to staff as plain text at most; source (text ≤ 200 characters, or a number) appears in the priority notification. Reserved keys: mentionStaffs, acknowledged, reminder_id, created_by_staff_id, idempotency_key.

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). 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. The type's block is missing or another one was sent, a media link is not public https, or the channel cannot carry this type (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 here: the conversation 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

No conversation with this id, or your key cannot see it.

409conflict

The customer's reply window is closed (reason: window_closed): send a template instead. 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

Reply to a customer

Request body

{
  "type": "text",
  "text": "Yes! The blue one is in stock and ships today.",
  "reply_to_message_id": "5501234"
}

Response 202

{
  "message_id": "5501236",
  "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
}
Ask with quick-reply buttons

Request body

{
  "type": "interactive",
  "interactive": {
    "kind": "buttons",
    "body": "Would you like us to hold one for you?",
    "buttons": [
      {
        "id": "hold_yes",
        "title": "Yes, please"
      },
      {
        "id": "hold_no",
        "title": "No, thanks"
      }
    ]
  }
}

Response 202

{
  "message_id": "5501237",
  "status": "queued",
  "type": "interactive",
  "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
}

Operation path

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