Message types — sending and receiving
This reference details what you send (the request) and what you receive
(the payload of the message.received webhook and of the history at
GET /conversations/{jid}/messages) for each type. Use it to consume the
information however you need.
Every message.received carries in the envelope: type, instance_id (the number
that received it), sender ({ jid, phone, lid, name }), and in the payload:
message_id, wa_message_id, from, body, and quoted_id + quoted_participant
(the quoted message's author) when it's a reply. Group messages: see Groups.
Phone numbers are stored as the customer's phone JID (a unified conversation
across all your numbers).
to fieldAccepts a phone number in E.164 (+5511999999999), a contact JID
([email protected]), a group JID ([email protected]) and the contact's @lid — the
privacy identifier this API returns in sender.lid and in group participants. A @lid is
translated to the phone number when the connected number already knows that LID; when it
doesn't, the message goes out to the @lid itself. See
Contacts with @lid. On the official rail the
destination is always the phone number.
Text
Sending — POST /messages/text
{ "to": "+5511999999999", "body": "Hi 👋" }
Received (type: "text")
{ "type": "text", "body": "Hi 👋", "from": "[email protected]" }
Image · Video · Document · Audio · Sticker
Sending — POST /messages/{image|video|document|audio|sticker}
{ "to": "+5511999999999",
"media": { "url": "https://.../photo.jpg", "caption": "Check this out", "filename": "photo.jpg", "mimetype": "image/jpeg", "ptt": false } }
urlorbase64.ptt: true(audio) = voice note.captionapplies to image/video/document.
Received (type: "image" | "video" | "audio" | "document" | "sticker")
{ "type": "image",
"body": "Check this out", // = caption
"media": {
"id": "uuid",
"url": "https://api.bzapper.com.br/media/uuid?exp=...&sig=...",
"mime_type": "image/jpeg", "filename": "photo.jpg", "size": 84211
} }
Received media is not public: it goes to a private bucket
(files.bzapper.com.br, separate from the public assets at assets.bzapper.com.br).
The media.url is a SigV4 pre-signed URL with a TTL (MEDIA_URL_TTL, ~24h,
configurable — see config.go) — it expires, so don't store it.
The stable reference is GET /media/{id} (with exp+sig): the API responds
with 302 redirecting to a fresh pre-signed URL, and the client downloads
directly from Spaces/CDN. Whenever you need the file, redo that GET (don't keep the
expired media.url). audio with ptt is a voice note.
Location
Sending — POST /messages/location
{ "to": "+5511999999999", "latitude": -23.561, "longitude": -46.656, "name": "Av. Paulista", "address": "São Paulo" }
Received (type: "location")
{ "type": "location", "body": "Av. Paulista", "latitude": -23.561, "longitude": -46.656 }
Contact (vCard)
Sending — POST /messages/contact
{ "to": "+5511999999999", "contact_name": "Support", "contact_phone": "+5511977776666" }
contact_phoneis the phone of the contact on the card — the person you are sharing, never the recipient. We build the vCard from it, including thewaidthat makes WhatsApp recognise the number as a contact and offer to message them.- Already have the card? Send
contact_vcardinstead — thencontact_name/contact_phoneare not used to build anything. contact_namewithoutcontact_phonereturns 400contact_phone_required.
Received (type: "contact") — body = the displayed name.
Reaction (emoji)
Sending — POST /messages/reaction
{ "to": "+5511999999999", "quoted_message_id": "<wa_message_id>", "emoji": "❤️" }
Received (type: "reaction") — body = the emoji; quoted_id = the reacted message.
Poll
Sending — POST /messages/poll
{ "to": "+5511999999999", "name": "Which time slot?", "options": ["Morning", "Afternoon", "Evening"], "selectable_count": 1 }
Received — the poll (type: "poll")
{ "type": "poll", "body": "Which time slot?",
"poll": { "name": "Which time slot?", "options": ["Morning", "Afternoon", "Evening"] } }
Received — the VOTE (type: "poll_vote")
{ "type": "poll_vote",
"body": "Afternoon",
"poll_vote": {
"poll_message_id": "<wa_message_id of the poll>",
"selected": ["Afternoon"] // chosen options, already resolved
} }
The vote arrives encrypted by WhatsApp (hashes only). bZapper decrypts and resolves the hashes against the original poll's options, delivering the names in
poll_vote.selected. Correlate it with the poll using thepoll_message_id. A multiple vote (selectable_count > 1) brings several items inselected.
Meta's Cloud API has no poll — it is the only parity gap between the rails,
and it is permanent. On an official project, POST /messages/poll returns
422 official_poll_unsupported and points at the alternative, which is a
list of options. See
Meta's official API.
Buttons and Lists (menu)
Sending — POST /messages/{buttons|list}
{ "to": "+5511999999999", "body": "Confirm the order?", "buttons": [{ "id": "yes", "title": "Yes" }, { "id": "no", "title": "No" }] }
WhatsApp restricts interactive buttons/lists to official API accounts. Since
bZapper sends over the multi-device protocol, what goes out is always an equivalent
numbered text menu (a stable fallback that always delivers). The customer's reply arrives
as type: "text" with the chosen text/number — treat it as plain text.
On the official rail they are native and they render: bZapper uses the
Cloud API interactive whenever the request fits Meta's limits (up to 3 buttons,
titles up to 20 characters; up to 10 list rows, titles up to 24). Above that it
falls back to the same text menu — the message arrives, it just does not arrive
clickable. The reply still comes as type: "text", now with the id of the chosen
option in payload.reply_id.
OTP (verification code)
Sending — POST /messages/otp
{ "to": "+5511999999999", "code": "738291", "expiry_minutes": 5 }
The OTP goes out as two messages on WhatsApp: a context text + a bubble with just the code (easy to copy with a long-press on any device). It's 1 logical OTP and counts as 1 send for billing.
- If you omit
body, the API generates the text in the account's language with random variations (repeating identical text to many recipients makes it easier for WhatsApp to fingerprint).expiry_minutes(optional) is only mentioned in the text. - The code is never persisted in cleartext nor shown in the inbox: we keep only
a masked version (e.g.
••••91) for audit/UX, and echoguard prevents the code from appearing in the transcript.
Treat the code as a secret. bZapper does not return it in any conversation
listing or status webhook — only you (who generated it) know it.
Approved template — official rail only
Send — POST /messages/template
{ "to": "+5511999999999", "template": "pedido_confirmado", "language": "pt_BR",
"variables": ["Ana", "10432"], "header_variables": ["10432"] }
It is the only way to start a conversation outside the Cloud API's 24-hour window. You send the template name; the id Meta gave it never appears.
languagemay be omitted when the name exists in a single language. If it exists in more than one, the answer is 409template_ambiguous— sending in the wrong language is worse than asking.- The number of
variablesmust match the approved template. We check before calling Meta: a mismatch is the number one cause of error132000. - For an
AUTHENTICATIONtemplate, sendcodeinstead ofvariables.
On an unofficial project this endpoint returns 409
template_official_only: there is no prior approval there, and so nothing to
send by name — send the text directly to /messages/text.
Creating, listing and following approval: Meta's official API.
Quick table (received types)
type | Key fields in the payload |
|---|---|
text | body |
image/video/audio/document/sticker | body (caption), media.{url,mime_type,filename,size} |
location | latitude, longitude, body (name) |
contact | body (name) |
reaction | body (emoji), quoted_id |
poll | poll.{name,options} |
poll_vote | poll_vote.{poll_message_id,selected[]}, body |
otpandtemplateare send-only. The OTP code never comes back in the inbox, and what arrives from a sent template is the person's reply — plain text, ortype: "text"withreply_idwhen they tap a button.
See also Webhooks (signature/delivery) and Customer support (a per-customer unified conversation).