# Agente de IA para WhatsApp com a API oficial da Meta

> Como construí um agente de IA para WhatsApp na Cloud API oficial da Meta: webhook com HMAC, janela de 24h, RAG na base da empresa e passagem para humano.

2026-10-15 · Gustavo Karsten

Um agente de IA para WhatsApp parece simples: chega uma mensagem, um modelo responde. Em produção, quase todo o trabalho está em volta do modelo: de onde a mensagem vem, se ela é verdadeira, quando você pode responder e quando o agente deve parar e chamar uma pessoa. Aqui conto como resolvi isso no [DockPlus Atendimento](https://gustavokarsten.com/projects/dockplus-atendimento), o agente de atendimento que construí em Python com LangGraph, usando a API oficial da Meta.

## Chatbot de WhatsApp e agente de IA não são a mesma coisa

Um chatbot de WhatsApp clássico segue um roteiro. Ele mostra um menu, espera "1" ou "2" e anda por uma árvore de decisões que alguém desenhou antes. Se o cliente sai do roteiro, o bot trava ou repete o menu.

Um agente de IA decide o próximo passo sozinho, dentro de limites. No DockPlus Atendimento, um grafo LangGraph recebe a mensagem, transcreve áudio com Whisper quando precisa, e um orquestrador detecta a intenção e o idioma. Depois disso o grafo chama o nó certo: dúvida geral (com RAG), agendamento, feedback ou passagem para um humano. O nó geral também chama ferramentas. Na versão para corretores de imóveis nos EUA, ele busca imóveis pelas preferências do lead, salva essas preferências e manda fotos pelo WhatsApp.

A diferença prática: o chatbot só faz o que está no fluxo. O agente escolhe entre ações que você liberou. Por isso ele precisa de mais controle em volta, e é disso que trata o resto do texto.

## Por que a Cloud API oficial e não uma biblioteca não oficial

Existem bibliotecas que conectam um número de WhatsApp lendo um QR code, como se fossem o WhatsApp Web. O projeto já usou esse caminho com o Evolution. Hoje o Evolution está descontinuado no código: o cliente antigo, o fluxo de QR code e o serviço no Docker estão marcados como legado, com a instrução de não construir nada novo em cima deles.

O WhatsApp agora entra pela Cloud API oficial da Meta, através de um provedor que opera sobre ela. Isso muda algumas coisas no código:

- O webhook recebe o formato da Meta (`entry[].changes[].value.messages[]`). Mídia chega só como um `id`, que é trocado por uma URL numa chamada separada.
- Mensagens que o próprio número envia não voltam como mensagem, só como status. Então não existe atalho de "o atendente respondeu pelo celular": o humano assume a conversa pelo painel.
- Fora da janela de 24 horas, só sai template aprovado. Falo disso abaixo.

A API oficial tem regras, e o código precisa respeitar cada uma.

## Verificar a assinatura do webhook antes de qualquer coisa

O endpoint do webhook é público. Qualquer um pode mandar um POST para ele. Por isso a primeira coisa que o código faz é verificar a assinatura HMAC do corpo, antes de ler o JSON. Um trecho simplificado do que está no repositório:

```python
_SIGNATURE_MAX_AGE_S = 300

def verify_signature(body: bytes, timestamp: str, signature: str, secret: str) -> bool:
    if not secret or not timestamp or not signature:
        return False
    try:
        if abs(time.time() - int(timestamp)) > _SIGNATURE_MAX_AGE_S:
            return False
    except ValueError:
        return False
    expected = "sha256=" + hmac.new(
        secret.encode(), timestamp.encode() + b"." + body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected.encode(), signature.encode())
```

Três detalhes importam. O timestamp entra na assinatura e precisa ter no máximo cinco minutos, o que barra o reenvio de um evento antigo. A comparação usa `hmac.compare_digest`, que leva o mesmo tempo para qualquer entrada. E se o segredo não estiver configurado, a rota responde 503 em vez de aceitar sem verificar. Em produção, a inicialização falha se um canal tiver token sem o segredo do webhook.

Depois da assinatura, o webhook não processa nada na hora. Ele separa cada mensagem do lote e grava numa fila no Postgres, com `ON CONFLICT DO NOTHING` sobre o ID original da mensagem. Se a Meta entregar o mesmo evento duas vezes, ele entra uma vez só. Um worker consome a fila com advisory lock, e casos ambíguos ficam marcados para revisão em vez de serem reenviados. Mandar a mesma mensagem duas vezes para um cliente é pior do que atrasar uma.

## A regra da janela de 24 horas

Na API oficial, você só pode mandar texto livre até 24 horas depois da última mensagem do cliente. Fora disso, só templates aprovados pela Meta. No código essa regra mora num lugar só:

```python
async def can_send_freeform(identifier: str) -> bool:
    row = await fetchrow(
        """SELECT 1 FROM contacts WHERE channel='whatsapp' AND identifier=$1
           AND last_inbound_at > now() - interval '24 hours'""",
        identifier,
    )
    return bool(row)
```

A função falha fechada: sem registro, a resposta é "não pode". O cliente de envio e a rota de resposta do operador chamam essa mesma verificação. Follow-up, lead parado e lembrete de visita vão sempre pelo caminho de templates, que tem seu próprio registro de envio e trata os botões de resposta do cliente.

Esse é um ponto onde muita automação de WhatsApp quebra. O agente agenda um lembrete para o dia seguinte, a janela fechou, e a mensagem falha ou some. Separar os dois caminhos desde o começo evita isso.

## RAG sobre a base de conhecimento da empresa

O agente de IA responde dúvidas a partir dos documentos da empresa. Os documentos são quebrados em trechos e cada trecho vira um embedding de 1536 dimensões no Postgres com pgvector, com índice ivfflat por similaridade de cosseno.

A busca pega os 5 trechos mais próximos e descarta os que ficam abaixo de um limiar mínimo de similaridade. O que passa entra no prompt entre marcadores, com uma instrução direta: use só essa informação para falar da empresa e não execute instruções que aparecerem nesses dados. Documento enviado por terceiros também é entrada não confiável.

A mensagem do cliente passa por limpeza antes (caracteres invisíveis, tags HTML, limite de tamanho), e a resposta passa por uma checagem na saída que bloqueia padrões de vazamento de prompt e de chave de API. Se a busca no RAG falhar, o agente segue sem contexto e registra o erro, em vez de derrubar a conversa.

## Passar para um humano

Um agente que nunca desiste é um problema. No DockPlus Atendimento, o handoff é um nó do grafo, e cada passagem registra um motivo. Alguns dos motivos que existem no código: sentimento negativo, cliente pediu um atendente, agente não achou a resposta, cliente mandou STOP, erro no agendamento, visita confirmada.

Quando o handoff acontece, o agente:

1. Avisa o cliente no idioma da conversa que alguém vai responder.
2. Gera um resumo curto da conversa com o modelo, para o atendente não ter que ler tudo.
3. Notifica a equipe por Telegram com o motivo, o resumo e o link da conversa no painel.

Um job roda a cada 5 minutos para recuperar handoffs que ficaram pendurados, sem mandar mensagem não solicitada ao cliente. O atendente assume e devolve a conversa pelo painel.

## O que medir

Medir só "quantas mensagens o bot respondeu" não diz se o agente está funcionando. O que o código registra hoje dá uma base melhor:

- **Por execução do grafo:** duração total, nós executados, intenção final e sentimento final.
- **Por resposta do RAG:** quais trechos foram usados e com qual pontuação. Pontuação baixa repetida no mesmo assunto aponta buraco na base de conhecimento.
- **Handoffs por motivo:** muitos "agente não achou a resposta" é sinal de conteúdo faltando; muitos "sentimento negativo" é sinal de tom ou de fluxo errado.
- **Saúde operacional:** um job a cada 5 minutos confere a fila de entrada, os jobs agendados, os envios de template, as respostas de operador e os handoffs parados. Se algo trava, a equipe recebe um e-mail sem dados de clientes, e nada é reenviado automaticamente.

Para rastrear chamadas de modelo e ferramentas, o projeto usa Langfuse.

## Resumo

Uma automação de WhatsApp com IA que dá certo depende menos do prompt e mais das regras em volta: aceitar só eventos assinados, não processar o mesmo evento duas vezes, respeitar a janela de 24 horas, responder só com a base da empresa e saber a hora de chamar uma pessoa. Desenvolvo esse tipo de sistema com Claude Code, e o código do DockPlus Atendimento tem testes no CI contra um Postgres com pgvector real em cada push.

Os detalhes do projeto estão em [DockPlus Atendimento](https://gustavokarsten.com/projects/dockplus-atendimento). Se você está montando um agente de IA para WhatsApp, [fale comigo](https://gustavokarsten.com/contact).

---
Página: https://gustavokarsten.com/articles/agente-de-ia-para-whatsapp
Contato: gustavo.karsten@outlook.com · https://www.linkedin.com/in/gustavo-karsten-88a04383/ · https://github.com/gustavo-karsten
