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, lid, name }), and in the payload:
message_id, wa_message_id, from, body, and quoted_id when it's a reply.
Phone numbers are stored as the customer's phone JID (a unified conversation
across all your numbers).
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_vcard": "BEGIN:VCARD..." }
- Without
contact_vcard, we generate a simple vCard from the name/phone number.
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.
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. For
non-official senders, bZapper automatically sends 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.
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 (anti-ban — repeating identical text at scale 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.
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 |
otpis send-only (POST /messages/otp) — the code never comes back on receipt/inbox.
See also Webhooks (signature/delivery) and Customer support (a per-customer unified conversation).