Synergy Connect

Documentação

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#

fieldsO que chega
messagesAs mensagens que os clientes enviam (changes[].value.messages)
statusesO 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.

JSON
{
  "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).

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.

JSON
{
  "from": "5511999999999",
  "id": "wamid.HBgNNTUxMTk5OTk5OTk5ORUCABIYFjNFQjBDNUE2RjQ3RjE4QjdBMz01AA==",
  "timestamp": "1767225600",
  "type": "text",
  "text": {
    "body": "Vocês têm esse tênis em azul?"
  }
}
JSON
{
  "from": "5511999999999",
  "id": "wamid.HBgNNTUxMTk5OTk5OTk5ORUCABIYFjNFQjBDNUE2RjQ3RjE4QjdBMz02AA==",
  "timestamp": "1767225600",
  "type": "text",
  "text": {
    "body": "Pode ser amanhã à tarde."
  },
  "context": {
    "from": "554430000000",
    "id": "wamid.HBgNNTUxMTk5OTk5OTk5ORUCABEYEjdGQjI0QzE4RTlBMDVENzQxAA=="
  }
}
JSON
{
  "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?"
    }
  }
}
JSON
{
  "from": "5511999999999",
  "id": "wamid.HBgNNTUxMTk5OTk5OTk5ORUCABIYFjNFQjBDNUE2RjQ3RjE4QjdBMz04AA==",
  "timestamp": "1767225600",
  "type": "image",
  "image": {
    "id": "1003383421387256",
    "mime_type": "image/jpeg",
    "sha256": "SfInY0gGKTsJlUWbwxC1k+FAD0FZHvzwfpvO0zX0GUI=",
    "caption": "Este aqui"
  }
}
JSON
{
  "from": "5511999999999",
  "id": "wamid.HBgNNTUxMTk5OTk5OTk5ORUCABIYFjNFQjBDNUE2RjQ3RjE4QjdBMz05AA==",
  "timestamp": "1767225600",
  "type": "audio",
  "audio": {
    "id": "1003383421387257",
    "mime_type": "audio/ogg; codecs=opus",
    "sha256": "SfInY0gGKTsJlUWbwxC1k+FAD0FZHvzwfpvO0zX0GUI=",
    "voice": true
  }
}
JSON
{
  "from": "5511999999999",
  "id": "wamid.HBgNNTUxMTk5OTk5OTk5ORUCABIYFjNFQjBDNUE2RjQ3RjE4QjdBMz06AA==",
  "timestamp": "1767225600",
  "type": "video",
  "video": {
    "id": "1003383421387258",
    "mime_type": "video/mp4",
    "sha256": "SfInY0gGKTsJlUWbwxC1k+FAD0FZHvzwfpvO0zX0GUI="
  }
}
JSON
{
  "from": "5511999999999",
  "id": "wamid.HBgNNTUxMTk5OTk5OTk5ORUCABIYFjNFQjBDNUE2RjQ3RjE4QjdBMz07AA==",
  "timestamp": "1767225600",
  "type": "document",
  "document": {
    "id": "1003383421387259",
    "mime_type": "application/pdf",
    "sha256": "SfInY0gGKTsJlUWbwxC1k+FAD0FZHvzwfpvO0zX0GUI=",
    "filename": "comprovante.pdf"
  }
}
JSON
{
  "from": "5511999999999",
  "id": "wamid.HBgNNTUxMTk5OTk5OTk5ORUCABIYFjNFQjBDNUE2RjQ3RjE4QjdBMz08AA==",
  "timestamp": "1767225600",
  "type": "sticker",
  "sticker": {
    "id": "1003383421387260",
    "mime_type": "image/webp",
    "sha256": "SfInY0gGKTsJlUWbwxC1k+FAD0FZHvzwfpvO0zX0GUI=",
    "animated": false
  }
}
JSON
{
  "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"
  }
}
JSON
{
  "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"
        }
      ]
    }
  ]
}
JSON
{
  "from": "5511999999999",
  "id": "wamid.HBgNNTUxMTk5OTk5OTk5ORUCABIYFjNFQjBDNUE2RjQ3RjE4QjdBMz11AA==",
  "timestamp": "1767225600",
  "type": "reaction",
  "reaction": {
    "message_id": "wamid.HBgNNTUxMTk5OTk5OTk5ORUCABEYEjdGQjI0QzE4RTlBMDVENzQxAA==",
    "emoji": "❤️"
  }
}
JSON
{
  "from": "5511999999999",
  "id": "wamid.HBgNNTUxMTk5OTk5OTk5ORUCABIYFjNFQjBDNUE2RjQ3RjE4QjdBMz12AA==",
  "timestamp": "1767225600",
  "type": "button",
  "context": {
    "from": "554430000000",
    "id": "wamid.HBgNNTUxMTk5OTk5OTk5ORUCABEYEjdGQjI0QzE4RTlBMDVENzQxAA=="
  },
  "button": {
    "payload": "CONFIRM_42",
    "text": "Confirmar"
  }
}
JSON
{
  "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"
    }
  }
}
JSON
{
  "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"
    }
  }
}
JSON
{
  "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\"}"
    }
  }
}
JSON
{
  "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"
      }
    ]
  }
}
JSON
{
  "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"
  }
}
JSON
{
  "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.

JSON
{
  "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"
                }
              }
            ]
          }
        }
      ]
    }
  ]
}
JSON
{
  "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"
  }
}
JSON
{
  "id": "wamid.HBgNNTUxMTk5OTk5OTk5ORUCABEYEjdGQjI0QzE4RTlBMDVENzQxAA==",
  "timestamp": "1767225600",
  "recipient_id": "5511999999999",
  "status": "delivered",
  "pricing": {
    "billable": true,
    "pricing_model": "PMP",
    "category": "utility"
  }
}
JSON
{
  "id": "wamid.HBgNNTUxMTk5OTk5OTk5ORUCABEYEjdGQjI0QzE4RTlBMDVENzQxAA==",
  "timestamp": "1767225600",
  "recipient_id": "5511999999999",
  "status": "read"
}
JSON
{
  "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/"
    }
  ]
}

Tratar os eventos#

Valide a assinatura, responda 2xx em até 5 s e processe depois (Webhooks). Depois, separe messages de statuses e despache por type:

Exemplo

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, e messages[].id também não muda.
  • Para responder, use o messages[].id em context.message_id (Enviar mensagens); para marcar como lida, o mesmo id com status: "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}/test envia um evento messages de exemplo.

Ficou com dúvida? Escreva para suporte@synergyconnect.com.br.