Skip to main content

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.

Common fields on every received message

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

SendingPOST /messages/text

{ "to": "+5511999999999", "body": "Hi 👋" }

Received (type: "text")

{ "type": "text", "body": "Hi 👋", "from": "[email protected]" }

Image · Video · Document · Audio · Sticker

SendingPOST /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 } }
  • url or base64. ptt: true (audio) = voice note. caption applies 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
} }
Conversation media is PRIVATE

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

SendingPOST /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)

SendingPOST /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)

SendingPOST /messages/reaction

{ "to": "+5511999999999", "quoted_message_id": "<wa_message_id>", "emoji": "❤️" }

Received (type: "reaction") — body = the emoji; quoted_id = the reacted message.

Poll

SendingPOST /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 the poll_message_id. A multiple vote (selectable_count > 1) brings several items in selected.

Buttons and Lists (menu)

SendingPOST /messages/{buttons|list}

{ "to": "+5511999999999", "body": "Confirm the order?", "buttons": [{ "id": "yes", "title": "Yes" }, { "id": "no", "title": "No" }] }
Behavior

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)

SendingPOST /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.
OTP security

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)

typeKey fields in the payload
textbody
image/video/audio/document/stickerbody (caption), media.{url,mime_type,filename,size}
locationlatitude, longitude, body (name)
contactbody (name)
reactionbody (emoji), quoted_id
pollpoll.{name,options}
poll_votepoll_vote.{poll_message_id,selected[]}, body

otp is 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).