Tipos de mensagem — envio e recebimento
Esta referência detalha o que você envia (request) e o que você recebe
(o payload do webhook message.received e do histórico em
GET /conversations/{jid}/messages) para cada tipo. Use isso para consumir
as informações conforme a sua necessidade.
Todo message.received traz no envelope: type, instance_id (o número que
recebeu), sender ({ jid, phone, lid, name }), e no payload: message_id,
wa_message_id, from, body, e quoted_id + quoted_participant (autor da citada)
quando for resposta. Mensagens de grupo: veja Grupos. Telefones
são gravados como o JID de telefone do cliente (conversa unificada entre
todos os seus números).
toAceita telefone em E.164 (+5511999999999), JID de contato
([email protected]), JID de grupo ([email protected]) e o @lid do contato — o
identificador de privacidade que a API devolve em sender.lid e nos participantes de
grupo. O @lid é traduzido para o telefone quando o número conectado já conhece aquele
LID; quando não conhece, a mensagem sai pelo próprio @lid. Ver
Contatos com @lid. No trilho oficial o destino
é sempre o telefone.
Texto
Envio — POST /messages/text
{ "to": "+5511999999999", "body": "Olá 👋" }
Recebido (type: "text")
{ "type": "text", "body": "Olá 👋", "from": "[email protected]" }
Imagem · Vídeo · Documento · Áudio · Sticker
Envio — POST /messages/{image|video|document|audio|sticker}
{ "to": "+5511999999999",
"media": { "url": "https://.../foto.jpg", "caption": "Olha isso", "filename": "foto.jpg", "mimetype": "image/jpeg", "ptt": false } }
urloubase64.ptt: true(áudio) = nota de voz.captionvale para image/video/document.
Recebido (type: "image" | "video" | "audio" | "document" | "sticker")
{ "type": "image",
"body": "Olha isso", // = caption
"media": {
"id": "uuid",
"url": "https://api.bzapper.com.br/media/uuid?exp=...&sig=...",
"mime_type": "image/jpeg", "filename": "foto.jpg", "size": 84211
} }
A mídia recebida não é pública: vai para um bucket privado
(files.bzapper.com.br, separado dos assets públicos em assets.bzapper.com.br).
A media.url é uma URL pré-assinada SigV4 com TTL (MEDIA_URL_TTL, ~24h,
configurável — ver config.go) — ela expira, então não a armazene.
A referência estável é GET /media/{id} (com exp+sig): a API responde
302 redirecionando para uma URL pré-assinada fresca, e o cliente baixa
direto do Spaces/CDN. Sempre que precisar do arquivo, refaça esse GET (não
guarde a media.url expirada). audio com ptt é nota de voz.
Localização
Envio — POST /messages/location
{ "to": "+5511999999999", "latitude": -23.561, "longitude": -46.656, "name": "Av. Paulista", "address": "São Paulo" }
Recebido (type: "location")
{ "type": "location", "body": "Av. Paulista", "latitude": -23.561, "longitude": -46.656 }
Contato (vCard)
Envio — POST /messages/contact
{ "to": "+5511999999999", "contact_name": "Suporte", "contact_phone": "+5511977776666" }
contact_phoneé o telefone do contato do cartão — a pessoa que você está compartilhando, nunca o destinatário. Com ele montamos o vCard, incluindo owaidque faz o WhatsApp reconhecer o número como contato e oferecer "conversar".- Já tem o cartão pronto? Mande
contact_vcarde pronto — aícontact_name/contact_phonenão são usados para montar nada. contact_namesemcontact_phonedevolve 400contact_phone_required.
Recebido (type: "contact") — body = nome exibido.
Reação (emoji)
Envio — POST /messages/reaction
{ "to": "+5511999999999", "quoted_message_id": "<wa_message_id>", "emoji": "❤️" }
Recebido (type: "reaction") — body = emoji; quoted_id = mensagem reagida.
Enquete (poll)
Envio — POST /messages/poll
{ "to": "+5511999999999", "name": "Qual horário?", "options": ["Manhã", "Tarde", "Noite"], "selectable_count": 1 }
Recebido — a enquete (type: "poll")
{ "type": "poll", "body": "Qual horário?",
"poll": { "name": "Qual horário?", "options": ["Manhã", "Tarde", "Noite"] } }
Recebido — o VOTO (type: "poll_vote")
{ "type": "poll_vote",
"body": "Tarde",
"poll_vote": {
"poll_message_id": "<wa_message_id da enquete>",
"selected": ["Tarde"] // opções escolhidas, já resolvidas
} }
O voto chega criptografado pelo WhatsApp (apenas hashes). O bZapper decifra e resolve os hashes contra as opções da enquete original, entregando os nomes em
poll_vote.selected. Correlacione com a enquete pelopoll_message_id. Voto múltiplo (selectable_count > 1) traz vários itens emselected.
A Cloud API da Meta não tem enquete — é a única lacuna de paridade entre os
trilhos, e ela é definitiva. Num projeto oficial, POST /messages/poll devolve
422 official_poll_unsupported e aponta a alternativa, que é uma
lista de opções. Ver
API oficial.
Botões e Listas (menu)
Envio — POST /messages/{buttons|list}
{ "to": "+5511999999999", "body": "Confirma o pedido?", "buttons": [{ "id": "sim", "title": "Sim" }, { "id": "nao", "title": "Não" }] }
O WhatsApp restringe botões/listas interativos a contas da API oficial. Como
o bZapper envia pelo protocolo multi-dispositivo, o que sai é sempre um menu de
texto numerado equivalente (fallback estável que sempre entrega). A resposta do
cliente chega como type: "text" com o texto/numero escolhido — trate como
texto comum.
No trilho oficial eles são nativos e renderizam: o bZapper usa o
interativo da Cloud API sempre que o pedido cabe nos limites da Meta (até 3
botões, título de até 20 caracteres; até 10 linhas de lista, título de até 24).
Acima disso, cai no mesmo menu de texto — a mensagem chega, só não chega
clicável. A resposta continua chegando como type: "text", agora com o id da
opção escolhida em payload.reply_id.
OTP (código de verificação)
Envio — POST /messages/otp
{ "to": "+5511999999999", "code": "738291", "expiry_minutes": 5 }
O OTP vai em duas mensagens no WhatsApp: um texto de contexto + um balão só com o código (fácil de copiar com long-press, em qualquer aparelho). É 1 OTP lógico e conta como 1 envio para cobrança.
- Se você omitir
body, a API gera o texto no idioma da conta com variações aleatórias (repetir texto idêntico para muitos destinatários facilita o fingerprint do WhatsApp).expiry_minutes(opcional) só é mencionado no texto. - O código nunca é persistido em claro nem exibido no inbox: guardamos apenas
uma versão mascarada (ex.:
••••91) para auditoria/UX, e o echoguard impede que o código apareça na transcrição.
Trate o code como segredo. O bZapper não o devolve em nenhuma listagem de
conversa nem em webhooks de status — só você (que o gerou) o conhece.
Template aprovado — só no trilho oficial
Envio — POST /messages/template
{ "to": "+5511999999999", "template": "pedido_confirmado", "language": "pt_BR",
"variables": ["Ana", "10432"], "header_variables": ["10432"] }
É o único jeito de iniciar conversa fora da janela de 24 horas da Cloud API. Você manda o nome do template; o id que a Meta deu a ele nunca aparece.
languagepode ser omitido quando o nome existe num só idioma. Se existir em mais de um, a resposta é 409template_ambiguous— mandar no idioma errado é pior que perguntar.- A quantidade de
variablestem que bater com o template aprovado. Conferimos antes de chamar a Meta: divergência é a causa nº 1 do erro132000. - Em template de categoria
AUTHENTICATION, mandecodeem vez devariables.
Num projeto do trilho não oficial este endpoint devolve 409
template_official_only: lá não existe aprovação prévia, e portanto não há o
que enviar por nome — mande o texto direto em /messages/text.
Criar, listar e acompanhar a aprovação: API oficial.
Tabela rápida (tipos recebidos)
type | Campos-chave no payload |
|---|---|
text | body |
image/video/audio/document/sticker | body (caption), media.{url,mime_type,filename,size} |
location | latitude, longitude, body (nome) |
contact | body (nome) |
reaction | body (emoji), quoted_id |
poll | poll.{name,options} |
poll_vote | poll_vote.{poll_message_id,selected[]}, body |
otpetemplatesão só de envio. O código do OTP nunca volta no recebimento/inbox, e o que chega de um template enviado é a resposta da pessoa — texto comum, outype: "text"comreply_idquando ela toca num botão.
Veja também Webhooks (assinatura/entrega) e Atendimento (conversa unificada por cliente).