Receber mensagens e status
O que o cliente escreve e o que acontece com o que você enviou chegam por webhook, no envelope da Meta. Este guia mostra o payload de cada tipo; o cadastro do webhook, a assinatura, as retentativas e a idempotência estão em Webhooks.
O que assinar#
fields | O que chega |
|---|---|
messages | As mensagens que os clientes enviam (changes[].value.messages) |
statuses | O status das mensagens que você enviou (changes[].value.statuses). Sai com changes[].field igual a messages: é a Meta que entrega os status dentro do campo messages |
Um webhook que assina os dois recebe messages e statuses no mesmo value quando a Meta os manda juntos. O value é o da Meta, repassado como chegou: ela pode acrescentar campos sem aviso, então ignore o que você não conhece.
O envelope#
Todo POST traz object: "whatsapp_business_account" e entry[].changes[], com field e value. value.metadata diz qual número recebeu (phone_number_id é o mesmo dos caminhos da API) e value.contacts traz o perfil do cliente. Uma mensagem de texto completa:
Os campos de toda mensagem: from (telefone do cliente, só dígitos), id (o wamid: use em context.message_id para responder e para descartar repetições), timestamp (segundos desde 1970, como texto) e type, que diz qual objeto traz o conteúdo. Nos exemplos abaixo, cada bloco é um item de value.messages[]; o resto do envelope é igual ao de cima.
{
"object": "whatsapp_business_account",
"entry": [
{
"id": "102290129340398",
"time": 1767225600,
"changes": [
{
"field": "messages",
"value": {
"messaging_product": "whatsapp",
"metadata": {
"display_phone_number": "554430000000",
"phone_number_id": "106540352242922"
},
"contacts": [
{
"profile": {
"name": "Maria Souza"
},
"wa_id": "5511999999999"
}
],
"messages": [
{
"from": "5511999999999",
"id": "wamid.HBgNNTUxMTk5OTk5OTk5ORUCABIYFjNFQjBDNUE2RjQ3RjE4QjdBMz01AA==",
"timestamp": "1767225600",
"type": "text",
"text": {
"body": "Vocês têm esse tênis em azul?"
}
}
]
}
}
]
}
]
}Mensagens por tipo#
Texto#
Texto em resposta a uma mensagem da empresa#
context.id é o wamid da mensagem que o cliente respondeu; context.from é o seu número. context também aparece em mensagens encaminhadas (forwarded, frequently_forwarded) e em quem escreveu pelo botão "Mensagem" de um produto (referred_product).
Primeira mensagem vinda de um anúncio (referral)#
Só na primeira mensagem de quem chegou por um anúncio de clique para o WhatsApp. ctwa_clid é o id do clique, o dado que fecha a atribuição da conversão.
Imagem#
A mídia chega só com o id, o mime_type e o sha256 (e caption, se houver). Baixe o arquivo com GET /{version}/{media_id} e a url que ele devolve (Mídia).
Áudio#
voice: true quer dizer que foi gravado no aplicativo (mensagem de voz).
Vídeo#
Documento#
Figurinha (sticker)#
Localização#
name, address e url só vêm quando o cliente escolheu um lugar. Também é o que chega quando o cliente responde a um pedido de localização (Mensagens interativas).
Contatos#
A Meta omite o que o cliente não compartilhou.
Reação#
Sem emoji quando o cliente remove a reação.
Toque num botão de resposta rápida de um modelo#
type: "button" (não interactive). context.id é o wamid do modelo. Veja Modelos de mensagem (templates).
Botão de resposta (button_reply)#
Resposta a uma mensagem interativa de botões. id é o que você pôs em reply.id no envio (Mensagens interativas).
Escolha numa lista (list_reply)#
Resposta de um Flow (nfm_reply)#
response_json é um texto com JSON: interprete com JSON.parse / json.loads. Traz o flow_token do envio e um campo por resposta (WhatsApp Flows).
Pedido pelo carrinho do catálogo#
Aviso do sistema: o cliente trocou de número#
Vem sem contacts no value.
Mensagem não suportada#
Uma mensagem que a Cloud API não entrega (por exemplo uma enquete, ou um tipo novo). Só errors diz o que houve; o type é unsupported ou unknown.
{
"from": "5511999999999",
"id": "wamid.HBgNNTUxMTk5OTk5OTk5ORUCABIYFjNFQjBDNUE2RjQ3RjE4QjdBMz01AA==",
"timestamp": "1767225600",
"type": "text",
"text": {
"body": "Vocês têm esse tênis em azul?"
}
}{
"from": "5511999999999",
"id": "wamid.HBgNNTUxMTk5OTk5OTk5ORUCABIYFjNFQjBDNUE2RjQ3RjE4QjdBMz02AA==",
"timestamp": "1767225600",
"type": "text",
"text": {
"body": "Pode ser amanhã à tarde."
},
"context": {
"from": "554430000000",
"id": "wamid.HBgNNTUxMTk5OTk5OTk5ORUCABEYEjdGQjI0QzE4RTlBMDVENzQxAA=="
}
}{
"from": "5511999999999",
"id": "wamid.HBgNNTUxMTk5OTk5OTk5ORUCABIYFjNFQjBDNUE2RjQ3RjE4QjdBMz03AA==",
"timestamp": "1767225600",
"type": "text",
"text": {
"body": "Olá! Quero saber mais sobre a promoção."
},
"referral": {
"source_url": "https://fb.me/3cr4Wqqkv",
"source_id": "120226305854810726",
"source_type": "ad",
"body": "Tênis com 25% de desconto",
"headline": "Fale com a gente",
"media_type": "image",
"image_url": "https://scontent.xx.fbcdn.net/v/t45.1/anuncio.jpg",
"ctwa_clid": "Aff-n8ZTODiE79d22KtAwQKj9e_mIEOOj27vDVwFjN80dp4_0NiNhEgpGo0AHemvuSoifXaytfTzcchptiErTKCqTrJ5nW1h7IHYeYymGb5K5J5iTROpBhWAGaIAeUzHL50",
"welcome_message": {
"text": "Olá! Como podemos ajudar?"
}
}
}{
"from": "5511999999999",
"id": "wamid.HBgNNTUxMTk5OTk5OTk5ORUCABIYFjNFQjBDNUE2RjQ3RjE4QjdBMz04AA==",
"timestamp": "1767225600",
"type": "image",
"image": {
"id": "1003383421387256",
"mime_type": "image/jpeg",
"sha256": "SfInY0gGKTsJlUWbwxC1k+FAD0FZHvzwfpvO0zX0GUI=",
"caption": "Este aqui"
}
}{
"from": "5511999999999",
"id": "wamid.HBgNNTUxMTk5OTk5OTk5ORUCABIYFjNFQjBDNUE2RjQ3RjE4QjdBMz05AA==",
"timestamp": "1767225600",
"type": "audio",
"audio": {
"id": "1003383421387257",
"mime_type": "audio/ogg; codecs=opus",
"sha256": "SfInY0gGKTsJlUWbwxC1k+FAD0FZHvzwfpvO0zX0GUI=",
"voice": true
}
}{
"from": "5511999999999",
"id": "wamid.HBgNNTUxMTk5OTk5OTk5ORUCABIYFjNFQjBDNUE2RjQ3RjE4QjdBMz06AA==",
"timestamp": "1767225600",
"type": "video",
"video": {
"id": "1003383421387258",
"mime_type": "video/mp4",
"sha256": "SfInY0gGKTsJlUWbwxC1k+FAD0FZHvzwfpvO0zX0GUI="
}
}{
"from": "5511999999999",
"id": "wamid.HBgNNTUxMTk5OTk5OTk5ORUCABIYFjNFQjBDNUE2RjQ3RjE4QjdBMz07AA==",
"timestamp": "1767225600",
"type": "document",
"document": {
"id": "1003383421387259",
"mime_type": "application/pdf",
"sha256": "SfInY0gGKTsJlUWbwxC1k+FAD0FZHvzwfpvO0zX0GUI=",
"filename": "comprovante.pdf"
}
}{
"from": "5511999999999",
"id": "wamid.HBgNNTUxMTk5OTk5OTk5ORUCABIYFjNFQjBDNUE2RjQ3RjE4QjdBMz08AA==",
"timestamp": "1767225600",
"type": "sticker",
"sticker": {
"id": "1003383421387260",
"mime_type": "image/webp",
"sha256": "SfInY0gGKTsJlUWbwxC1k+FAD0FZHvzwfpvO0zX0GUI=",
"animated": false
}
}{
"from": "5511999999999",
"id": "wamid.HBgNNTUxMTk5OTk5OTk5ORUCABIYFjNFQjBDNUE2RjQ3RjE4QjdBMz09AA==",
"timestamp": "1767225600",
"type": "location",
"location": {
"latitude": -23.561414,
"longitude": -46.655881,
"name": "Loja Paulista",
"address": "Av. Paulista, 1000 - São Paulo, SP"
}
}{
"from": "5511999999999",
"id": "wamid.HBgNNTUxMTk5OTk5OTk5ORUCABIYFjNFQjBDNUE2RjQ3RjE4QjdBMz10AA==",
"timestamp": "1767225600",
"type": "contacts",
"contacts": [
{
"name": {
"formatted_name": "João Lima",
"first_name": "João",
"last_name": "Lima"
},
"phones": [
{
"phone": "+55 11 97777-6666",
"wa_id": "5511977776666",
"type": "CELL"
}
]
}
]
}{
"from": "5511999999999",
"id": "wamid.HBgNNTUxMTk5OTk5OTk5ORUCABIYFjNFQjBDNUE2RjQ3RjE4QjdBMz11AA==",
"timestamp": "1767225600",
"type": "reaction",
"reaction": {
"message_id": "wamid.HBgNNTUxMTk5OTk5OTk5ORUCABEYEjdGQjI0QzE4RTlBMDVENzQxAA==",
"emoji": "❤️"
}
}{
"from": "5511999999999",
"id": "wamid.HBgNNTUxMTk5OTk5OTk5ORUCABIYFjNFQjBDNUE2RjQ3RjE4QjdBMz12AA==",
"timestamp": "1767225600",
"type": "button",
"context": {
"from": "554430000000",
"id": "wamid.HBgNNTUxMTk5OTk5OTk5ORUCABEYEjdGQjI0QzE4RTlBMDVENzQxAA=="
},
"button": {
"payload": "CONFIRM_42",
"text": "Confirmar"
}
}{
"from": "5511999999999",
"id": "wamid.HBgNNTUxMTk5OTk5OTk5ORUCABIYFjNFQjBDNUE2RjQ3RjE4QjdBMz13AA==",
"timestamp": "1767225600",
"type": "interactive",
"context": {
"from": "554430000000",
"id": "wamid.HBgNNTUxMTk5OTk5OTk5ORUCABEYEjdGQjI0QzE4RTlBMDVENzQxAA=="
},
"interactive": {
"type": "button_reply",
"button_reply": {
"id": "confirm",
"title": "Confirmar"
}
}
}{
"from": "5511999999999",
"id": "wamid.HBgNNTUxMTk5OTk5OTk5ORUCABIYFjNFQjBDNUE2RjQ3RjE4QjdBMz14AA==",
"timestamp": "1767225600",
"type": "interactive",
"context": {
"from": "554430000000",
"id": "wamid.HBgNNTUxMTk5OTk5OTk5ORUCABEYEjdGQjI0QzE4RTlBMDVENzQxAA=="
},
"interactive": {
"type": "list_reply",
"list_reply": {
"id": "express",
"title": "Expressa",
"description": "1 a 2 dias"
}
}
}{
"from": "5511999999999",
"id": "wamid.HBgNNTUxMTk5OTk5OTk5ORUCABIYFjNFQjBDNUE2RjQ3RjE4QjdBMz15AA==",
"timestamp": "1767225600",
"type": "interactive",
"context": {
"from": "554430000000",
"id": "wamid.HBgNNTUxMTk5OTk5OTk5ORUCABEYEjdGQjI0QzE4RTlBMDVENzQxAA=="
},
"interactive": {
"type": "nfm_reply",
"nfm_reply": {
"name": "flow",
"body": "Sent",
"response_json": "{\"flow_token\":\"cadastro-pedido-42\",\"name\":\"Maria Souza\",\"email\":\"maria@example.com\"}"
}
}
}{
"from": "5511999999999",
"id": "wamid.HBgNNTUxMTk5OTk5OTk5ORUCABIYFjNFQjBDNUE2RjQ3RjE4QjdBMz16AA==",
"timestamp": "1767225600",
"type": "order",
"order": {
"catalog_id": "367025965434465",
"text": "",
"product_items": [
{
"product_retailer_id": "tenis-azul-42",
"quantity": 1,
"item_price": 299.9,
"currency": "BRL"
}
]
}
}{
"from": "5511999999999",
"id": "wamid.HBgNNTUxMTk5OTk5OTk5ORUCABIYFjNFQjBDNUE2RjQ3RjE4QjdBMz17AA==",
"timestamp": "1767225600",
"type": "system",
"system": {
"body": "User Maria Souza changed from 5511999999999 to 5511966665555",
"wa_id": "5511966665555",
"type": "user_changed_number"
}
}{
"from": "5511999999999",
"id": "wamid.HBgNNTUxMTk5OTk5OTk5ORUCABIYFjNFQjBDNUE2RjQ3RjE4QjdBMz18AA==",
"timestamp": "1767225600",
"type": "unsupported",
"errors": [
{
"code": 131051,
"title": "Message type unknown",
"message": "Message type unknown",
"error_data": {
"details": "Message type is currently not supported."
}
}
]
}Status das mensagens enviadas#
Cada mensagem que você enviou (pela API, pelo Inbox ou por uma campanha) gera status em value.statuses[]. id é o wamid da resposta do envio (messages[0].id), recipient_id o cliente e status o estado. O filtro statuses do webhook escolhe quais chegam; null = todos. Um envelope completo:
Os demais são itens de value.statuses[].
sent#
A Meta aceitou e despachou. Traz a conversa cobrada: conversation.expiration_timestamp é quando a janela fecha (Janela de atendimento de 24 horas). pricing diz se a mensagem é cobrada e em qual categoria.
delivered#
read#
failed#
A mensagem aceita (200) não foi entregue. errors[].code e error_data.details dizem por quê. Aqui, o código 131047: fora da janela de 24 h.
A ordem dos status não é garantida: um read pode chegar antes do delivered da mesma mensagem. Guarde o status mais avançado (sent < delivered < read). Se o envio tinha biz_opaque_callback_data, ele volta no status.
{
"object": "whatsapp_business_account",
"entry": [
{
"id": "102290129340398",
"time": 1767225600,
"changes": [
{
"field": "messages",
"value": {
"messaging_product": "whatsapp",
"metadata": {
"display_phone_number": "554430000000",
"phone_number_id": "106540352242922"
},
"statuses": [
{
"id": "wamid.HBgNNTUxMTk5OTk5OTk5ORUCABEYEjdGQjI0QzE4RTlBMDVENzQxAA==",
"timestamp": "1767225600",
"recipient_id": "5511999999999",
"status": "delivered",
"pricing": {
"billable": true,
"pricing_model": "PMP",
"category": "utility"
}
}
]
}
}
]
}
]
}{
"id": "wamid.HBgNNTUxMTk5OTk5OTk5ORUCABEYEjdGQjI0QzE4RTlBMDVENzQxAA==",
"timestamp": "1767225600",
"recipient_id": "5511999999999",
"status": "sent",
"conversation": {
"id": "b1a2c3d4e5f60718293a4b5c6d7e8f90",
"expiration_timestamp": "1767312000",
"origin": {
"type": "utility"
}
},
"pricing": {
"billable": true,
"pricing_model": "PMP",
"category": "utility"
}
}{
"id": "wamid.HBgNNTUxMTk5OTk5OTk5ORUCABEYEjdGQjI0QzE4RTlBMDVENzQxAA==",
"timestamp": "1767225600",
"recipient_id": "5511999999999",
"status": "delivered",
"pricing": {
"billable": true,
"pricing_model": "PMP",
"category": "utility"
}
}{
"id": "wamid.HBgNNTUxMTk5OTk5OTk5ORUCABEYEjdGQjI0QzE4RTlBMDVENzQxAA==",
"timestamp": "1767225600",
"recipient_id": "5511999999999",
"status": "read"
}{
"id": "wamid.HBgNNTUxMTk5OTk5OTk5ORUCABEYEjdGQjI0QzE4RTlBMDVENzQxAA==",
"timestamp": "1767225600",
"recipient_id": "5511999999999",
"status": "failed",
"errors": [
{
"code": 131047,
"title": "Re-engagement message",
"message": "Re-engagement message",
"error_data": {
"details": "Message failed to send because more than 24 hours have passed since the customer last replied to this number."
},
"href": "https://developers.facebook.com/docs/whatsapp/cloud-api/support/error-codes/"
}
]
}JavaScript
function handle(deliveryId, envelope) {
if (alreadyProcessed(deliveryId)) return; // ver webhooks.md
for (const change of envelope.entry[0].changes) {
if (change.field !== 'messages') continue;
const { messages = [], statuses = [], contacts = [] } = change.value;
for (const message of messages) {
switch (message.type) {
case 'text': onText(message.from, message.text.body); break;
case 'image': case 'audio': case 'video': case 'document': case 'sticker':
onMedia(message.type, message[message.type].id); break; // baixe com GET /{version}/{media_id}
case 'location': onLocation(message.location); break;
case 'interactive': onInteractiveReply(message); break; // button_reply, list_reply, nfm_reply
case 'button': onTemplateButton(message.context?.id, message.button.payload); break;
case 'unsupported': case 'unknown': console.warn(message.errors); break;
default: break; // contacts, reaction, order, system: trate se precisar
}
}
for (const status of statuses) {
if (status.status === 'failed') onFailed(status.id, status.errors); // ex.: 131047 -> reenviar como modelo
else onStatus(status.id, status.status);
}
}
}Python
def handle(delivery_id, envelope):
if already_processed(delivery_id): # ver webhooks.md
return
for change in envelope["entry"][0]["changes"]:
if change["field"] != "messages":
continue
value = change["value"]
for message in value.get("messages", []):
kind = message["type"]
if kind == "text":
on_text(message["from"], message["text"]["body"])
elif kind in ("image", "audio", "video", "document", "sticker"):
on_media(kind, message[kind]["id"]) # baixe com GET /{version}/{media_id}
elif kind == "location":
on_location(message["location"])
elif kind == "interactive":
on_interactive_reply(message) # button_reply, list_reply, nfm_reply
elif kind == "button":
on_template_button(message.get("context", {}).get("id"), message["button"]["payload"])
elif kind in ("unsupported", "unknown"):
print(message.get("errors"))
for status in value.get("statuses", []):
if status["status"] == "failed":
on_failed(status["id"], status.get("errors")) # ex.: 131047 -> reenviar como modelo
else:
on_status(status["id"], status["status"])Outras notas#
- Repetições: a Meta pode reenviar o mesmo evento. Ele chega com o mesmo
X-Synergy-Delivery-Id, emessages[].idtambém não muda. - Para responder, use o
messages[].idemcontext.message_id(Enviar mensagens); para marcar como lida, o mesmoidcomstatus: "read". - O recebimento nunca é bloqueado pelo plano.
- Um envio pela API pública não gera
smb_message_echoes: foi a própria integração que enviou. - Para testar sem esperar um cliente real,
POST /v1/webhooks/{hook_id}/testenvia um eventomessagesde exemplo.
Ficou com dúvida? Escreva para suporte@synergyconnect.com.br.