No Brasil, atendimento acontece no WhatsApp. Não no chat do site, não por e-mail: no WhatsApp. Por isso um chatbot com IA nesse canal é, para a maioria das empresas daqui, o projeto de IA com retorno mais rápido que existe.
Este tutorial monta um do zero: recebimento de mensagem, resposta com LLM, memória de conversa, passagem para humano e controle de custo. Com código que roda e com as armadilhas que só aparecem em produção.
WhatsApp Cloud API (oficial, da Meta) + um webhook em Python + a API por token da GPUBrasil. Custo de IA por conversa: cerca de R$ 0,01 no modelo econômico. O custo de mensageria da Meta é cobrado à parte, por eles.
Antes de escrever código: a escolha que define o projeto
Existem dois caminhos para conectar no WhatsApp, e o segundo é uma armadilha frequente:
| Cloud API (oficial) | Bibliotecas não oficiais | |
|---|---|---|
| Como funciona | API da Meta, contrato formal | Automatizam o WhatsApp Web |
| Risco de banimento | Nenhum | Alto — e o número banido não volta |
| Custo | Cobrado pela Meta, por conversa/mensagem | "Grátis" até o número cair |
| Estabilidade | Contratual | Quebra a cada atualização do WhatsApp |
| Para que serve | Qualquer coisa séria | Protótipo pessoal, no máximo |
Se o bot vai atender cliente da sua empresa, use a Cloud API oficial. Perder o número comercial da empresa por causa de uma biblioteca não oficial é um prejuízo que nenhuma economia justifica. O resto deste guia assume a via oficial.
Passo 1: as credenciais da Meta
A burocracia é chata mas é uma vez só:
- Crie um app em developers.facebook.com e adicione o produto WhatsApp.
- Você recebe um número de teste e um token temporário — o bastante para desenvolver hoje.
- Para produção: verifique a empresa no Gerenciador de Negócios e conecte um número real (um que não tenha WhatsApp comum ativo).
- Gere um token permanente por usuário de sistema. O temporário expira em 24 horas e vai te derrubar no pior momento.
Anote: PHONE_NUMBER_ID, WHATSAPP_TOKEN e um VERIFY_TOKEN que você inventa.
Passo 2: o webhook
A Meta manda cada mensagem recebida para uma URL sua, via POST. Antes, ela valida essa URL com um GET — e ele precisa devolver o challenge em texto puro, senão o cadastro não passa.
import os, hmac, hashlib, httpx
from fastapi import FastAPI, Request, Response, BackgroundTasks
app = FastAPI()
VERIFY = os.environ["VERIFY_TOKEN"]
TOKEN = os.environ["WHATSAPP_TOKEN"]
PHONE_ID = os.environ["PHONE_NUMBER_ID"]
SEGREDO = os.environ["META_APP_SECRET"]
GRAPH = f"https://graph.facebook.com/v21.0/{PHONE_ID}/messages"
@app.get("/webhook")
def verificar(request: Request):
p = request.query_params
if p.get("hub.mode") == "subscribe" and p.get("hub.verify_token") == VERIFY:
return Response(content=p.get("hub.challenge"), media_type="text/plain")
return Response(status_code=403)
@app.post("/webhook")
async def receber(request: Request, tarefas: BackgroundTasks):
corpo = await request.body()
if not assinatura_valida(corpo, request.headers.get("x-hub-signature-256", "")):
return Response(status_code=403)
dados = await request.json()
for entrada in dados.get("entry", []):
for mud in entrada.get("changes", []):
for msg in mud.get("value", {}).get("messages", []):
tarefas.add_task(processar, msg)
return Response(status_code=200) # 200 IMEDIATO. Ver aviso abaixo.
def assinatura_valida(corpo: bytes, cabecalho: str) -> bool:
esperado = "sha256=" + hmac.new(SEGREDO.encode(), corpo, hashlib.sha256).hexdigest()
return hmac.compare_digest(esperado, cabecalho)
Se o seu webhook demora para responder, a Meta considera falha e reenvia a mesma mensagem. Você chama o LLM duas, três vezes, paga por todas e o cliente recebe respostas repetidas.
A regra: valide a assinatura, jogue o processamento para segundo plano e devolva 200 em milissegundos. Em volume maior, troque o BackgroundTasks por uma fila de verdade.
E guarde o id de cada mensagem já processada: reenvio acontece de qualquer jeito, e a idempotência é o que impede cobrança dupla.
Passo 3: responder com o LLM
Agora a parte de IA. A API da GPUBrasil é compatível com a da OpenAI, então qualquer biblioteca do ecossistema serve — e a cobrança é em reais, o que facilita a conta por conversa.
from openai import OpenAI
llm = OpenAI(base_url="https://gpubrasil.com.br/v1",
api_key=os.environ["GPUBRASIL_API_KEY"])
SISTEMA = """Você é o atendente virtual da {empresa}.
O que você faz:
- Responde dúvidas sobre produtos, prazos, formas de pagamento e horário.
- Consulta o status de um pedido quando o cliente informar o número.
Como responde:
- Português do Brasil, cordial e direto. No máximo 3 frases.
- Sem markdown: WhatsApp não renderiza. Use *negrito* do próprio WhatsApp.
- Uma pergunta por vez.
Limites (importante):
- NUNCA invente prazo, preço, promoção ou política.
- Se não souber, ou se o cliente pedir humano, reclamar ou falar de cancelamento,
responda exatamente: TRANSFERIR_HUMANO
"""
async def processar(msg: dict):
de = msg["from"]
if msg.get("type") != "text":
await enviar(de, "Por enquanto consigo ler só mensagens de texto. "
"Pode escrever o que você precisa?")
return
texto = msg["text"]["body"]
if ja_processada(msg["id"]):
return
marcar_processada(msg["id"])
historico = carregar_historico(de)
mensagens = ([{"role": "system", "content": SISTEMA.format(empresa="Sua Empresa")}]
+ historico
+ [{"role": "user", "content": texto}])
r = llm.chat.completions.create(
model="gpub-fast", # atendimento é tarefa objetiva: modelo econômico
messages=mensagens,
temperature=0.3,
max_tokens=300, # trava dura: resposta longa no WhatsApp é ruim E cara
)
resposta = r.choices[0].message.content.strip()
registrar_custo(de, r.model, r.usage)
if "TRANSFERIR_HUMANO" in resposta:
await enviar(de, "Vou chamar alguém do time para te ajudar. Um instante 🙂")
abrir_ticket(de, historico + [{"role": "user", "content": texto}])
return
salvar_historico(de, texto, resposta)
await enviar(de, resposta)
async def enviar(para: str, texto: str):
async with httpx.AsyncClient(timeout=20) as c:
await c.post(GRAPH,
headers={"Authorization": f"Bearer {TOKEN}"},
json={"messaging_product": "whatsapp", "to": para,
"type": "text", "text": {"body": texto[:4096]}})
Três detalhes do prompt que evitam incidente
- "No máximo 3 frases." Sem isso o modelo escreve parágrafos, o cliente não lê e você paga pelos tokens.
- "Sem markdown." WhatsApp não renderiza
**negrito**; o cliente vê os asteriscos. O negrito do WhatsApp é*assim*. - A palavra-chave de transferência. Uma marca literal que o seu código detecta é muito mais confiável do que tentar inferir intenção depois. E cubra explicitamente reclamação e cancelamento — são exatamente os casos em que um bot insistindo causa dano.
Passo 4: memória de conversa
Sem histórico, o cliente diz "quero dois" e o bot pergunta "dois do quê?". Com histórico ilimitado, você paga por uma conversa de três semanas em toda mensagem. O meio-termo: últimas N trocas, com expiração.
import json, time, redis
r = redis.Redis(decode_responses=True)
JANELA = 6 * 2 # 6 trocas = 12 mensagens
EXPIRA = 60 * 60 * 6 # 6 horas de silêncio → conversa nova
def carregar_historico(tel: str):
return [json.loads(x) for x in r.lrange(f"hist:{tel}", -JANELA, -1)]
def salvar_historico(tel: str, pergunta: str, resposta: str):
chave = f"hist:{tel}"
r.rpush(chave, json.dumps({"role": "user", "content": pergunta}),
json.dumps({"role": "assistant", "content": resposta}))
r.ltrim(chave, -JANELA, -1)
r.expire(chave, EXPIRA)
A expiração faz mais do que economizar: cliente que volta no dia seguinte com outro assunto não quer o bot preso no contexto anterior.
Passo 5: dar braços ao bot
Um bot que só conversa tem valor limitado. O que resolve chamado é consultar o seu sistema. O padrão mais robusto é fazer o modelo devolver uma intenção estruturada, e o seu código decidir o que executar:
ROTEADOR = """Classifique a mensagem do cliente. Responda só JSON:
{"intencao": "pedido|duvida|humano|saudacao", "numero_pedido": "se houver, senão null"}"""
def rotear(texto: str) -> dict:
r = llm.chat.completions.create(
model="gpub-fast",
messages=[{"role": "system", "content": ROTEADOR},
{"role": "user", "content": texto}],
temperature=0,
response_format={"type": "json_object"},
)
try:
return json.loads(r.choices[0].message.content)
except json.JSONDecodeError:
return {"intencao": "duvida", "numero_pedido": None} # sempre tenha o caminho de erro
def responder(texto, tel):
rota = rotear(texto)
if rota["intencao"] == "pedido" and rota["numero_pedido"]:
p = erp.buscar_pedido(rota["numero_pedido"]) # SEU sistema, dado real
if not p:
return "Não localizei esse número. Pode conferir?"
return f"Pedido *{p.numero}*: {p.status}. Previsão: {p.previsao}."
...
Repare que o status do pedido é lido do seu sistema e formatado por código, não gerado pelo modelo. Prazo e status são exatamente o tipo de informação que um LLM inventa com toda a confiança do mundo. Deixe o modelo entender a pergunta; deixe o seu código responder o fato.
Passo 6: a janela de 24 horas
Regra da Meta que pega todo mundo de surpresa: você só pode enviar mensagem livre dentro de 24 horas após a última mensagem do cliente. Passou disso, só com template aprovado previamente.
- Dentro das 24 h: responda o que quiser, como no código acima.
- Fora das 24 h: só template aprovado — e template tem processo de aprovação de dias.
- Consequência prática: notificação proativa (status de pedido, lembrete, cobrança) precisa de template planejado com antecedência. Não dá para improvisar na véspera.
Guarde o instante da última mensagem de cada cliente e verifique antes de enviar. Tentar mandar texto livre fora da janela retorna erro — e o cliente simplesmente não recebe.
A conta: quanto custa por conversa
Duas contas separadas, e vale entender as duas:
| Item | Quem cobra | Ordem de grandeza |
|---|---|---|
| Mensageria (a conversa em si) | Meta | Consulte a tabela vigente da Meta — varia por país e categoria |
| Inteligência (o LLM) | GPUBrasil, em reais | ~R$ 0,01 por conversa no gpub-fast |
| Hospedagem do webhook | Onde você rodar | Instância de CPU resolve — não precisa de GPU |
Detalhando a parte de IA, numa conversa típica de 10 trocas (cerca de 16 mil tokens de entrada acumulados e 1.500 de saída):
| Modelo | Por conversa | 5.000 conversas/mês |
|---|---|---|
gpub-fast | R$ 0,0095 | R$ 48 |
gpub-mini | R$ 0,0147 | R$ 73 |
gpub-plus | R$ 0,0170 | R$ 85 |
gpub-base | R$ 0,1532 | R$ 766 |
Para atendimento, o gpub-fast costuma bastar — a tarefa é objetiva e o prompt faz a maior parte do trabalho. Suba de modelo apenas se medir erro de compreensão, não por precaução.
Quase nunca. O webhook roda tranquilo numa instância de CPU e a inteligência vem por token, sem custo parado. GPU dedicada só entra se você quiser servir o modelo internamente por exigência de governança, ou se o volume for muito alto e constante.
Passo 7: os cuidados que evitam processo
- Diga que é um bot. Na primeira mensagem. Além de ser exigência de boas práticas, reduz frustração.
- Saída para humano sempre disponível. Um bot que não deixa falar com gente gera reclamação em órgão de defesa do consumidor.
- Não peça dado sensível no chat. Cartão, senha, documento completo — nada disso deve trafegar por ali. Mande um link seguro.
- Trate LGPD desde o começo. Conversa de WhatsApp é dado pessoal: tenha base legal, política de retenção e caminho para exclusão a pedido. A expiração do histórico já ajuda.
- Registre o que o bot respondeu. No dia da disputa, o log é a sua prova.
- Teste o cliente irritado. Ele existe, escreve em caixa alta e o bot precisa transferir rápido em vez de insistir.
Erros de produção que você vai encontrar
| Sintoma | Causa |
|---|---|
| Cliente recebe a mesma resposta 2–3 vezes | Webhook lento → reenvio da Meta. Devolva 200 na hora e use idempotência por id |
| Cadastro do webhook não passa | O GET não devolveu o challenge em texto puro |
| Bot mudo depois de 24 h | Janela fechada — exige template aprovado |
| Asteriscos aparecem no texto | Modelo usou markdown; reforce no prompt |
| Parou de funcionar do nada | Token temporário expirou — gere o permanente |
| Bot inventou prazo de entrega | Informação factual veio do modelo em vez do seu sistema |
| Custo explodiu | Histórico sem ltrim, ou max_tokens sem trava |
A inteligência do seu bot, cobrada em reais
API compatível com a da OpenAI a partir de R$ 0,49 por milhão de tokens, no mesmo saldo das suas máquinas. Sem assinatura, sem câmbio, com suporte em português.
Criar conta →Conclusão
Um chatbot de WhatsApp com IA é um projeto de duas semanas, não de dois meses. A parte de LLM é a mais simples — e a mais barata, a centavos por conversa. O que separa um bot que ajuda de um que irrita está nos detalhes operacionais: responder 200 na hora, não repetir mensagem, saber transferir para humano e nunca deixar o modelo inventar um prazo.
Comece pequeno: um bot que responde as cinco perguntas mais frequentes e transfere o resto já tira carga real do time. Aumente o escopo com base no que aparecer nos logs, não no que você imagina que o cliente vai perguntar.
Continue: agente com ferramentas · responder com base nos seus documentos · n8n, se preferir sem código