Sending messages
Este conteúdo não está disponível em sua língua ainda.
Call wabery.messages.send() or POST /v1/messages. Pass a channel, target a
conversation or opted-in WhatsApp number, and include exactly one content
field.
Per-recipient send rate
Section titled “Per-recipient send rate”Wabery accepts at most one new individual message every 6 seconds for the same channel sender and recipient. The limit applies independently to WhatsApp, Instagram, and Messenger. Different recipients, senders, or channel platforms have separate limits.
If a second POST /v1/messages request arrives during that window, Wabery does
not create or queue the message. It returns 429:
{ "error": "message_pair_rate_limit", "message": "Messaging to this recipient is rate limited. Retry after the indicated delay.", "channel_id": "channel_...", "channel_platform": "MESSENGER", "queued": false, "retryable": true, "retry_after": 4}Honor the Retry-After response header before retrying. The response also sets
RateLimit-Limit: 1, RateLimit-Remaining: 0, RateLimit-Reset to the Unix
reset timestamp, and X-Wabery-Limit-Type: message_pair_rate_limit.
Use one stable Idempotency-Key for every attempt at the same logical send. If
Wabery already has that key, it replays the existing result before applying a
new pair-limit check. A different request body with the same key still returns
409.
Wabery also applies pair pacing at the final provider boundary to messages from the inbox, automations, and workers. Those internal sends can remain queued until their safe delivery slot. WhatsApp group sends are excluded.
If the distributed delivery limiter is unavailable, the API fails closed: it
returns 503 message_rate_limit_unavailable, Retry-After: 1, and does not
queue the message. See Errors and rate limits
for retry guidance.
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.
Commerce messages
Section titled “Commerce messages”Commerce is available for individual WhatsApp recipients only. Use the flattened
Wabery shapes; the provider adapter builds Meta’s action envelope:
await wabery.messages.send({ channelId: "channel_...", conversationId: "conversation_...", interactive: { type: "product_list", header: { type: "text", text: "Summer collection" }, body: { text: "Choose an item" }, catalog_id: "catalog_...", sections: [{ title: "Featured", product_items: [{ product_retailer_id: "sku_123" }], }], },});product sends one SKU, product_list allows up to 10 sections and 30 products
total, and catalog_message opens the catalog. Groups reject all three. Inbound
individual messages preserve order, referral, ctwa_clid,
referred_product, and identity_key_hash in message history and
message.received webhooks.
Template parameters support text, image, document, video, currency,
date_time, coupon_code, payload, group_id, and Flow action values.
Calling messages
Section titled “Calling messages”WhatsApp supports two call-related interactive messages through
wabery.messages.send():
call_permission_requestasks the user to authorize future business-initiated calls;voice_calldisplays a button that lets the user call the business.
Neither message carries audio, and voice_call does not place an outbound
call. The business must use the Calling API control flow after permission is
granted. See WhatsApp Calling for the complete SDK,
REST, MCP, WebRTC/SIP, and webhook sequence.
Regional payment messages
Section titled “Regional payment messages”Meta’s WhatsApp Payments API is available only for eligible accounts and
supported countries. Wabery supports the regional order_details and
order_status interactive types without rewriting their country-specific
payment objects.
For India, provide Meta’s complete review_and_pay action:
await wabery.messages.send({ channelId: "channel_...", to: "+919876543210", idempotencyKey: "india-order-123", interactive: { type: "order_details", body: { text: "Review and pay for your order" }, action: { ...metaIndiaOrderAction, name: "review_and_pay", }, },});For Singapore, provide Meta’s nested order-details object instead of body or
action:
await wabery.messages.send({ channelId: "channel_...", to: "+6591234567", idempotencyKey: "singapore-order-123", interactive: { type: "order_details", order_details: metaSingaporeOrderDetails, },});Send a regional order-status update with Meta’s complete review_order action:
await wabery.messages.send({ channelId: "channel_...", to: "+919876543210", idempotencyKey: "order-status-123-paid", interactive: { type: "order_status", body: { text: "Your payment was received" }, action: { ...metaOrderStatusAction, name: "review_order", }, },});The placeholder objects above must contain the current fields required by Meta for the account’s country, payment configuration, currency, totals, order, and payment method. Wabery validates the outer regional shape and preserves the nested object. Meta remains responsible for eligibility and payment processing; your payment provider may charge separately. Groups reject payment interactive messages.
Send to a WhatsApp group
Section titled “Send to a WhatsApp group”Use groupId instead of conversationId or to. Groups support text, media,
approved text/media templates, and pin/unpin only:
await wabery.messages.send({ channelId: "channel_...", groupId: "group_opaque_7f3a92", idempotencyKey: "welcome-group-2026-08-22", pin: { type: "pin", messageId: "wamid....", expirationDays: 7 },});The REST pin field is expiration_days. Authentication/interactive templates,
commerce, reactions, locations, contacts, calls, edit/delete,
ephemeral, and view-once messages are unsupported in groups. See
WhatsApp Groups for management and webhooks.
Group sends reserve a durable intent before Wabery contacts Meta. Reuse the same
idempotencyKey (or REST Idempotency-Key/idempotency_key) for retries. The
SDK type, CLI, MCP tool, and REST API require a caller-stable key for groups. A normal
successful response is 202 with object: "message"; if Meta acceptance or
local persistence still needs reconciliation, Wabery returns 202 with
object: "message_request" and status: "processing". Do not create a new
key in response to that status, because it could send a duplicate group
message. If Wabery still cannot determine provider acceptance after the
reconciliation window, the same key returns 409 with
whatsapp_group_send_acceptance_unknown; verify delivery in WhatsApp Manager
before deciding whether to create a new send.
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. |
groupId |
string |
Opaque WhatsApp group id; mutually exclusive with conversationId and to. |
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 reply/button/prompt or individual-only product, product_list, and catalog_message. |
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. |
pin |
object |
Group-only pin/unpin; pin requires expirationDays (REST: expiration_days) from 1–30. |
replyTo |
string |
WhatsApp message id to quote. A modifier — combines with any content field. |
idempotencyKey |
string |
Required safe-retry key for groups. Generate it once and reuse it for retries (see Errors). |