Appearance
Roteamento HTTP
Rotas expostas pelo API Gateway e dispatch interno do Lambda.
Rotas
| Método | Path | Handler interno | Descrição |
|---|---|---|---|
POST | /webhook | InboundWebhookHandler.handle | Recebe mensagens (Z-API ou Meta Cloud API) |
GET | /webhook | MetaVerificationHandler.handle | Handshake de verificação do webhook da Meta (hub.challenge) |
GET | /health | healthResponse | Health check para monitoria e smoke tests |
POST | /outbound | OutboundHandler.handle | Envio proativo (chamado pelo nest-api) |
| qualquer | qualquer | Retorna 404 Not Found |
O mesmo path /webhook atende os dois provedores: o POST detecta o provedor pelo formato do payload (isMetaPayload → object: "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ário | Status | Body |
|---|---|---|
| Health OK | 200 | { "status": "ok", "service": "contrasync-whatsapp-bot" } |
| Webhook processado | 200 | { "ok": true } |
| Webhook ignorado (sem messageId/phone) | 200 | { "ignored": true } |
| Webhook do próprio bot (fromMe) | 200 | { "ignored": "self" } |
| Body vazio | 400 | { "error": "Webhook body vazio" } (ValidationError) |
| Verificação Meta OK | 200 | <hub.challenge> (text/plain) |
| Verificação Meta rejeitada | 403 | { "error": "Forbidden" } |
| Z-API indisponível | 502 | { "error": "Falha ao enviar mensagem via Z-API" } (BotError) |
| Meta indisponível | 502 | { "error": "Falha ao enviar mensagem via Meta" } (BotError) |
| Rota inválida | 404 | { "error": "Not found" } |
| Erro inesperado | 500 | { "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