Como criar um chatbot de WhatsApp com Python (Flask + APIBrasil)

Capa do artigo Como criar um chatbot de WhatsApp com Python (Flask + APIBrasil)

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.

Infográfico da arquitetura de um chatbot de WhatsApp com Python: webhook no Flask, estado por número, menu numérico, IA para perguntas abertas e transbordo para atendente humano

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.

Loading

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *