Sending messages
Send a message with wabery.messages.send() (or POST /v1/messages). Set
the channelId returned by the API, target either an existing conversation or
a dedicated-channel WhatsApp phone number with opt-in, and provide exactly one
content field.
Send text
Section titled “Send text”PHP and Python examples use the reusable clients from PHP, Python, and API clients.
await wabery.messages.send({ channelId: "channel_...", conversationId: "conversation_...", text: "Thanks for your message",});$message = waberyRequest("POST", "/messages", [ "channel_id" => "channel_...", "conversation_id" => "conversation_...", "text" => "Thanks for your message",]);message = wabery_request( "POST", "/messages", json={ "channel_id": "channel_...", "conversation_id": "conversation_...", "text": "Thanks for your message", },)curl https://api.wabery.com/v1/messages \ -H "Authorization: Bearer $WABERY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "channel_id": "channel_...", "conversation_id": "conversation_...", "text": "Thanks for your message" }'Send a template
Section titled “Send a template”Outside the 24-hour window, use an approved
template instead of free-form text. Create and get one approved first — see
WhatsApp templates — then send it by id, or by name plus
language:
Templates can only be submitted and approved on dedicated WhatsApp channels
whose Meta business account is verified and has a Meta payment method. The
Wabery sandbox cannot submit your templates. Check
channel.publish_readiness.can_submit_templates before creating templates;
readiness failures return 412 with blockers such as BUSINESS_NOT_VERIFIED
or NO_PAYMENT_METHOD. See
why WhatsApp templates need a payment method
for the Meta billing reason.
await wabery.messages.send({ channelId: "channel_...", to: "+14155550100", template: { name: "order_shipped", language: "en", components: [ { type: "body", parameters: [{ type: "text", text: "AB-2291" }], }, ], },});curl https://api.wabery.com/v1/messages \ -H "Authorization: Bearer $WABERY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "channel_id": "channel_...", "to": "+14155550100", "template": { "name": "order_shipped", "language": "en", "components": [ { "type": "body", "parameters": [{ "type": "text", "text": "AB-2291" }] } ] } }'Send a Flow
Section titled “Send a Flow”Trigger a WhatsApp Flow to collect structured data in-chat:
await wabery.flows.send("flow_...", { channelId: "channel_...", contactId: "contact_...", bodyText: "Tell us about your project",});curl https://api.wabery.com/v1/flows/flow_.../send \ -H "Authorization: Bearer $WABERY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "channel_id": "channel_...", "contact_id": "contact_...", "body_text": "Tell us about your project" }'flows.send sends an interactive flow message, which Meta only allows inside the
24-hour window. To send by the contact’s language, use
flows.sendByConfigKey("config_key", { … }) — see Localization.
Proactive flows (outside the 24-hour window)
Section titled “Proactive flows (outside the 24-hour window)”To reach a contact proactively, send an approved flow-type template (a template with
a FLOW button) and pass the flow token via a button action:
await wabery.messages.send({ channelId: "channel_...", to: "+14155550100", template: { name: "daily_reminder", language: "en", components: [ { type: "button", sub_type: "flow", index: "0", parameters: [{ type: "action", action: { flow_token: "..." } }], }, ], },});Send media
Section titled “Send media”Send an attachment through the same message endpoint. Channel support differs:
WhatsApp accepts image, document, audio, and video; Instagram accepts image,
audio, and video; Messenger accepts all four. The example below is a WhatsApp
document because caption and filename are WhatsApp-only.
await wabery.messages.send({ channelId: "channel_...", conversationId: "conversation_...", media: { type: "document", link: "https://example.com/invoice.pdf", filename: "invoice-2291.pdf", caption: "Your invoice", },});curl https://api.wabery.com/v1/messages \ -H "Authorization: Bearer $WABERY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "channel_id": "channel_...", "conversation_id": "conversation_...", "media": { "type": "document", "link": "https://example.com/invoice.pdf", "filename": "invoice-2291.pdf", "caption": "Your invoice" } }'WhatsApp accepts exactly one of media.link or media.id. id is a
WhatsApp-hosted media handle. Instagram and Messenger accept only a public HTTPS
link that Meta can fetch without custom headers; both reject id, caption,
and filename with HTTP 422. Instagram also rejects document attachments—send
the document URL as an ordinary text message.
See Media messages for Instagram/Messenger examples, inbound payloads, download authentication and expiry, provider size limits, and the complete validation-error list.
type: "audio" sends a voice note or audio file — useful for replying in kind
when a customer sends one.
await wabery.messages.send({ channelId: "channel_...", conversationId: "conversation_...", media: { type: "audio", link: "https://example.com/reply.ogg" },});Reply to a specific message
Section titled “Reply to a specific message”Set replyTo (reply_to) to the WhatsApp id of a message to quote it, so your
message renders in a contextual bubble under it. It works with any content
type — it is a modifier, not a content field, so it does not count toward the
exactly-one-of rule.
await wabery.messages.send({ channelId: "channel_...", conversationId: "conversation_...", replyTo: "wamid....", text: "Yes, that one is still available",});curl https://api.wabery.com/v1/messages \ -H "Authorization: Bearer $WABERY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "channel_id": "channel_...", "conversation_id": "conversation_...", "reply_to": "wamid....", "text": "Yes, that one is still available" }'When a customer replies to one of your messages, the message.received
webhook carries messages[].replied_to with the quoted
message_id and its from. Pass that message_id straight back as replyTo
to keep a thread going. replied_to is null on a fresh message.
Send a contact card
Section titled “Send a contact card”Share one or more contacts with contacts (1–20 cards), in the same shape Meta
uses.
await wabery.messages.send({ channelId: "channel_...", conversationId: "conversation_...", contacts: [ { name: { formatted_name: "Ada Lovelace", first_name: "Ada" }, phones: [{ phone: "+15555550123", type: "WORK", wa_id: "15555550123" }], org: { company: "Analytical Engines", title: "Lead" }, }, ],});curl https://api.wabery.com/v1/messages \ -H "Authorization: Bearer $WABERY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "channel_id": "channel_...", "conversation_id": "conversation_...", "contacts": [ { "name": { "formatted_name": "Ada Lovelace", "first_name": "Ada" }, "phones": [ { "phone": "+15555550123", "type": "WORK", "wa_id": "15555550123" } ] } ] }'name.formatted_name is required, and Meta additionally requires at least one of
first_name, last_name, middle_name, suffix or prefix — a card without
one returns 400 naming the field rather than failing at Meta. Setting
phones[].wa_id makes that number tappable as a WhatsApp contact in the card.
Optional fields: birthday (YYYY-MM-DD), emails, urls, addresses, org.
Contacts a customer shares with you arrive as inbound messages of type
contacts.
Send a reaction
Section titled “Send a reaction”React to an existing WhatsApp message with reaction. Use an empty emoji string
to remove a previous reaction.
await wabery.messages.send({ channelId: "channel_...", conversationId: "conversation_...", reaction: { messageId: "wamid....", emoji: "👍", },});curl https://api.wabery.com/v1/messages \ -H "Authorization: Bearer $WABERY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "channel_id": "channel_...", "conversation_id": "conversation_...", "reaction": { "message_id": "wamid....", "emoji": "👍" } }'Send a location
Section titled “Send a location”Send a WhatsApp location pin with latitude and longitude. name and address
are optional.
await wabery.messages.send({ channelId: "channel_...", conversationId: "conversation_...", location: { latitude: 37.7749, longitude: -122.4194, name: "San Francisco office", },});Send a sticker
Section titled “Send a sticker”Send a WhatsApp sticker by public HTTPS link or Meta media id:
await wabery.messages.send({ channelId: "channel_...", conversationId: "conversation_...", sticker: { link: "https://example.com/sticker.webp", },});Provide exactly one of sticker.link or sticker.id.
Send an interactive message
Section titled “Send an interactive message”Interactive reply buttons (up to 3), single-select lists, link
buttons, and the location / contact info prompts are all sent with the
interactive field. The SDK types match this wire shape exactly:
await wabery.messages.send({ channelId: "channel_...", conversationId: "conversation_...", interactive: { type: "button", body: { text: "Confirm your order?" }, buttons: [ { id: "confirm", title: "Confirm" }, { id: "cancel", title: "Cancel" }, ], },});The full wire shape, with limits:
curl https://api.wabery.com/v1/messages \ -H "Authorization: Bearer $WABERY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "channel_id": "channel_...", "conversation_id": "conversation_...", "interactive": { "type": "button", "body": { "text": "Confirm your order?" }, "buttons": [ { "id": "confirm", "title": "Confirm" }, { "id": "cancel", "title": "Cancel" } ] } }'buttons: 1–3 entries; title ≤ 20 chars. Optional header ({ "type": "text", "text": "…" }, ≤ 60) and footer ({ "text": "…" }, ≤ 60).
curl https://api.wabery.com/v1/messages \ -H "Authorization: Bearer $WABERY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "channel_id": "channel_...", "conversation_id": "conversation_...", "interactive": { "type": "list", "body": { "text": "Pick a delivery slot" }, "button": "View slots", "sections": [ { "title": "Today", "rows": [ { "id": "slot_am", "title": "Morning", "description": "9am–12pm" }, { "id": "slot_pm", "title": "Afternoon", "description": "1pm–5pm" } ] } ] } }'button is the list’s CTA label (≤ 20). sections: 1–10, each with 1–10 rows;
row title ≤ 24, description ≤ 72.
curl https://api.wabery.com/v1/messages \ -H "Authorization: Bearer $WABERY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "channel_id": "channel_...", "conversation_id": "conversation_...", "interactive": { "type": "cta_url", "body": { "text": "Your invoice is ready" }, "display_text": "Open invoice", "url": "https://example.com/i/abc123" } }'Maps a URL to a button so a long or opaque link does not have to sit in the body
text. One button only, per Meta. display_text ≤ 20 chars; url must be
http(s). Optional header and footer.
Free-form: it sends inside the 24-hour window with no template approval and no per-message template fee. The alternative is an approved template carrying a URL button, which costs approval per language and a fee per send.
curl https://api.wabery.com/v1/messages \ -H "Authorization: Bearer $WABERY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "channel_id": "channel_...", "conversation_id": "conversation_...", "interactive": { "type": "location_request_message", "body": { "text": "Where should we deliver?" } } }'location_request_message asks the customer to share their location;
request_contact_info asks for the phone number on their WhatsApp account. Both
take only body.
A tap on a reply button or list row comes back as a message.received
webhook carrying the id you set — deduplicate and branch on
it.
The other three send nothing back to route on. A cta_url tap just opens the
URL in the device browser and produces no webhook at all. A location request
arrives as an ordinary inbound location message, and a contact info request
as an inbound contacts message, so correlate those by conversation rather
than by an id.
Parameters
Section titled “Parameters”| Field | Type | Notes |
|---|---|---|
channelId |
string |
Required channel id from channels.list(). |
to |
string |
E.164 phone for dedicated WhatsApp channels (requires opt-in). |
conversationId |
string |
Existing conversation id. |
text |
string |
Free-form message, ≤ 4096 chars (within the window). |
template |
object |
Approved WhatsApp template id, or name plus language. |
media |
object |
Channel-supported image, document, video, or audio. WhatsApp accepts link/id; Instagram and Messenger require a public HTTPS link. |
interactive |
object |
WhatsApp button, list, cta_url, location_request_message, or request_contact_info payload. |
reaction |
object |
React to an existing WhatsApp message by messageId/message_id and emoji. |
location |
object |
WhatsApp location pin with latitude, longitude, optional name and address. |
sticker |
object |
WhatsApp sticker by public HTTPS link or Meta media id. |
contacts |
array |
1–20 contact cards. Each needs name.formatted_name plus one other name part. |
replyTo |
string |
WhatsApp message id to quote. A modifier — combines with any content field. |
idempotencyKey |
string |
Optional; safe-retry key (see Errors). |