Objetivos de aprendizado
Depois de ler este artigo, você vai conseguir:
- Entender o caminho de uma mensagem do WhatsApp até o LLM e de volta
- Escrever um webhook em FastAPI que verifica a assinatura e responde com IA
- Estimar o que a Meta, o modelo e a hospedagem cobram
Um bot de WhatsApp com IA parece complicado, mas é uma API web comum que conversa com dois lados: a Meta, que entrega e envia as mensagens, e um provedor de modelo de linguagem, que escreve a resposta. O seu código fica no meio e decide o que o modelo sabe e quanto você gasta.
A decisão mais importante vem antes do código: usar a Cloud API oficial da Meta ou uma biblioteca não oficial. Aqui usamos a Cloud API, porque é a via suportada e funciona por webhook, sem manter uma sessão do WhatsApp Web aberta.
Como é a arquitetura de um bot de WhatsApp com IA?#
São quatro peças e um caminho de ida e volta:
- O usuário manda uma mensagem para o número do bot.
- A Cloud API da Meta faz um
POSTno seu webhook, um endpoint HTTPS público. - Seu servidor valida a assinatura, devolve
200 OKe, em segundo plano, envia o histórico da conversa ao LLM. Para entender o que o modelo faz por dentro, veja o que é um LLM. - A resposta volta por um
POSTna Cloud API, que entrega ao usuário.
Isso tem duas consequências práticas. O servidor precisa estar sempre ligado, porque a Meta só reenvia a notificação por um tempo limitado. E o webhook precisa de uma URL HTTPS pública, então testar em localhost pede um túnel.
O que você precisa antes de começar?#
- Uma conta de desenvolvedor da Meta com um app e o produto WhatsApp adicionado. No painel você pega o
Phone Number ID, o token de acesso e oApp Secretdo app. - Uma chave de API de um provedor de LLM. O exemplo usa a da Anthropic.
- Uma versão do Python ainda com suporte e noção de variáveis de ambiente.
Para o passo a passo da conta e do número, sem IA, veja como fazer um bot de WhatsApp.
Como escrever o webhook?#
O requirements.txt:
fastapi
uvicorn
httpx
anthropicE o main.py. Ele faz quatro coisas: responde ao desafio de verificação (GET), confere a assinatura X-Hub-Signature-256 (POST), ignora mensagens repetidas e responde com o modelo em segundo plano.
import hashlib
import hmac
import os
from collections import OrderedDict, defaultdict, deque
import httpx
from anthropic import AsyncAnthropic
from fastapi import BackgroundTasks, FastAPI, HTTPException, Query, Request
from fastapi.responses import PlainTextResponse
VERIFY_TOKEN = os.environ["VERIFY_TOKEN"] # string que você escolhe no painel da Meta
APP_SECRET = os.environ["APP_SECRET"].encode()
WA_TOKEN = os.environ["WA_TOKEN"]
PHONE_ID = os.environ["WA_PHONE_NUMBER_ID"]
GRAPH_API_VERSION = os.environ["GRAPH_API_VERSION"] # no formato vXX.0, do changelog da Meta
MODEL = os.environ["ANTHROPIC_MODEL"] # ID da lista de modelos da Anthropic
SYSTEM = (
"Você é o assistente de atendimento da loja Exemplo no WhatsApp. "
"Responda em português, em no máximo 3 frases, sem markdown."
)
SEND_URL = f"https://graph.facebook.com/{GRAPH_API_VERSION}/{PHONE_ID}/messages"
llm = AsyncAnthropic() # lê ANTHROPIC_API_KEY do ambiente
app = FastAPI()
history: dict[str, deque] = defaultdict(lambda: deque(maxlen=10)) # RAM: some no restart
seen: OrderedDict[str, None] = OrderedDict()
@app.get("/webhook")
async def verify(
mode: str = Query("", alias="hub.mode"),
token: str = Query("", alias="hub.verify_token"),
challenge: str = Query("", alias="hub.challenge"),
):
if mode == "subscribe" and hmac.compare_digest(token.encode(), VERIFY_TOKEN.encode()):
return PlainTextResponse(challenge)
raise HTTPException(403)
@app.post("/webhook")
async def receive(request: Request, tasks: BackgroundTasks):
raw = await request.body() # assinatura é sobre o corpo cru, não sobre o JSON reparseado
expected = "sha256=" + hmac.new(APP_SECRET, raw, hashlib.sha256).hexdigest()
received = request.headers.get("X-Hub-Signature-256", "")
if not hmac.compare_digest(expected.encode(), received.encode()):
raise HTTPException(403)
for entry in (await request.json()).get("entry", []):
for change in entry.get("changes", []):
for msg in change.get("value", {}).get("messages", []):
if msg["id"] in seen or msg.get("type") != "text":
continue
seen[msg["id"]] = None
if len(seen) > 1000:
seen.popitem(last=False)
tasks.add_task(answer, msg["from"], msg["text"]["body"])
return {"ok": True} # 200 imediato; a Meta reenvia se não receber
async def answer(user: str, text: str):
turns = history[user]
turns.append({"role": "user", "content": text})
while turns and turns[0]["role"] != "user":
turns.popleft() # o histórico tem de começar por "user"
try:
out = await llm.messages.create(
model=MODEL,
max_tokens=1024,
output_config={"effort": "low"},
system=SYSTEM,
messages=list(turns),
)
reply = "".join(b.text for b in out.content if b.type == "text") or "..." # ignora blocos de thinking
except Exception as e:
print("llm error:", e)
reply = "Não consegui responder agora. Tente de novo em instantes."
turns.append({"role": "assistant", "content": reply})
async with httpx.AsyncClient(timeout=15) as http:
r = await http.post(
SEND_URL,
headers={"Authorization": f"Bearer {WA_TOKEN}"},
json={
"messaging_product": "whatsapp",
"to": user,
"type": "text",
"text": {"body": reply},
},
)
if r.status_code >= 400:
print("whatsapp error:", r.status_code, r.text)O que observar no código:
- Responda 200 antes de chamar o modelo. Se o endpoint não devolver
200, a Meta tenta de novo, com frequência decrescente por até 7 dias. Sem oseen, o cliente receberia respostas duplicadas. - Valide a assinatura. A documentação da Meta diz que a validação não é obrigatória, mas é recomendada. Sem ela, qualquer pessoa que descubra a URL pode mandar mensagens falsas e gastar o seu crédito de IA.
fromé o número do usuário e serve direto como destino (to) da resposta.- O modelo e a versão da Graph API são configuração. A Anthropic aposenta modelos, e cada versão da Graph API deixa de funcionar cerca de dois anos depois do lançamento. Por isso os dois ficam em variáveis de ambiente, sem valor padrão: se faltar uma, o app para ao iniciar com
KeyError, e atualizar vira trocar o valor e reiniciar. Copie o ID de um modelo pequeno, rápido e barato (da linha Haiku, por exemplo) da lista de modelos da Anthropic e a versão mais recente do changelog da Graph API. - O raciocínio entra no
max_tokens. Se o modelo escolhido pensa antes de responder, esses tokens contam no limite e são cobrados como saída. Commax_tokensbaixo demais, a resposta pode sair cortada ou vazia. Oeffortbaixo reduz o raciocínio num atendimento simples, e 1024 deixa folga para a resposta curta. Se o modelo não aceitar o parâmetroeffort, a API devolve erro 400: remova a linha. - O código só trata texto. Áudio, imagem e botões chegam com outro
type; adicione um ramo para cada um que o seu bot suportar.
Como configurar e testar o webhook?#
Rode localmente e exponha a porta com um túnel HTTPS:
pip install -r requirements.txt
export VERIFY_TOKEN="uma-string-secreta" APP_SECRET="..." WA_TOKEN="..." \
WA_PHONE_NUMBER_ID="..." ANTHROPIC_API_KEY="..." \
ANTHROPIC_MODEL="id-de-um-modelo-pequeno" GRAPH_API_VERSION="vXX.0"
uvicorn main:app --port 8000No painel da Meta, na configuração do WhatsApp, informe a URL https://seu-dominio/webhook e o mesmo VERIFY_TOKEN, e assine o campo messages. Para ver a verificação funcionando antes disso:
curl "localhost:8000/webhook?hub.mode=subscribe&hub.verify_token=uma-string-secreta&hub.challenge=123"
echo "(a resposta deve ser 123)"Depois mande uma mensagem do seu celular para o número do bot e acompanhe os logs do servidor.
Quanto custa um bot de WhatsApp com IA?#
Há três contas separadas:
- Meta. A cobrança é por template entregue, e não por conversa. Mensagens que não são template (texto, imagem) dentro da janela de atendimento de 24 horas são gratuitas. Um bot que só responde ao cliente fica, em geral, dentro dessa janela. Disparos ativos, como avisos e promoções, usam templates e são cobrados por categoria e país. Veja os detalhes em custo da API oficial do WhatsApp.
- Modelo. Você paga por token de entrada e saída. Como o histórico é reenviado a cada mensagem, conversas longas custam mais; o
maxlen=10e omax_tokens=1024do código seguram isso. Para fazer as contas, veja quanto custa uma API de LLM. - Hospedagem. Um webhook que só repassa texto usa pouca memória. O que pesa é estar sempre ligado.
Quais são os limites e erros comuns?#
- Memória some no restart. O
historyvive na RAM. Para produção, guarde as últimas mensagens num Redis ou banco, indexadas pelo número. - Janela de 24 horas. Fora dela, só templates aprovados passam. Se o bot precisa puxar assunto, planeje templates.
- Falha ao salvar o webhook no painel: o
VERIFY_TOKENdo painel é diferente do da variável de ambiente, ou o servidor não está acessível por HTTPS público. - Assinatura sempre inválida: o
APP_SECRETestá errado ou você validou sobre o JSON reserializado em vez derequest.body(). - Respostas duplicadas: o servidor demorou ou falhou ao devolver
200. Confirme que o trabalho do modelo roda em segundo plano. - Abuso e prompt injection: o usuário controla o texto que o modelo lê. Não dê ferramentas ou dados sensíveis ao modelo sem filtro e, para volume alto, limite mensagens por número.
Como fazer isso na Square Cloud#
O webhook precisa de duas coisas que o computador de casa não dá: estar no ar sempre que a Meta chamar e uma URL HTTPS pública. Na Square Cloud, a aplicação roda num container isolado e sempre ligado, sem cold start nem modo de suspensão, e ganha um subdomínio *.squareweb.app com HTTPS, que pode servir como URL do webhook no painel da Meta, sem túnel.
- Envie
main.pyerequirements.txtpor upload de ZIP no painel, CLI, GitHub ou extensão do VS Code. O arquivosquarecloud.appé opcional: o painel o gera no deploy, ou você o escreve na raiz do projeto. - Use
uvicorn main:app --host 0.0.0.0 --port $PORTcomo comando de início: uma aplicação web escuta em0.0.0.0, na porta da variávelPORT, e usa pelo menos 512 MB do pool de RAM do plano. - Cadastre
VERIFY_TOKEN,APP_SECRET,WA_TOKEN,WA_PHONE_NUMBER_ID,GRAPH_API_VERSION,ANTHROPIC_API_KEYeANTHROPIC_MODELnas variáveis de ambiente da aplicação. A mudança vale no próximo restart, sem rebuild. - Mande a primeira mensagem com os logs em tempo real abertos: os erros da Meta e do modelo aparecem ali.
A partir do plano Standard (R$ 44,99 por mês), a hospedagem de Redis pode guardar o histórico entre restarts e, no mesmo plano, o AI Gateway pode substituir o provedor do modelo: uma API compatível com a da OpenAI para o modelo cubic, em beta com acesso antecipado sem custo extra. Ele só aceita texto, que é o que este webhook trata. Adicione openai ao requirements.txt, troque a chamada dentro de answer e apague a linha do MODEL, que deixa de ser usada:
from openai import AsyncOpenAI
llm = AsyncOpenAI(
base_url="https://api.squarecloud.app/v2/ai",
api_key=os.environ["SQUARECLOUD_API_KEY"], # chave da conta com o escopo ai:chat
)
# dentro de answer():
out = await llm.chat.completions.create(
model="cubic",
messages=[{"role": "system", "content": SYSTEM}, *turns],
)
reply = out.choices[0].message.content or "..."Confira os limites antes de trocar: no Standard, até 2,5 milhões de tokens e até 1.000 requisições por dia, uma requisição por vez. Com muitos clientes escrevendo ao mesmo tempo, uma chamada que chega enquanto outra está em andamento recebe erro 429; o SDK da OpenAI tenta de novo algumas vezes, e o except do código responde com a mensagem de reserva. Limites e planos podem mudar quando o beta acabar. O Hobby, a partir de R$ 12,49 por mês, hospeda o webhook, mas não inclui bancos nem o gateway, e a Meta cobra os templates à parte. Veja os detalhes em bots de WhatsApp.
IA
Hospede apps com IA em containers sempre ligados
Perguntas frequentes
Posso usar uma biblioteca como Baileys em vez da Cloud API?
Dá, mas bibliotecas como Baileys não são a via oficial da Meta e dependem de uma sessão do WhatsApp Web aberta. A Cloud API é a opção suportada e funciona por webhook, sem sessão para manter.
Por que o bot responde duas vezes à mesma mensagem?
A Meta reenvia a notificação se o seu servidor não devolver 200. Responda 200 logo, faça o trabalho em segundo plano e guarde o id das mensagens já tratadas.
O bot pode responder a qualquer hora?
Com mensagem livre, só enquanto a janela de atendimento de 24 horas estiver aberta, e ela recomeça a cada mensagem do cliente. Fora dela, só templates aprovados pela Meta.
Cada resposta do bot é cobrada pela Meta?
Não. Mensagens que não são templates, enviadas dentro da janela de atendimento aberta, são gratuitas. A Meta cobra por template entregue, conforme a categoria e o país.
Mais em IA
Ver todos os 13 artigos- Quanto custa criar e manter um chatbot com IAManter um chatbot com IA custa tokens do modelo, hospedagem, banco e, com RAG, contexto extra por mensagem. Veja como fazer a conta por conversa.
- Quanto custa um sistema RAG: embeddings, banco e LLMUm RAG tem três custos: embeddings, banco vetorial e LLM. Veja a conta para mil perguntas por dia, com preços hipotéticos, e onde a fatura cresce.
- Como criar um bot do Discord com IACrie um bot do Discord com IA em discord.js: comando /ask, chave de API em variável de ambiente, memória por canal, limites do Discord e como colocar no ar.
- Como hospedar um chatbot com IAUm chatbot com IA é uma API que recebe mensagens, chama um modelo de linguagem e devolve a resposta em streaming. Veja o código e como colocá-lo no ar.