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_whatsapp and nothing is sent.
  • Status and broadcast lists (...@broadcast) are refused with 422 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 the message.sent and message.status webhooks, and you use it to react, reply quoting, edit and delete. It can be null if WhatsApp didn't return an id.
  • chat_id: where the message actually went. It can be an @lid address.

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 }'
FieldRequiredNotes
toYesRecipient.
textYesUp to 65,536 characters. WhatsApp formatting works: *bold*, _italic_, ~strikethrough~.
reply_toNoThe message_id of a message to reply to, quoting it. The older name replyTo is still accepted.
link_previewNotrue asks WhatsApp to show a preview of the first link.

Image, file, video and voice note

RouteSendsExtra fields
POST /messages/imageAn imagecaption (up to 1,024 characters)
POST /messages/fileA documentfilename (the name the contact sees), caption
POST /messages/videoA videocaption
POST /messages/voiceA 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:

By url
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" }'
By base64
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 public http(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, use url.
  • mimetype (optional): the file type, such as image/jpeg or application/pdf. Without it, Wappfy uses the download's Content-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 }'
  • name is the question. options takes 2 to 12 different options, up to 100 characters each.
  • multiple_answers: true lets people pick more than one option (default false).
  • Votes arrive on the poll.vote webhook, with this send's message_id as poll_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" }
      ] }
    ]
  }'
FieldLimit
titleRequired, up to 60 characters
descriptionRequired, up to 1,024 characters (the message text)
footerOptional, up to 60 characters
buttonRequired, up to 20 characters
sections1 to 10 sections, with up to 10 rows in total
Section titleUp to 24 characters
Row titleUp to 24 characters
Row descriptionOptional, up to 72 characters
Row row_idOptional, 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_...) answers 422 mensagem_de_outro_remetente. An incomplete id answers 422 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.

RouteFields
POST /status/texttext (up to 700 characters), background_color (#rrggbb), font (0 to 10)
POST /status/imageurl or base64, caption
POST /status/videourl or base64, caption
POST /status/voiceurl 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:

EventWhen
message.sentA message left the number, from any source: the dashboard, the API or the phone.
message.statusA 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.

On this page