Sending Messages
Every message type the API sends, how media works, reactions, edits, deletes, typing and Status, and how to follow delivery.
All send routes are under the Messages tag in the API Reference tab. They share the same rules for the recipient, the response and retries.
The basics
Recipient (to)
- A phone number with country code and area code, digits only:
5581999999999.+, spaces and dashes are ignored. - Or a chat ID:
5581999999999@c.us,...@lid, or a group...@g.us. - Brazilian numbers are checked on WhatsApp before sending, in both spellings of the ninth digit. If the number has no WhatsApp account, the answer is
422 numero_sem_whatsappand nothing is sent. - Status and broadcast lists (
...@broadcast) are refused with422 destino_nao_suportado. To publish a Status, use the Status routes.
Response
{
"data": {
"message_id": "true_5581999999999@c.us_3EB0C767D26A1D5E",
"chat_id": "5581999999999@c.us"
}
}message_id(true_<chat>_<id>for messages this number sent): store it. It matches themessage.sentandmessage.statuswebhooks, and you use it to react, reply quoting, edit and delete. It can benullif WhatsApp didn't return an id.chat_id: where the message actually went. It can be an@lidaddress.
Retries
Send an Idempotency-Key header with every send your system might repeat. The same value within 24 hours returns the first response without sending again. See Response format.
Text
curl -X POST https://app.wappfy.io/api/v1/messages/text \
-H "X-Api-Key: wpf_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "to": "5581999999999", "text": "Hi! Your order is on its way.", "link_preview": true }'| Field | Required | Notes |
|---|---|---|
to | Yes | Recipient. |
text | Yes | Up to 65,536 characters. WhatsApp formatting works: *bold*, _italic_, ~strikethrough~. |
reply_to | No | The message_id of a message to reply to, quoting it. The older name replyTo is still accepted. |
link_preview | No | true asks WhatsApp to show a preview of the first link. |
Image, file, video and voice note
| Route | Sends | Extra fields |
|---|---|---|
POST /messages/image | An image | caption (up to 1,024 characters) |
POST /messages/file | A document | filename (the name the contact sees), caption |
POST /messages/video | A video | caption |
POST /messages/voice | A voice note (the microphone audio, not an audio file) | reply_to. No caption: WhatsApp voice notes don't have one. |
Every media route takes the file in one of two ways, never both:
curl -X POST https://app.wappfy.io/api/v1/messages/image \
-H "X-Api-Key: wpf_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "to": "5581999999999", "url": "https://example.com/product.jpg", "caption": "Our new product" }'curl -X POST https://app.wappfy.io/api/v1/messages/file \
-H "X-Api-Key: wpf_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "to": "5581999999999", "base64": "data:application/pdf;base64,JVBERi0xLjQK...", "filename": "invoice.pdf" }'url: the file is downloaded by Wappfy's server, not by WhatsApp. It must be a publichttp(s)address; internal addresses, and redirects to them, are refused (400 midia_invalida). Up to 50 MB, 30 seconds and 3 redirects.base64: the file goes inside the request, plain or as a data URL (data:image/png;base64,...). The whole request is capped at 25 MB, so a file of about 18 MB fits. For larger files, useurl.mimetype(optional): the file type, such asimage/jpegorapplication/pdf. Without it, Wappfy uses the download'sContent-Type, then the file extension.
Voice notes play on WhatsApp in OGG/Opus (audio/ogg; codecs=opus). Other audio formats are only accepted when the server can convert them; otherwise the answer is 422 formato_de_voz_invalido and nothing is sent. To send an audio file as a file, use /messages/file.
curl -X POST https://app.wappfy.io/api/v1/messages/voice \
-H "X-Api-Key: wpf_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "to": "5581999999999", "url": "https://example.com/note.ogg", "mimetype": "audio/ogg; codecs=opus" }'Media errors: 413 midia_grande (over 50 MB), 413 corpo_grande_demais (body over 25 MB), 422 midia_indisponivel (the url didn't deliver a file), 502 midia_inacessivel (the connection to the url failed) and 504 midia_sem_resposta (the url didn't answer in time).
Location
curl -X POST https://app.wappfy.io/api/v1/messages/location \
-H "X-Api-Key: wpf_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "to": "5581999999999", "latitude": -8.0476, "longitude": -34.877, "title": "Downtown Store" }'latitude (-90 to 90) and longitude (-180 to 180) are decimal degrees. title, the place name, is optional.
Contact card
curl -X POST https://app.wappfy.io/api/v1/messages/contact \
-H "X-Api-Key: wpf_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "to": "5581999999999", "contacts": [{ "name": "Store Support", "phone": "5581988888888", "organization": "Downtown Store" }] }'Up to 10 contacts per card (vCard), each with name, phone (country code and area code, digits only) and an optional organization. The number on the card doesn't need WhatsApp.
Poll
curl -X POST https://app.wappfy.io/api/v1/messages/poll \
-H "X-Api-Key: wpf_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "to": "5581999999999", "name": "Which time works best?", "options": ["Morning", "Afternoon", "Evening"], "multiple_answers": false }'nameis the question.optionstakes 2 to 12 different options, up to 100 characters each.multiple_answers: truelets people pick more than one option (defaultfalse).- Votes arrive on the
poll.votewebhook, with this send'smessage_idaspoll_id.
List message
A button that opens a list of options. It only goes to individual chats: a group answers 422 lista_so_individual.
curl -X POST https://app.wappfy.io/api/v1/messages/list \
-H "X-Api-Key: wpf_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "5581999999999",
"title": "Menu",
"description": "Choose an option",
"footer": "Downtown Store",
"button": "See options",
"sections": [
{ "title": "Orders", "rows": [
{ "title": "Place an order", "row_id": "order" },
{ "title": "Talk to an agent", "description": "Mon to Fri, 8am to 6pm" }
] }
]
}'| Field | Limit |
|---|---|
title | Required, up to 60 characters |
description | Required, up to 1,024 characters (the message text) |
footer | Optional, up to 60 characters |
button | Required, up to 20 characters |
sections | 1 to 10 sections, with up to 10 rows in total |
Section title | Up to 24 characters |
Row title | Up to 24 characters |
Row description | Optional, up to 72 characters |
Row row_id | Optional, up to 200 characters, unique |
The response adds rows, with the row_id of each row. Rows without a row_id get row_1, row_2... by their position in the whole list. When the contact picks a row, a message.received webhook arrives with type: "list_response" and selected.row_id.
List messages are not part of WhatsApp's official API, and WhatsApp may stop delivering them at any time. Keep a plan B, such as a text or a poll.
Sticker
curl -X POST https://app.wappfy.io/api/v1/messages/sticker \
-H "X-Api-Key: wpf_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "to": "5581999999999", "url": "https://example.com/sticker.webp" }'The sticker must be WebP (ideally 512×512), by url or base64. It is not converted: another format answers 422 formato_de_figurinha_invalido and nothing is sent.
React to a message
curl -X PUT https://app.wappfy.io/api/v1/messages/reaction \
-H "X-Api-Key: wpf_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "messageId": "false_5581999999999@c.us_3EB0C767D26A1D5E", "reaction": "👍" }'messageId is the message's id, from a send response or a webhook; the chat is already inside it, so there is no to. An empty reaction ("") removes the reaction.
Mark as read
curl -X POST https://app.wappfy.io/api/v1/messages/seen \
-H "X-Api-Key: wpf_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "to": "5581999999999" }'Marks the whole chat as read (the blue ticks). To mark only some messages, add messageIds (up to 100).
Edit and delete what you sent
Use the full message_id of a message this number sent (it starts with true_). In the path, encode @ as %40 if your HTTP client doesn't.
# edit the text (or the caption of a media message)
curl -X PUT "https://app.wappfy.io/api/v1/messages/true_5581999999999%40c.us_3EB0C767D26A1D5E" \
-H "X-Api-Key: wpf_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "text": "Update: delivery tomorrow morning." }'
# delete for everyone
curl -X DELETE "https://app.wappfy.io/api/v1/messages/true_5581999999999%40c.us_3EB0C767D26A1D5E" \
-H "X-Api-Key: wpf_YOUR_KEY"- WhatsApp only accepts edits for a limited time after sending (currently 15 minutes). After that, it refuses with
recusado_pelo_whatsapp. The recipient sees the message marked as edited. - WhatsApp also limits how long after sending a message can be deleted for everyone. The recipient sees "This message was deleted".
- A received message (
false_...) answers422 mensagem_de_outro_remetente. An incomplete id answers422 id_de_mensagem_invalido.
Typing indicator
curl -X POST https://app.wappfy.io/api/v1/chats/typing \
-H "X-Api-Key: wpf_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "to": "5581999999999", "state": "typing" }'typing shows "typing...", recording shows "recording audio..." and paused removes it. WhatsApp also removes it after a while and when your message arrives.
Status (stories)
curl -X POST https://app.wappfy.io/api/v1/status/text \
-H "X-Api-Key: wpf_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "text": "Sale today only!", "background_color": "#38b42f", "contacts": ["5581999999999"] }'Without contacts, the Status goes to ALL of the number's contacts. To test, send contacts with only a number of your own.
| Route | Fields |
|---|---|
POST /status/text | text (up to 700 characters), background_color (#rrggbb), font (0 to 10) |
POST /status/image | url or base64, caption |
POST /status/video | url or base64, caption |
POST /status/voice | url or base64 (OGG/Opus, as for voice notes), background_color |
All four accept contacts: who sees it, as phone numbers or chat IDs (up to 256). The response has status_id and contacts. To delete, send the same contacts:
curl -X DELETE "https://app.wappfy.io/api/v1/status/true_status%40broadcast_3EB0C767D26A1D5E_5581988888888%40c.us" \
-H "X-Api-Key: wpf_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "contacts": ["5581999999999"] }'These routes are under the Status tag in the API Reference tab.
Following delivery
A 200 means WhatsApp accepted the message. What happens next arrives on your webhook:
| Event | When |
|---|---|
message.sent | A message left the number, from any source: the dashboard, the API or the phone. |
message.status | A sent message was delivered, read, played (audio or video) or failed. Status only moves forward. |
Match them to your send with message_id. Delivery order is not guaranteed: a message.status can arrive before the message.sent for the same message.

