New skills covering 10 categories: **Security & Audit**: 007 (STRIDE/PASTA/OWASP), cred-omega (secrets management) **AI Personas**: Karpathy, Hinton, Sutskever, LeCun (4 sub-skills), Altman, Musk, Gates, Jobs, Buffett **Multi-agent Orchestration**: agent-orchestrator, task-intelligence, multi-advisor **Code Analysis**: matematico-tao (Terence Tao-inspired mathematical code analysis) **Social & Messaging**: Instagram Graph API, Telegram Bot, WhatsApp Cloud API, social-orchestrator **Image Generation**: AI Studio (Gemini), Stability AI, ComfyUI Gateway, image-studio router **Brazilian Domain**: 6 auction specialist modules, 2 legal advisors, auctioneers data scraper **Product & Growth**: design, invention, monetization, analytics, growth engine **DevOps & LLM Ops**: Docker/CI-CD/AWS, RAG/embeddings/fine-tuning **Skill Governance**: installer, sentinel auditor, context management Each skill includes: - Standardized YAML frontmatter (name, description, risk, source, tags, tools) - Structured sections (Overview, When to Use, How it Works, Best Practices) - Python scripts and reference documentation where applicable - Cross-platform compatibility (Claude Code, Antigravity, Cursor, Gemini CLI, Codex CLI) Co-authored-by: ProgramadorBrasil <214873561+ProgramadorBrasil@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
604 lines
17 KiB
Markdown
604 lines
17 KiB
Markdown
# Configuracao de Webhooks - WhatsApp Cloud API
|
|
|
|
> Guia completo para configurar, validar e proteger webhooks da WhatsApp Cloud API.
|
|
|
|
---
|
|
|
|
## 1. Visao Geral
|
|
|
|
Webhooks sao callbacks HTTP que a Meta envia para o seu servidor sempre que um evento
|
|
ocorre na sua conta do WhatsApp Business. Sem webhooks, voce nao recebe mensagens,
|
|
confirmacoes de entrega nem atualizacoes de status em tempo real.
|
|
|
|
**Requisitos obrigatorios:**
|
|
|
|
| Requisito | Detalhe |
|
|
|-----------|---------|
|
|
| Protocolo | HTTPS com certificado SSL valido (nao aceita auto-assinado) |
|
|
| Resposta | HTTP 200 OK em ate **5 segundos** |
|
|
| Disponibilidade | Endpoint deve estar acessivel publicamente na internet |
|
|
| Idempotencia | A Meta pode reenviar o mesmo evento; trate duplicatas |
|
|
|
|
Se o seu servidor nao responder 200 dentro de 5 segundos, a Meta reenvia o evento
|
|
com backoff exponencial por ate 7 dias. Apos esse periodo, o webhook e desativado
|
|
automaticamente.
|
|
|
|
---
|
|
|
|
## 2. Configuracao no Meta Developers
|
|
|
|
### Passo a passo
|
|
|
|
1. Acesse [developers.facebook.com](https://developers.facebook.com)
|
|
2. Selecione seu App
|
|
3. No menu lateral: **WhatsApp > Configuration**
|
|
4. Na secao **Webhook**, clique em **Edit**
|
|
|
|
### Campos obrigatorios
|
|
|
|
| Campo | Descricao |
|
|
|-------|-----------|
|
|
| **Callback URL** | URL HTTPS do seu servidor (ex: `https://api.seudominio.com/webhook`) |
|
|
| **Verify Token** | String secreta que voce define (ex: `meu_token_secreto_2024`) |
|
|
|
|
### Campos para inscrever (Webhook Fields)
|
|
|
|
Marque pelo menos:
|
|
|
|
- **messages** - Mensagens recebidas, status de entrega, leitura
|
|
|
|
Campos opcionais uteis:
|
|
|
|
- **message_template_status_update** - Aprovacao/rejeicao de templates
|
|
- **account_update** - Alteracoes na conta Business
|
|
|
|
> **IMPORTANTE:** O Verify Token NAO e o mesmo que o Access Token da API.
|
|
> Escolha um valor forte e unico, e armazene-o como variavel de ambiente.
|
|
|
|
---
|
|
|
|
## 3. Verificacao de Webhook (GET)
|
|
|
|
Quando voce salva a configuracao no painel da Meta, ela envia um GET request para
|
|
validar que o endpoint pertence a voce. Esse fluxo e chamado de **challenge-response**.
|
|
|
|
### Fluxo de verificacao
|
|
|
|
```
|
|
Meta Seu Servidor
|
|
| |
|
|
| GET /webhook? |
|
|
| hub.mode=subscribe |
|
|
| hub.verify_token=SEU_TOKEN |
|
|
| hub.challenge=RANDOM_STRING |
|
|
| ---------------------------->> |
|
|
| | 1. Verifica hub.verify_token
|
|
| | 2. Se valido, retorna hub.challenge
|
|
| <<---------------------------- |
|
|
| HTTP 200 + challenge como body |
|
|
```
|
|
|
|
### Node.js / Express
|
|
|
|
```javascript
|
|
// GET /webhook - Verification endpoint
|
|
app.get('/webhook', (req, res) => {
|
|
const VERIFY_TOKEN = process.env.WEBHOOK_VERIFY_TOKEN;
|
|
|
|
const mode = req.query['hub.mode'];
|
|
const token = req.query['hub.verify_token'];
|
|
const challenge = req.query['hub.challenge'];
|
|
|
|
if (mode === 'subscribe' && token === VERIFY_TOKEN) {
|
|
console.log('Webhook verified successfully');
|
|
return res.status(200).send(challenge);
|
|
}
|
|
|
|
console.error('Webhook verification failed: invalid token');
|
|
return res.sendStatus(403);
|
|
});
|
|
```
|
|
|
|
### Python / Flask
|
|
|
|
```python
|
|
# GET /webhook - Verification endpoint
|
|
@app.route('/webhook', methods=['GET'])
|
|
def verify_webhook():
|
|
verify_token = os.environ.get('WEBHOOK_VERIFY_TOKEN')
|
|
|
|
mode = request.args.get('hub.mode')
|
|
token = request.args.get('hub.verify_token')
|
|
challenge = request.args.get('hub.challenge')
|
|
|
|
if mode == 'subscribe' and token == verify_token:
|
|
print('Webhook verified successfully')
|
|
return challenge, 200
|
|
|
|
print('Webhook verification failed: invalid token')
|
|
return 'Forbidden', 403
|
|
```
|
|
|
|
### Erros comuns na verificacao
|
|
|
|
| Erro | Causa | Solucao |
|
|
|------|-------|---------|
|
|
| 403 Forbidden | Verify token nao confere | Verifique a variavel de ambiente |
|
|
| Webhook nao valida | Challenge retornado como JSON | Retorne como **plain text**, nao JSON |
|
|
| Timeout | Servidor demorou mais de 5s | Verifique latencia e middleware |
|
|
| SSL Error | Certificado invalido ou expirado | Use Let's Encrypt ou certificado valido |
|
|
|
|
> **Erro classico:** Retornar `res.json({ challenge })` em vez de `res.send(challenge)`.
|
|
> A Meta espera o challenge como texto puro no body da resposta.
|
|
|
|
---
|
|
|
|
## 4. Recebimento de Mensagens (POST)
|
|
|
|
Apos a verificacao, a Meta envia eventos via POST. Cada payload segue a mesma
|
|
estrutura base, mas o conteudo varia conforme o tipo de evento.
|
|
|
|
### Node.js / Express - Handler completo
|
|
|
|
```javascript
|
|
// POST /webhook - Receive events
|
|
app.post('/webhook', (req, res) => {
|
|
// SEMPRE responda 200 imediatamente
|
|
res.sendStatus(200);
|
|
|
|
const body = req.body;
|
|
|
|
if (!body.object || !body.entry) return;
|
|
|
|
for (const entry of body.entry) {
|
|
for (const change of entry.changes) {
|
|
if (change.field !== 'messages') continue;
|
|
|
|
const value = change.value;
|
|
const metadata = value.metadata;
|
|
const phoneNumberId = metadata.phone_number_id;
|
|
|
|
// Status updates (sent, delivered, read, failed)
|
|
if (value.statuses) {
|
|
for (const status of value.statuses) {
|
|
handleStatusUpdate(status);
|
|
}
|
|
}
|
|
|
|
// Incoming messages
|
|
if (value.messages) {
|
|
for (const message of value.messages) {
|
|
const from = message.from;
|
|
const timestamp = message.timestamp;
|
|
|
|
switch (message.type) {
|
|
case 'text':
|
|
handleTextMessage(from, message.text.body, phoneNumberId);
|
|
break;
|
|
case 'image':
|
|
case 'video':
|
|
case 'audio':
|
|
case 'document':
|
|
handleMediaMessage(from, message.type, message[message.type]);
|
|
break;
|
|
case 'interactive':
|
|
handleInteractiveResponse(from, message.interactive);
|
|
break;
|
|
case 'button':
|
|
handleButtonResponse(from, message.button);
|
|
break;
|
|
case 'location':
|
|
handleLocationMessage(from, message.location);
|
|
break;
|
|
default:
|
|
console.log(`Unhandled message type: ${message.type}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
```
|
|
|
|
### Python / Flask - Handler completo
|
|
|
|
```python
|
|
# POST /webhook - Receive events
|
|
@app.route('/webhook', methods=['POST'])
|
|
def receive_webhook():
|
|
body = request.get_json()
|
|
|
|
if not body or 'entry' not in body:
|
|
return 'OK', 200
|
|
|
|
for entry in body.get('entry', []):
|
|
for change in entry.get('changes', []):
|
|
if change.get('field') != 'messages':
|
|
continue
|
|
|
|
value = change.get('value', {})
|
|
metadata = value.get('metadata', {})
|
|
phone_number_id = metadata.get('phone_number_id')
|
|
|
|
# Status updates
|
|
for status in value.get('statuses', []):
|
|
handle_status_update(status)
|
|
|
|
# Incoming messages
|
|
for message in value.get('messages', []):
|
|
sender = message['from']
|
|
msg_type = message['type']
|
|
|
|
if msg_type == 'text':
|
|
handle_text_message(sender, message['text']['body'], phone_number_id)
|
|
elif msg_type in ('image', 'video', 'audio', 'document'):
|
|
handle_media_message(sender, msg_type, message[msg_type])
|
|
elif msg_type == 'interactive':
|
|
handle_interactive_response(sender, message['interactive'])
|
|
elif msg_type == 'button':
|
|
handle_button_response(sender, message['button'])
|
|
elif msg_type == 'location':
|
|
handle_location_message(sender, message['location'])
|
|
|
|
return 'OK', 200
|
|
```
|
|
|
|
### Exemplos de payload por tipo de evento
|
|
|
|
**Mensagem de texto recebida:**
|
|
```json
|
|
{
|
|
"messages": [{
|
|
"from": "5511999887766",
|
|
"id": "wamid.HBgNNTUxMTk5OTg...",
|
|
"timestamp": "1677000000",
|
|
"type": "text",
|
|
"text": { "body": "Ola, preciso de ajuda" }
|
|
}]
|
|
}
|
|
```
|
|
|
|
**Resposta de botao interativo (list/button reply):**
|
|
```json
|
|
{
|
|
"messages": [{
|
|
"from": "5511999887766",
|
|
"type": "interactive",
|
|
"interactive": {
|
|
"type": "button_reply",
|
|
"button_reply": {
|
|
"id": "btn_confirm",
|
|
"title": "Confirmar pedido"
|
|
}
|
|
}
|
|
}]
|
|
}
|
|
```
|
|
|
|
**Atualizacao de status (entrega):**
|
|
```json
|
|
{
|
|
"statuses": [{
|
|
"id": "wamid.HBgNNTUxMTk5OTg...",
|
|
"status": "delivered",
|
|
"timestamp": "1677000030",
|
|
"recipient_id": "5511999887766"
|
|
}]
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 5. Seguranca HMAC-SHA256 (CRITICO)
|
|
|
|
### Por que e essencial
|
|
|
|
Sem validacao de assinatura, qualquer pessoa que descubra a URL do seu webhook pode
|
|
enviar payloads falsos. Isso permite:
|
|
|
|
- **Spoofing de mensagens** - Simular que um cliente enviou algo que nunca enviou
|
|
- **Execucao de comandos** - Se o webhook dispara acoes (pagamentos, envios), atacantes controlam
|
|
- **Exfiltracao de dados** - Payloads maliciosos podem explorar falhas de parsing
|
|
|
|
> **Incidente real documentado:** Uma empresa de e-commerce sofreu prejuizo de US$ 847 mil
|
|
> apos atacantes enviarem payloads falsos de "confirmacao de pagamento" para o webhook
|
|
> que nao validava assinatura, disparando envios de mercadoria sem pagamento real.
|
|
|
|
### Como funciona
|
|
|
|
A cada request POST, a Meta inclui o header `X-Hub-Signature-256` contendo:
|
|
|
|
```
|
|
sha256=<hmac-sha256-hex-digest>
|
|
```
|
|
|
|
O HMAC e calculado usando o **App Secret** como chave e o **raw body** como mensagem.
|
|
|
|
### Passo a passo da validacao
|
|
|
|
```
|
|
1. Capture o raw body ANTES do JSON parsing
|
|
2. Extraia o header X-Hub-Signature-256
|
|
3. Compute HMAC-SHA256(app_secret, raw_body)
|
|
4. Compare usando funcao constant-time (previne timing attack)
|
|
5. Se nao bater, rejeite com 401
|
|
```
|
|
|
|
### Node.js / Express - Middleware de validacao
|
|
|
|
```javascript
|
|
const crypto = require('crypto');
|
|
|
|
function validateWebhookSignature(req, res, next) {
|
|
const APP_SECRET = process.env.META_APP_SECRET;
|
|
|
|
// CRITICO: raw body deve ser capturado ANTES do json parser
|
|
// Configure o Express assim:
|
|
// app.use(express.json({
|
|
// verify: (req, _res, buf) => { req.rawBody = buf; }
|
|
// }));
|
|
|
|
const signature = req.headers['x-hub-signature-256'];
|
|
|
|
if (!signature) {
|
|
console.error('Missing X-Hub-Signature-256 header');
|
|
return res.sendStatus(401);
|
|
}
|
|
|
|
const expectedSignature = 'sha256=' + crypto
|
|
.createHmac('sha256', APP_SECRET)
|
|
.update(req.rawBody)
|
|
.digest('hex');
|
|
|
|
const signatureBuffer = Buffer.from(signature);
|
|
const expectedBuffer = Buffer.from(expectedSignature);
|
|
|
|
if (signatureBuffer.length !== expectedBuffer.length ||
|
|
!crypto.timingSafeEqual(signatureBuffer, expectedBuffer)) {
|
|
console.error('Invalid webhook signature');
|
|
return res.sendStatus(401);
|
|
}
|
|
|
|
next();
|
|
}
|
|
|
|
// Uso:
|
|
// app.post('/webhook', validateWebhookSignature, webhookHandler);
|
|
```
|
|
|
|
### Python / Flask - Decorator de validacao
|
|
|
|
```python
|
|
import hmac
|
|
import hashlib
|
|
from functools import wraps
|
|
|
|
def validate_webhook_signature(f):
|
|
@wraps(f)
|
|
def decorated(*args, **kwargs):
|
|
app_secret = os.environ.get('META_APP_SECRET')
|
|
|
|
# CRITICO: raw body ANTES do JSON parsing
|
|
raw_body = request.get_data()
|
|
|
|
signature = request.headers.get('X-Hub-Signature-256', '')
|
|
|
|
if not signature:
|
|
print('Missing X-Hub-Signature-256 header')
|
|
return 'Unauthorized', 401
|
|
|
|
expected = 'sha256=' + hmac.new(
|
|
app_secret.encode('utf-8'),
|
|
raw_body,
|
|
hashlib.sha256
|
|
).hexdigest()
|
|
|
|
if not hmac.compare_digest(signature, expected):
|
|
print('Invalid webhook signature')
|
|
return 'Unauthorized', 401
|
|
|
|
return f(*args, **kwargs)
|
|
return decorated
|
|
|
|
# Uso:
|
|
# @app.route('/webhook', methods=['POST'])
|
|
# @validate_webhook_signature
|
|
# def receive_webhook():
|
|
# ...
|
|
```
|
|
|
|
### Erro classico: usar body parseado
|
|
|
|
```javascript
|
|
// ERRADO - body ja foi parseado para JSON, altera o conteudo
|
|
const hmac = crypto.createHmac('sha256', secret)
|
|
.update(JSON.stringify(req.body)) // NAO FACA ISSO
|
|
.digest('hex');
|
|
|
|
// CORRETO - usar o raw body original
|
|
const hmac = crypto.createHmac('sha256', secret)
|
|
.update(req.rawBody) // Buffer original do request
|
|
.digest('hex');
|
|
```
|
|
|
|
> **Por que falha:** `JSON.stringify(JSON.parse(raw))` pode produzir output diferente
|
|
> do raw original (espacamento, ordem de chaves, encoding de caracteres Unicode).
|
|
> A assinatura da Meta foi calculada sobre o raw body exato.
|
|
|
|
---
|
|
|
|
## 6. Desenvolvimento Local
|
|
|
|
Para testar webhooks localmente, voce precisa expor seu servidor local para a internet.
|
|
A ferramenta mais usada para isso e o **ngrok**.
|
|
|
|
### Instalacao e uso do ngrok
|
|
|
|
```bash
|
|
# Instalar (macOS)
|
|
brew install ngrok
|
|
|
|
# Instalar (Windows via Chocolatey)
|
|
choco install ngrok
|
|
|
|
# Instalar (Linux)
|
|
snap install ngrok
|
|
|
|
# Autenticar (necessario uma vez)
|
|
ngrok config add-authtoken SEU_AUTH_TOKEN
|
|
|
|
# Expor porta local 3000
|
|
ngrok http 3000
|
|
```
|
|
|
|
### Saida do ngrok
|
|
|
|
```
|
|
Session Status online
|
|
Forwarding https://a1b2c3d4.ngrok-free.app -> http://localhost:3000
|
|
```
|
|
|
|
### Configurar no Meta Developers
|
|
|
|
1. Copie a URL HTTPS do ngrok (ex: `https://a1b2c3d4.ngrok-free.app`)
|
|
2. No painel da Meta, atualize o Callback URL para: `https://a1b2c3d4.ngrok-free.app/webhook`
|
|
3. Salve e valide
|
|
|
|
> **ATENCAO:** A URL do ngrok muda a cada reinicio (no plano gratuito).
|
|
> Voce precisara atualizar no painel da Meta toda vez que reiniciar o ngrok.
|
|
|
|
### Debugging de payloads
|
|
|
|
```bash
|
|
# O painel web do ngrok mostra todos os requests
|
|
# Acesse: http://127.0.0.1:4040
|
|
|
|
# Alternativa: log detalhado no servidor
|
|
app.post('/webhook', (req, res) => {
|
|
console.log('Headers:', JSON.stringify(req.headers, null, 2));
|
|
console.log('Body:', JSON.stringify(req.body, null, 2));
|
|
res.sendStatus(200);
|
|
});
|
|
```
|
|
|
|
### Dicas para desenvolvimento local
|
|
|
|
- Use `ngrok http 3000 --log=stdout` para ver logs no terminal
|
|
- O Inspector web (`http://127.0.0.1:4040`) permite **replay** de requests
|
|
- Adicione um endpoint `/health` para verificar rapidamente se o servidor esta no ar
|
|
- Considere usar **localtunnel** como alternativa gratuita ao ngrok
|
|
|
|
---
|
|
|
|
## 7. Deploy em Producao
|
|
|
|
### Requisitos de certificado HTTPS
|
|
|
|
- Certificado SSL valido emitido por CA reconhecida
|
|
- **Let's Encrypt** e aceito e gratuito
|
|
- Certificados auto-assinados **NAO** sao aceitos
|
|
- Certifique-se de que a cadeia completa (chain) esta configurada
|
|
- Configure renovacao automatica (certbot renew via cron)
|
|
|
|
### Retry logic e idempotencia
|
|
|
|
A Meta reenvia eventos com backoff exponencial quando nao recebe HTTP 200:
|
|
|
|
| Tentativa | Intervalo aproximado |
|
|
|-----------|---------------------|
|
|
| 1a | Imediato |
|
|
| 2a | ~1 minuto |
|
|
| 3a | ~5 minutos |
|
|
| 4a | ~30 minutos |
|
|
| Seguintes | Backoff crescente ate 7 dias |
|
|
|
|
**Implemente idempotencia:**
|
|
|
|
```javascript
|
|
const processedMessages = new Set(); // Em producao, use Redis
|
|
|
|
function isNewMessage(messageId) {
|
|
if (processedMessages.has(messageId)) {
|
|
return false;
|
|
}
|
|
processedMessages.add(messageId);
|
|
|
|
// Limpar mensagens antigas apos 24h (em producao, use TTL do Redis)
|
|
setTimeout(() => processedMessages.delete(messageId), 86400000);
|
|
return true;
|
|
}
|
|
|
|
// No handler:
|
|
if (!isNewMessage(message.id)) {
|
|
console.log(`Duplicate message ${message.id}, skipping`);
|
|
return;
|
|
}
|
|
```
|
|
|
|
### Scaling e capacidade
|
|
|
|
A Meta recomenda que o seu servidor suporte:
|
|
|
|
| Metrica | Recomendacao |
|
|
|---------|-------------|
|
|
| Capacidade de entrada | **3x** o volume de mensagens enviadas + **1x** mensagens recebidas |
|
|
| Tempo de resposta | < 5 segundos (idealmente < 1 segundo) |
|
|
| Disponibilidade | 99.9% uptime minimo |
|
|
|
|
**Arquitetura recomendada para alto volume:**
|
|
|
|
```
|
|
[Meta Webhook] --> [Load Balancer]
|
|
|
|
|
[Web Server] --> Responde 200 imediatamente
|
|
|
|
|
[Message Queue] (Redis/SQS/RabbitMQ)
|
|
|
|
|
[Workers] --> Processamento assincrono
|
|
```
|
|
|
|
### Monitoramento de saude do webhook
|
|
|
|
```javascript
|
|
// Endpoint de health check
|
|
app.get('/health', (req, res) => {
|
|
res.json({
|
|
status: 'healthy',
|
|
uptime: process.uptime(),
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
});
|
|
|
|
// Metricas essenciais para monitorar:
|
|
// - Taxa de erros 4xx/5xx no endpoint /webhook
|
|
// - Latencia media de resposta (deve ser < 1s)
|
|
// - Numero de mensagens duplicadas recebidas
|
|
// - Fila de processamento (tamanho e tempo medio)
|
|
// - Falhas de validacao HMAC (possivel ataque)
|
|
```
|
|
|
|
**Alertas recomendados:**
|
|
|
|
| Alerta | Threshold | Acao |
|
|
|--------|-----------|------|
|
|
| Latencia alta | > 3 segundos | Investigar gargalos, escalar workers |
|
|
| Taxa de erro | > 1% | Verificar logs, possivel bug no handler |
|
|
| Falha HMAC | > 0 por hora | Possivel ataque; verificar APP_SECRET |
|
|
| Fila crescendo | > 1000 mensagens | Escalar workers de processamento |
|
|
| Webhook desativado | Alerta da Meta | Verificar SSL e disponibilidade |
|
|
|
|
---
|
|
|
|
## Checklist Final
|
|
|
|
- [ ] Endpoint acessivel via HTTPS com certificado valido
|
|
- [ ] Verificacao GET retorna challenge como plain text
|
|
- [ ] Handler POST responde 200 em menos de 5 segundos
|
|
- [ ] Validacao HMAC-SHA256 implementada com raw body
|
|
- [ ] Comparacao constant-time (timingSafeEqual / compare_digest)
|
|
- [ ] Idempotencia para mensagens duplicadas
|
|
- [ ] Processamento assincrono para operacoes demoradas
|
|
- [ ] Monitoramento e alertas configurados
|
|
- [ ] APP_SECRET e VERIFY_TOKEN em variaveis de ambiente (nunca no codigo)
|
|
- [ ] Logs estruturados para debugging em producao
|