Skip to content

Roteamento HTTP

Rotas expostas pelo API Gateway e dispatch interno do Lambda.


Rotas

MétodoPathHandler internoDescrição
POST/webhookInboundWebhookHandler.handleRecebe mensagens (Z-API ou Meta Cloud API)
GET/webhookMetaVerificationHandler.handleHandshake de verificação do webhook da Meta (hub.challenge)
GET/healthhealthResponseHealth check para monitoria e smoke tests
POST/outboundOutboundHandler.handleEnvio proativo (chamado pelo nest-api)
qualquerqualquerRetorna 404 Not Found

O mesmo path /webhook atende os dois provedores: o POST detecta o provedor pelo formato do payload (isMetaPayloadobject: "whatsapp_business_account") e escolhe o mapper certo (mapMetaPayloadToIncomingMessage ou mapWebhookPayloadToIncomingMessage). Em produção o bot roda como container na VPS atrás do Caddy: https://wpp.contrasync.com/webhook. No deploy via CDK a URL base é exposta como CfnOutput.BotApiUrl.

Verificação do webhook da Meta (GET /webhook)

A Meta valida o callback enviando GET /webhook?hub.mode=subscribe&hub.challenge=...&hub.verify_token=.... O MetaVerificationHandler confere o hub.verify_token contra WHATSAPP_VERIFY_TOKEN e devolve o hub.challenge cru com 200 (text/plain); caso contrário, 403.


Dispatch em handler.ts

typescript
const route = (event: APIGatewayProxyEventV2) => {
  const path = event.rawPath ?? ''

  const method = event.requestContext?.http?.method ?? ''

  if (method === 'GET' && path.endsWith('/health')) {
    return Promise.resolve(healthResponse())
  }

  if (method === 'GET' && path.endsWith('/webhook')) {
    return Promise.resolve(metaVerificationHandler.handle(event))
  }

  if (method === 'POST' && path.endsWith('/webhook')) {
    return inboundHandler.handle(event)
  }

  if (method === 'POST' && path.endsWith('/outbound')) {
    return outboundHandler.handle(event)
  }

  return Promise.resolve({
    statusCode: 404,
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ error: 'Not found' })
  })
}

O uso de endsWith permite que o stage do API Gateway (/$default, /dev, etc) seja anexado sem quebrar o roteamento.


Respostas Padronizadas

CenárioStatusBody
Health OK200{ "status": "ok", "service": "contrasync-whatsapp-bot" }
Webhook processado200{ "ok": true }
Webhook ignorado (sem messageId/phone)200{ "ignored": true }
Webhook do próprio bot (fromMe)200{ "ignored": "self" }
Body vazio400{ "error": "Webhook body vazio" } (ValidationError)
Verificação Meta OK200<hub.challenge> (text/plain)
Verificação Meta rejeitada403{ "error": "Forbidden" }
Z-API indisponível502{ "error": "Falha ao enviar mensagem via Z-API" } (BotError)
Meta indisponível502{ "error": "Falha ao enviar mensagem via Meta" } (BotError)
Rota inválida404{ "error": "Not found" }
Erro inesperado500{ "error": "Internal server error" } (mascarado)

Todas as respostas incluem Content-Type: application/json exceto a { "ok": true } (que omite headers, o API Gateway aplica content-type padrão).


Por Que Sempre Status 200 em Webhooks Ignorados

O Z-API retransmite mensagens que recebem status >= 400. Para evitar loops em payloads inválidos (sem messageId, sem phone, ou fromMe), respondemos 200 com ignored no body. O Z-API marca a entrega como bem-sucedida e não tenta de novo.

400 só é retornado quando o body está realmente vazio, sinaliza configuração errada do webhook, não um problema com a mensagem.


Documento atualizado em Maio 2026