Skip to content

Integração WhatsApp Cloud API (Meta)

Contrato HTTP da Cloud API oficial da Meta: verificação do webhook, payload de entrada, envio via Graph API e roteamento por provedor.


Visão Geral

O bot suporta dois provedores de WhatsApp em paralelo, atrás do mesmo endpoint /webhook:

  • Z-API (z-api.io) — ver zapi-integration.md
  • WhatsApp Cloud API (Meta) — API oficial da Meta (graph.facebook.com)

O provedor de cada mensagem é detectado pelo formato do payload e propagado no campo IncomingMessage.provider ("zapi" | "meta"). A resposta sai pelo mesmo provedor de onde a mensagem entrou.

As credenciais da Meta ficam em variáveis de ambiente: WHATSAPP_VERIFY_TOKEN (verificação), META_PHONE_NUMBER_ID, META_ACCESS_TOKEN, META_GRAPH_BASE_URL (default https://graph.facebook.com) e META_GRAPH_VERSION (default v21.0).


Verificação do Webhook (GET /webhook)

Ao salvar o webhook no painel da Meta, ela envia:

GET /webhook?hub.mode=subscribe&hub.challenge=<random>&hub.verify_token=<token>

MetaVerificationHandler (src/modules/gateway/handlers/meta-verification.handler.ts):

  1. Confere hub.mode === "subscribe" e hub.verify_token === WHATSAPP_VERIFY_TOKEN
  2. Em caso positivo, responde 200 com o hub.challenge cru (text/plain)
  3. Caso contrário, 403

No painel da Meta, o Verify token precisa ser idêntico ao WHATSAPP_VERIFY_TOKEN do ambiente. Sem isso, a validação do callback falha com "The callback URL or verify token couldn't be validated".


Webhook de Entrada (POST /webhook)

Payload (MetaWebhookPayload)

Arquivo: src/modules/gateway/adapters/meta/meta.types.ts

typescript
interface MetaWebhookPayload {
  object?: string                     // 'whatsapp_business_account'
  entry?: {
    id?: string
    changes?: {
      field?: string
      value?: {
        messaging_product?: string    // 'whatsapp'
        messages?: MetaMessage[]      // mensagens recebidas
        statuses?: unknown[]          // recibos de entrega/leitura (ignorados)
      }
    }[]
  }[]
}

Cada MetaMessage traz id, from (telefone, sem +), timestamp (unix em segundos), type e o conteúdo (text.body, image.caption, video.caption, ...).

Detecção de provedor

isMetaPayload (src/modules/gateway/helpers/is-meta-payload.ts) retorna true quando body.object === "whatsapp_business_account". O InboundWebhookHandler usa isso para escolher o mapper:

  • Meta → mapMetaPayloadToIncomingMessage
  • Z-API → mapWebhookPayloadToIncomingMessage

Mapeamento (mapMetaPayloadToIncomingMessage)

Arquivo: src/modules/gateway/adapters/meta/meta.mapper.ts. Produz o mesmo IncomingMessage que o Z-API, com provider: "meta". Retorna null (webhook ignorado com 200) quando:

  • não há entry[0].changes[0].value.messages[0] (ex.: webhook só de statuses)
  • a mensagem não tem id ou from

Limitação atual: apenas a primeira mensagem do batch é processada. A Meta pode agrupar várias mensagens num único webhook.


Envio de Mensagens (MetaClient)

Arquivo: src/modules/gateway/adapters/meta/meta.client.ts. Implementa MessagingClient (mesma interface do ZapiClient).

POST {META_GRAPH_BASE_URL}/{META_GRAPH_VERSION}/{META_PHONE_NUMBER_ID}/messages
Authorization: Bearer {META_ACCESS_TOKEN}

{ "messaging_product": "whatsapp", "recipient_type": "individual",
  "to": "<phone>", "type": "text", "text": { "body": "<msg>", "preview_url": false } }

A resposta ({ messages: [{ id }] }) vira SentMessageReceipt { messageId }. Falha de rede → BotError 502.


Roteamento por Provedor (MessagingGateway)

Arquivo: src/modules/gateway/services/messaging-gateway.service.ts. É o MessagingClient injetado em todos os flows/handlers no lugar do client concreto.

  • No inbound, o handler chama rememberProvider(phone, incoming.provider)
  • No sendText, o gateway resolve o provedor pelo telefone (mapa phone → provider) e delega ao client certo
  • Telefone sem provedor lembrado cai no default ("zapi"), o que mantém o envio proativo (/outbound) compatível

Limitação atual: o mapa phone → provider é em memória (uma instância, reseta no restart). A resposta síncrona à mensagem recebida sempre acerta o provedor; o envio proativo usa o default. Persistir o provedor na sessão é um follow-up.


Pendências / Follow-ups

  • Verificação de assinatura (X-Hub-Signature-256 com META_APP_SECRET) ainda não implementada — o POST /webhook da Meta não é autenticado (mesmo patamar do Z-API hoje)
  • Processar todas as mensagens de um batch
  • Persistir phone → provider (sessão) para roteamento proativo correto

Documento criado em Junho 2026