Um chatbot de WhatsApp com Python resolve a parte repetitiva do atendimento: horário de funcionamento, segunda via, status do pedido, triagem antes de passar para uma pessoa. Com a APIBrasil, o bot é um servidor Flask que recebe cada mensagem por webhook, decide a resposta e devolve pelo endpoint de envio. Nada de automatizar o WhatsApp Web ou manter um navegador aberto.
O guia constrói esse bot de ponta a ponta: webhook no Flask, menu numérico com máquina de estados por número, integração opcional com um modelo de IA para perguntas abertas, transbordo para atendente humano e deploy com monitoramento. Os nomes de eventos e endpoints vêm da documentação em doc.apibrasil.io.
Você precisa de Python 3.10 ou superior, uma conta na APIBrasil com um dispositivo WhatsApp conectado (Bearer Token e DeviceToken) e uma URL pública para o webhook, que em desenvolvimento pode ser um túnel ngrok.
O que o chatbot de WhatsApp com Python vai responder
Antes do código, defina o escopo. Um bot que tenta responder tudo frustra; um bot que resolve três coisas bem e passa o resto para uma pessoa funciona. O exemplo deste guia atende uma loja fictícia com três opções: horário e endereço, segunda via de boleto por número do pedido e falar com atendente. Perguntas fora do menu vão para a IA, se ela estiver configurada, ou para o transbordo.
A arquitetura tem quatro peças, e cada uma vira um arquivo:
- app.py: o Flask que recebe o webhook, filtra os eventos e responde 200 rápido.
- bot.py: a máquina de estados, com uma função por etapa da conversa.
- whatsapp.py: o envio de mensagens pela API (sendText).
- ia.py: a chamada opcional a um modelo de linguagem, isolada para poder ser trocada.
Se o seu time prefere JavaScript, o Programador Brasil tem um guia equivalente de bot de WhatsApp com Node.js; os eventos e endpoints são os mesmos, só muda a linguagem do servidor.
Flask recebendo webhooks
O dispositivo WhatsApp da APIBrasil envia um POST para a URL cadastrada em wh_message sempre que uma mensagem entra ou sai. A URL é registrada ao iniciar a sessão, com POST /whatsapp/start, no mesmo corpo em que você define o nome da sessão e pede o QR Code. Em desenvolvimento, aponte para a URL do ngrok; em produção, para o domínio do servidor.
O corpo do webhook é uma lista com um evento, identificado pelo campo wook. Para o bot interessa apenas RECEIVE_MESSAGE com fromMe igual a false (mensagens que o cliente enviou) e isGroupMsg igual a false (ignorar grupos). O texto está em content e o remetente em from.
# app.py
import threading
from flask import Flask, jsonify, request
from bot import processar_mensagem
app = Flask(__name__)
VISTOS: set[str] = set() # ids já processados; em produção use Redis com TTL
@app.post("/webhook/whatsapp")
def webhook_whatsapp():
eventos = request.get_json(silent=True) or []
if isinstance(eventos, dict):
eventos = [eventos]
for ev in eventos:
if ev.get("wook") != "RECEIVE_MESSAGE" or ev.get("fromMe") or ev.get("isGroupMsg"):
continue
if ev.get("id") in VISTOS: # evento duplicado (retentativa ou webhook global)
continue
VISTOS.add(ev["id"])
# Responde 200 em milissegundos; o bot trabalha em outra thread
threading.Thread(target=processar_mensagem, args=(ev,), daemon=True).start()
return jsonify(ok=True), 200
@app.get("/saude")
def saude():
return jsonify(status="ok")
A thread é suficiente para um bot pequeno. Quando o volume crescer, troque por uma fila (RQ ou Celery com Redis) mantendo o handler igual: filtrar, deduplicar, enfileirar, responder. A APIBrasil espera a resposta em até 10 segundos; qualquer processamento dentro desse handler é risco de evento perdido.
O envio fica em um módulo próprio, com os dois headers obrigatórios das APIs de WhatsApp e a checagem do campo error, que pode vir com HTTP 200:
# whatsapp.py
import os
import requests
BASE_URL = "https://gateway.apibrasil.io/api/v2/whatsapp"
HEADERS = {
"Content-Type": "application/json",
"Authorization": f"Bearer {os.environ['APIBRASIL_TOKEN']}",
"DeviceToken": os.environ["APIBRASIL_DEVICE_TOKEN"],
}
def enviar_texto(numero: str, texto: str) -> dict:
r = requests.post(f"{BASE_URL}/sendText", headers=HEADERS,
json={"number": numero, "text": texto}, timeout=30)
corpo = r.json()
if r.status_code != 200 or corpo.get("error"):
raise RuntimeError(f"sendText falhou: {corpo.get('message')}")
return corpo
Menu e máquina de estados
Uma conversa é uma sequência de etapas, e o bot precisa lembrar em qual delas cada número está. A máquina de estados faz isso com um dicionário por telefone: etapa atual, dados coletados, contador de erros e horário da última mensagem. Se o cliente some por 30 minutos, a conversa recomeça do zero.
Cada etapa é uma função que recebe o texto e o estado e devolve a resposta e o novo estado. Isso mantém as regras legíveis e testáveis sem WhatsApp: basta chamar a função com uma string.
# bot.py
import os
import time
from ia import responder_com_ia
from whatsapp import enviar_texto
ESTADOS: dict[str, dict] = {} # em produção: Redis com TTL
TTL_SEGUNDOS = 30 * 60
ATENDENTE = os.environ.get("NUMERO_ATENDENTE", "")
MENU = (
"Olá! Sou o assistente virtual da Loja Exemplo. Digite o número da opção:\n"
"1 - Horário e endereço\n"
"2 - Segunda via de boleto\n"
"3 - Falar com um atendente"
)
PEDIU_HUMANO = ("atendente", "humano", "pessoa", "falar com alguém")
def obter_estado(numero: str) -> dict:
estado = ESTADOS.get(numero)
if not estado or time.time() - estado["atualizado_em"] > TTL_SEGUNDOS:
estado = {"etapa": "INICIO", "dados": {}, "erros": 0, "atualizado_em": time.time()}
return estado
def transbordar(numero: str, nome: str, estado: dict) -> tuple[str, dict]:
estado["etapa"] = "HUMANO"
estado["transbordado_em"] = time.time()
if ATENDENTE:
enviar_texto(ATENDENTE, f"Novo atendimento: {nome} ({numero}). Último passo: {estado['dados']}")
return "Certo! Um atendente vai continuar a conversa por aqui em instantes.", estado
def etapa_inicio(texto, numero, nome, estado):
estado["etapa"] = "MENU"
return MENU, estado
def etapa_menu(texto, numero, nome, estado):
t = texto.lower()
if t == "1":
return "Funcionamos de segunda a sexta, das 9h às 18h, na Rua Exemplo, 100. Digite 0 para o menu.", estado
if t == "2":
estado["etapa"] = "AGUARDANDO_PEDIDO"
return "Informe o número do pedido (só os dígitos).", estado
if t == "3" or any(p in t for p in PEDIU_HUMANO):
return transbordar(numero, nome, estado)
if t == "0":
return MENU, estado
resposta_ia = responder_com_ia(texto)
if resposta_ia:
return resposta_ia + "\n\nDigite 0 para o menu ou 3 para falar com um atendente.", estado
estado["erros"] += 1
if estado["erros"] >= 2:
return transbordar(numero, nome, estado)
return "Não entendi. " + MENU, estado
def etapa_aguardando_pedido(texto, numero, nome, estado):
if texto.isdigit():
estado["dados"]["pedido"] = texto
estado["etapa"] = "MENU"
estado["erros"] = 0
# Aqui entra a consulta ao seu sistema de pedidos
return f"Pedido {texto} localizado. A segunda via foi enviada ao e-mail cadastrado. Digite 0 para o menu.", estado
return "Preciso só dos dígitos do pedido, por exemplo 1234. Ou digite 0 para voltar.", estado
def etapa_humano(texto, numero, nome, estado):
return None, estado # bot em silêncio enquanto a pessoa atende
ETAPAS = {
"INICIO": etapa_inicio,
"MENU": etapa_menu,
"AGUARDANDO_PEDIDO": etapa_aguardando_pedido,
"HUMANO": etapa_humano,
}
def processar_mensagem(ev: dict) -> None:
numero = ev["from"]
nome = ev.get("name") or numero
texto = (ev.get("content") or "").strip()
if not texto:
return # áudio, imagem ou figurinha: trate à parte se fizer sentido
estado = obter_estado(numero)
resposta, estado = ETAPAS[estado["etapa"]](texto, numero, nome, estado)
estado["atualizado_em"] = time.time()
ESTADOS[numero] = estado
if resposta:
enviar_texto(numero, resposta)
Repare em duas decisões. O menu aceita tanto o número quanto palavras-chave como “atendente”, porque muita gente ignora instruções e escreve o que quer. E o contador de erros limita a frustração: na segunda mensagem que o bot não entende, ele transborda em vez de repetir o menu pela terceira vez.

Integrando uma IA (opcional)
O menu cobre o previsível; a IA cobre o resto, com limites. A regra que mais importa é o escopo: o modelo recebe um texto de sistema dizendo o que ele sabe (horários, políticas de troca, prazos) e a instrução de responder apenas sobre isso, devolvendo um sinal claro quando não souber, para o bot cair no transbordo em vez de inventar.
A função abaixo é um esqueleto independente de fornecedor: a URL e a chave vêm do ambiente, e o formato do corpo segue o padrão de chat da maioria dos provedores. Confira na documentação do seu provedor o formato exato da requisição e da resposta antes de usar.
# ia.py
import os
import requests
CONTEXTO = (
"Você é o assistente da Loja Exemplo. Responda em até 3 frases, em português, "
"somente sobre horário (seg a sex, 9h às 18h), trocas (até 7 dias com nota) e prazo de "
"entrega (3 a 5 dias úteis). Se a pergunta for sobre outro assunto, responda exatamente: NAO_SEI."
)
def responder_com_ia(pergunta: str) -> str | None:
url, chave, modelo = os.environ.get("LLM_URL"), os.environ.get("LLM_API_KEY"), os.environ.get("LLM_MODEL")
if not (url and chave and modelo):
return None # IA desligada: o bot segue só com o menu
try:
r = requests.post(
url,
headers={"Authorization": f"Bearer {chave}", "Content-Type": "application/json"},
json={"model": modelo, "max_tokens": 200,
"messages": [{"role": "system", "content": CONTEXTO},
{"role": "user", "content": pergunta[:500]}]},
timeout=20,
)
r.raise_for_status()
texto = r.json()["choices"][0]["message"]["content"].strip() # ajuste ao formato do provedor
except Exception:
return None # qualquer falha na IA vira fallback, nunca erro para o cliente
return None if "NAO_SEI" in texto else texto
Dois cuidados de produção. Não mande dados pessoais do cliente para o modelo sem necessidade; a pergunta já basta. E registre cada par pergunta/resposta da IA para revisão: é assim que você descobre o que o menu deveria cobrir e onde o modelo está escorregando.
Transbordo para atendente humano
O transbordo é a parte mais importante do bot, porque é onde a experiência ruim costuma acontecer: o cliente pede uma pessoa e recebe o menu de novo. No código acima, três gatilhos levam ao estado HUMANO: a opção 3, palavras como “atendente” ou “humano” e dois erros seguidos de interpretação.
Ao transbordar, o bot faz três coisas: muda o estado para HUMANO (e passa a ignorar as próximas mensagens daquele número), avisa o cliente que alguém vai assumir e notifica o atendente com nome, número e o que já foi coletado. O aviso ao atendente pode ser uma mensagem no WhatsApp dele, como no exemplo, ou um ticket no seu sistema de atendimento.
Falta o caminho de volta. Duas opções simples: um endpoint administrativo que apaga o estado do número quando o atendente encerra, ou uma expiração do estado HUMANO (por exemplo, 2 horas sem mensagens) após a qual o bot reassume com o menu. Decidir o que fica com o bot e o que vai para a pessoa é uma escolha de operação, e o artigo sobre chatbot ou atendimento humano no WhatsApp do Blog PulseOut ajuda a traçar essa linha com critérios de volume, complexidade e custo.
Deploy e monitoramento
Em produção, o chatbot de WhatsApp com Python roda atrás de um servidor WSGI com HTTPS, porque a APIBrasil só vai entregar o webhook em uma URL pública e segura. O caminho comum é gunicorn com dois ou mais workers atrás de um nginx ou de um balanceador com certificado.
pip install flask requests gunicorn
export APIBRASIL_TOKEN=... APIBRASIL_DEVICE_TOKEN=... NUMERO_ATENDENTE=5531999999999
gunicorn -w 2 -b 0.0.0.0:8000 app:app
Com mais de um worker, o dicionário ESTADOS em memória deixa de funcionar, porque cada processo tem o seu. Troque por Redis com expiração: a chave é o número, o valor é o estado em JSON e o TTL substitui a verificação manual de tempo. O conjunto VISTOS de ids segue a mesma regra.
Para monitorar, acompanhe quatro sinais:
- Conexão do dispositivo: registre a URL wh_status no start e alerte quando chegar STATUS_CONNECTION diferente de CONNECTED; um health check periódico em POST /whatsapp/isConnected cobre o intervalo entre eventos.
- Taxa de transbordo: quantas conversas foram para o humano e por qual gatilho. Se a maioria vem de “não entendi”, o menu precisa de ajuste.
- Tempo de resposta do bot: do webhook recebido ao sendText concluído. Se crescer, a IA ou a consulta ao seu sistema está lenta.
- Erros de envio: respostas com error: true ou HTTP 4xx no sendText, geralmente dispositivo desconectado ou número inválido.
Nos logs, grave o id do evento e a etapa, mas mascare o número do cliente e nunca registre o conteúdo completo das mensagens sem necessidade: é dado pessoal e cai na LGPD. O guia de boas práticas de integração com a APIBrasil em produção detalha tokens, retries e alertas de saldo que valem para o bot inteiro.
Perguntas frequentes
Preciso da API oficial do WhatsApp Business para fazer o chatbot?
Não. O exemplo usa a API WhatsApp WPP da APIBrasil, que conecta um número comum pelo QR Code. Se o seu caso exigir conta verificada e templates aprovados, a plataforma também oferece a API Oficial (Cloud); a lógica de estados do bot continua a mesma, mudando apenas o formato dos eventos.
O bot responde a si mesmo em loop?
Não, desde que você filtre fromMe igual a true, como no app.py. Toda mensagem enviada pelo bot gera um evento SEND_MESSAGE no mesmo webhook; sem o filtro, o bot trataria a própria resposta como entrada e entraria em loop.
Como o bot lida com áudio, imagem ou figurinha?
No exemplo, mensagens sem texto em content são ignoradas. Uma versão melhor responde algo como “Por aqui consigo ajudar por texto; digite 1, 2 ou 3”. Para áudio, é possível transcrever antes de passar para a máquina de estados, mas isso adiciona custo e latência, então avalie se o volume justifica.
Posso testar o bot sem um número de cliente?
Sim. Mande mensagens do seu próprio celular para o número conectado ao dispositivo de teste e acompanhe os eventos no inspetor do ngrok. As funções de etapa também podem ser testadas com pytest, chamando cada uma com um texto e um estado, sem WhatsApp envolvido.
Quantas mensagens o bot aguenta?
O gargalo não é o Flask, e sim o envio sequencial e as chamadas externas (IA, seu sistema). Com fila e Redis, um bot simples atende um volume alto de conversas; o limite prático é o do plano do dispositivo e as políticas do WhatsApp contra disparos em massa, que valem também para respostas automáticas.
Próximos passos
Com o esqueleto do chatbot de WhatsApp com Python rodando, a evolução natural é conectar as etapas ao seu sistema (pedidos, boletos, agenda) e medir onde o bot resolve e onde ele transborda. Se ainda não integrou o envio, o guia para enviar WhatsApp com Python usando a APIBrasil cobre texto, imagem e arquivo com o mesmo cliente usado aqui. Crie sua conta em platform.apibrasil.com.br, conecte um dispositivo de teste e converse com o seu bot antes de apontá-lo para o número da empresa.
![]()









