/v1/conversations/{conversation_id}/messagesbetaSend 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
Protects against doing it twice if you retry: a retry with the same key gets the first answer back instead of running again.
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
| Field | Type | What it is |
|---|---|---|
| conversation_idrequired | string · path | The conversation id.pattern ^\d{1,19}$Signed in? Pick one from your data with “My data”. |
Body
| Field | Type | What it is |
|---|---|---|
| typerequired | string | 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.)one of: text, image, video, audio, document, sticker, location, interactive, reaction, system |
| text | string | type text: the message (1–4096 characters).1–4096 characters |
| reply_to_message_id | string | Quote this message of the conversation (a message id from its message list).pattern ^\d{1,19}$ |
| media | object | 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). |
| urlrequired | string | Public https URL of the file (the platform downloads it).0–2000 characters |
| caption | string | Caption (image, video, document).0–1024 characters |
| filename | string | document: file name shown.0–200 characters |
| location | object | type location: latitude, longitude, and an optional name and address. |
| latituderequired | number | Latitude.-90–90 |
| longituderequired | number | Longitude.-180–180 |
| name | string | Place name.0–255 characters |
| address | string | Address.0–255 characters |
| interactive | object | type interactive: kind buttons (1–3 quick replies), list (a menu of up to 10 rows) or cta_url (one link button). |
| kindrequired | string | buttons (1–3 quick replies), list (a menu), cta_url (one link button).one of: buttons, list, cta_url |
| bodyrequired | string | Message text (≤ 1024).1–1024 characters |
| header | string | Header text (≤ 60).0–60 characters |
| footer | string | Footer text (≤ 60).0–60 characters |
| buttons | array of object | kind buttons: 1–3 buttons, unique ids and titles.1–3 items |
| idrequired | string | Reply id you get back when tapped.1–256 characters |
| titlerequired | string | Button text (≤ 20).1–20 characters |
| list_button | string | kind list: the text of the button that opens the menu (≤ 20).1–20 characters |
| sections | array of object | kind list: 1–10 sections, 10 rows in total at most.1–10 items |
| title | string | Section title (≤ 24; needed with several sections).0–24 characters |
| rowsrequired | array of object | Rows.1–10 items |
| idrequired | string | Row id you get back.1–200 characters |
| titlerequired | string | Row title (≤ 24).1–24 characters |
| description | string | Row description (≤ 72).0–72 characters |
| url | string | kind cta_url: https link.uri · 0–2000 characters |
| url_text | string | kind cta_url: button text (≤ 20).1–20 characters |
| reaction | object | type reaction: the message id to react to and one emoji ("" removes the reaction). |
| message_idrequired | string | Message to react to (GET /v1/conversations/{conversation_id}/messages id).pattern ^\d{1,19}$ |
| emojirequired | string | One emoji; "" removes the reaction.0–16 characters |
| system | object | Deprecated: use Add a system message (POST /v1/conversations/{conversation_id}/system-messages), which takes the same fields. Still accepted with type: system. |
| textrequired | string | The line shown in the thread to staff (plain text, ≤ 4096).1–4096 characters |
| priority | boolean | High priority: moves the conversation up and notifies its assignee, a manager and the team leads (in-app + push). |
| system_type | string | 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.pattern ^[A-Za-z][A-Za-z0-9_]*$ · 0–100 characters |
| meta | object | 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. |
Headers
| Field | Type | What it is |
|---|---|---|
| Authorizationrequired | header | Bearer $API_KEY — your API key. |
| Api-Version | header | The 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-Keyrequired | header | Required 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.
| Field | Type | What it is |
|---|---|---|
| message_idrequired | string or null | |
| statusrequired | string | queued (accepted for delivery), created (system line) or dry_run. |
| typerequired | string | |
| conversation_idrequired | string | The conversation it went into (resolved from contactId / phone when those were given). |
| contact_idrequired | string or null | The customer it went to. |
| channelrequired | object | |
| idrequired | string | |
| namerequired | string or null | |
| typerequired | string | |
| windowrequired | object | |
| appliesrequired | boolean | false for channels without a window (web chat, custom) and for system messages. |
| openrequired | boolean or null | null when the window does not apply. |
| kindrequired | string or null | service (24 h), free_entry (72 h after an ad), standard, tiktok (48 h), web. |
| closes_atrequired | string or null | Open window: when it closes. |
| closed_atrequired | string or null | Closed window: when it closed (null = the customer never wrote on this channel, or unknown). |
| visible_to_customerrequired | boolean | |
| dry_run | boolean | true when this was a dry run: every check ran and nothing changed. |
Errors
Errors are application/problem+json. Branch on code.
| Status | Code | When |
|---|---|---|
| 400 | invalid_input | A field is missing or has the wrong format. |
| 401 | unauthenticated | The Authorization header is missing, the key is unknown, expired or revoked. |
| 403 | entitlement_required | The workspace's plan does not include API access ( |
| 403 | forbidden | The key's staff member may not reply here: the conversation is closed, or assigned to someone else ( |
| 403 | insufficient_scope | The key does not have the permission this operation needs. |
| 404 | not_found | No conversation with this id, or your key cannot see it. |
| 409 | conflict | The customer's reply window is closed ( |
| 429 | rate_limited | The key or workspace went over its rate limit. Wait for |
| 503 | upstream_unavailable | A service behind the API is briefly unavailable. Safe to retry with backoff. |
| 504 | timeout | 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.