← Blog

·

Bot de WhatsApp com Python na API oficial (FastAPI, do zero)

Atualizado em 28/10/2026

Para criar um bot de WhatsApp com Python na API oficial, você precisa de um servidor HTTPS com duas rotas: um GET que responde ao desafio de verificação da Meta e um POST que recebe as mensagens, valida a assinatura X-Hub-Signature-256 e responde chamando o endpoint /messages da Cloud API. Com FastAPI e httpx isso cabe em um arquivo de cerca de 150 linhas, e é o que este tutorial monta do zero.

O bot final responde “oi” ou “menu” com três botões, trata o toque em cada botão, marca a mensagem como lida, ignora eventos repetidos e roda em produção com Docker e HTTPS automático. O código foi testado com FastAPI 0.128 e funciona tanto direto na Meta quanto pela Vectalk, mudando só duas variáveis de ambiente.

Como o bot funciona

O fluxo tem quatro passos:

  1. O cliente manda uma mensagem para o seu número.
  2. A Meta envia um POST com o evento para a URL do seu webhook.
  3. O seu servidor valida a assinatura, responde 200 na hora e processa o evento em segundo plano.
  4. O bot chama POST /<PHONE_NUMBER_ID>/messages para responder.

Como a resposta sai logo depois da mensagem do cliente, ela está dentro da janela de 24 horas e pode ser texto livre ou mensagem interativa, sem template. Se você ainda está decidindo como acessar a API, o guia da API oficial do WhatsApp explica as opções.

O que você precisa antes

VariávelO que éOnde encontrar
PHONE_NUMBER_IDID do número na Cloud APIPainel de Apps, na seção do WhatsApp
ACCESS_TOKENToken com permissão whatsapp_business_messagingToken de usuário do sistema
APP_SECRETChave secreta do app, usada na assinaturaConfigurações do app > Básico
VERIFY_TOKENTexto qualquer escolhido por vocêVocê define e repete no painel

Use um token permanente de usuário do sistema: o token temporário do painel expira e derruba o bot. O número também precisa estar registrado na Cloud API; se ainda não está, veja como registrar o número.

No ambiente local, você precisa de Python 3.10 ou mais recente.

Quer testar enquanto lê? Crie sua conta na Vectalk e envie a primeira mensagem pela API oficial. Criar conta grátis →

1. Prepare o projeto

mkdir bot-whatsapp && cd bot-whatsapp
python -m venv .venv
source .venv/bin/activate
pip install "fastapi[standard]" httpx

O pacote fastapi[standard] traz o Uvicorn e o comando fastapi. O httpx faz as chamadas assíncronas para a API.

Crie o arquivo main.py. Os blocos de código das seções 1 a 5 formam o arquivo completo: cole cada um em sequência.

import hashlib
import hmac
import json
import logging
import os

import httpx
from fastapi import BackgroundTasks, FastAPI, HTTPException, Request
from fastapi.responses import PlainTextResponse

VERIFY_TOKEN = os.environ["VERIFY_TOKEN"]
ACCESS_TOKEN = os.environ["ACCESS_TOKEN"]
APP_SECRET = os.environ.get("APP_SECRET", "")
WEBHOOK_KEY = os.environ.get("WEBHOOK_KEY", "")
PHONE_NUMBER_ID = os.environ.get("PHONE_NUMBER_ID", "")
API_URL = os.environ.get(
    "API_URL", f"https://graph.facebook.com/v26.0/{PHONE_NUMBER_ID}/messages"
)

log = logging.getLogger("bot")
app = FastAPI()
client = httpx.AsyncClient(timeout=10)
processed = set()  # em produção, use Redis ou o banco

API_URL tem a URL da Meta como padrão. Para usar a Vectalk, você só troca essa variável e o token, como mostra a seção sobre a Vectalk mais abaixo.

2. Verificação do webhook (GET)

Quando você salva a URL de callback no painel, a Meta manda um GET com hub.mode=subscribe, hub.verify_token e hub.challenge. Se o token bater com o seu, responda 200 com o valor de hub.challenge em texto puro. Qualquer outra resposta deixa o endpoint como não verificado, e a Meta não envia eventos.

@app.get("/webhook")
def verify(request: Request):
    q = request.query_params
    if q.get("hub.mode") == "subscribe" and q.get("hub.verify_token") == VERIFY_TOKEN:
        return PlainTextResponse(q.get("hub.challenge", ""))
    raise HTTPException(status_code=403)

Os nomes dos parâmetros têm ponto (hub.mode), por isso o código lê request.query_params em vez de declarar parâmetros na função.

3. Validar a assinatura X-Hub-Signature-256

Todo POST da Meta traz o cabeçalho X-Hub-Signature-256: sha256=<hash>. O hash é um HMAC-SHA256 do corpo da requisição, calculado com a chave secreta do app. Sem essa checagem, qualquer pessoa que descubra sua URL consegue fingir mensagens de clientes.

def signature_ok(body: bytes, header: str) -> bool:
    if not APP_SECRET:
        return True
    expected = "sha256=" + hmac.new(APP_SECRET.encode(), body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, header or "")

Dois cuidados que evitam horas de depuração:

  • Use os bytes originais. Calcule o HMAC sobre await request.body(), nunca sobre um JSON reserializado. Uma vírgula ou um espaço diferente muda o hash.
  • Compare em tempo constante. hmac.compare_digest evita que o tempo da comparação revele o hash.

O artigo sobre validar a assinatura do webhook aprofunda o tema e mostra os erros mais comuns.

4. Receber o evento e responder rápido

A Meta espera 200 rápido. Se a entrega falha, ela tenta de novo com frequência cada vez menor por até 7 dias, então o mesmo evento pode chegar mais de uma vez. Por isso o código responde antes de processar e ignora IDs de mensagem já vistos.

@app.post("/webhook")
async def receive(request: Request, background: BackgroundTasks):
    if WEBHOOK_KEY and request.query_params.get("key") != WEBHOOK_KEY:
        raise HTTPException(status_code=403)
    body = await request.body()
    if not signature_ok(body, request.headers.get("x-hub-signature-256", "")):
        raise HTTPException(status_code=401, detail="assinatura invalida")
    background.add_task(process, json.loads(body))
    return {"ok": True}


async def process(payload: dict) -> None:
    for entry in payload.get("entry", []):
        for change in entry.get("changes", []):
            for msg in change.get("value", {}).get("messages", []):
                if msg["id"] in processed:
                    continue  # a Meta pode reenviar o mesmo evento
                processed.add(msg["id"])
                try:
                    await handle(msg)
                except httpx.HTTPError:
                    log.exception("falha ao responder %s", msg["id"])

O BackgroundTasks do FastAPI roda process depois que a resposta é enviada. Eventos de status (sent, delivered, read, failed) chegam no mesmo webhook, dentro de value.statuses, e este bot os ignora. Para tratá-los, veja status de mensagem via webhook.

5. Responder texto e botões interativos

A lógica do bot fica em handle. Texto “oi” ou “menu” abre o menu de botões; o toque em um botão chega como type: "interactive" com interactive.type: "button_reply" e o id que você definiu no envio.

ANSWERS = {
    "horario": "Atendemos de segunda a sexta, das 9h às 18h.",
    "precos": "Veja a tabela completa em https://example.com/precos",
    "humano": "Certo! Um atendente vai continuar a conversa por aqui.",
}


async def handle(msg: dict) -> None:
    dest = recipient_of(msg)
    await mark_read(msg["id"])
    kind = msg.get("type")
    if kind == "text":
        text = msg["text"]["body"].strip().lower()
        if text in {"oi", "olá", "ola", "menu"}:
            await send_menu(dest)
        else:
            await send_text(dest, "Não entendi. Digite *menu* para ver as opções.")
    elif kind == "interactive" and msg["interactive"]["type"] == "button_reply":
        choice = msg["interactive"]["button_reply"]["id"]
        await send_text(dest, ANSWERS.get(choice, "Opção inválida."))
    else:
        await send_text(dest, "Por enquanto eu entendo só texto e botões.")


def recipient_of(msg: dict) -> dict:
    # Usuários com nome de usuário podem chegar sem "from", só com o BSUID
    if "from" in msg:
        return {"to": msg["from"]}
    return {"recipient": msg["from_user_id"]}

A função recipient_of já prepara o bot para os nomes de usuário do WhatsApp. Segundo a documentação da Meta, quando o cliente adota um nome de usuário, o campo from pode não vir no webhook, mas o identificador from_user_id (BSUID) vem sempre, e desde julho de 2026 é possível enviar mensagens para ele no campo recipient. O artigo sobre usernames e BSUID explica a mudança.

Por fim, as funções que chamam a API:

async def call_api(payload: dict) -> dict:
    r = await client.post(
        API_URL,
        headers={"Authorization": f"Bearer {ACCESS_TOKEN}"},
        json={"messaging_product": "whatsapp", **payload},
    )
    if r.is_error:
        log.error("API %s: %s", r.status_code, r.text)
    r.raise_for_status()
    return r.json()


async def mark_read(message_id: str) -> None:
    try:
        await call_api({
            "status": "read",
            "message_id": message_id,
            "typing_indicator": {"type": "text"},
        })
    except httpx.HTTPError:
        pass  # não impede a resposta


async def send_text(dest: dict, text: str) -> dict:
    return await call_api({
        "recipient_type": "individual",
        **dest,
        "type": "text",
        "text": {"body": text},
    })


async def send_menu(dest: dict) -> dict:
    buttons = [("horario", "Horário"), ("precos", "Preços"), ("humano", "Falar com atendente")]
    return await call_api({
        "recipient_type": "individual",
        **dest,
        "type": "interactive",
        "interactive": {
            "type": "button",
            "body": {"text": "Olá! Como posso ajudar?"},
            "action": {
                "buttons": [
                    {"type": "reply", "reply": {"id": i, "title": t}} for i, t in buttons
                ]
            },
        },
    })

mark_read marca a mensagem como lida e mostra “digitando…” para o cliente. A Meta remove o indicador quando você responde ou depois de 25 segundos.

Os limites dos botões de resposta, segundo a documentação da Meta:

CampoLimite
Botões por mensagematé 3
title do botão20 caracteres
id do botão256 caracteres
body.text1.024 caracteres
footer.text60 caracteres

Para listas com até 10 opções e outros formatos, veja mensagens interativas.

6. Testar localmente

Exporte as variáveis e suba o servidor em modo de desenvolvimento, com recarga automática:

export VERIFY_TOKEN="um-texto-qualquer"
export ACCESS_TOKEN="EAAG..."
export APP_SECRET="sua-chave-secreta"
export PHONE_NUMBER_ID="123456789012345"
fastapi dev main.py

Teste a verificação e um evento assinado sem depender da Meta:

curl "http://127.0.0.1:8000/webhook?hub.mode=subscribe&hub.verify_token=um-texto-qualquer&hub.challenge=42"

BODY='{"entry":[{"changes":[{"value":{"messages":[{"from":"5511999999999","id":"wamid.teste1","type":"text","text":{"body":"menu"}}]}}]}]}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$APP_SECRET" | awk '{print $NF}')
curl -X POST http://127.0.0.1:8000/webhook \
  -H "Content-Type: application/json" \
  -H "X-Hub-Signature-256: sha256=$SIG" \
  --data-binary "$BODY"

O primeiro comando deve devolver 42. O segundo devolve {"ok":true} e faz o bot enviar o menu para o número do campo from (use o seu número de teste).

Para receber eventos reais, a Meta exige uma URL pública com certificado TLS válido. Em desenvolvimento, um túnel resolve: o guia testar o webhook localmente com ngrok mostra o passo a passo. Com a URL pública em mãos, vá em Painel de Apps > WhatsApp > Configuração, preencha a URL de callback (https://.../webhook) e o token de verificação, salve e assine o campo messages.

7. Deploy simples com Docker e HTTPS

Crie o requirements.txt:

fastapi[standard]
httpx

E o Dockerfile:

FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY main.py .
CMD ["fastapi", "run", "main.py", "--port", "8000"]

fastapi run é o modo de produção, sem recarga automática. Em um servidor com Docker:

docker build -t bot-whatsapp .
docker run -d --restart unless-stopped -p 127.0.0.1:8000:8000 \
  -e VERIFY_TOKEN -e ACCESS_TOKEN -e APP_SECRET -e PHONE_NUMBER_ID \
  bot-whatsapp

Falta o HTTPS. Uma opção simples é o Caddy, que obtém e renova o certificado sozinho quando o DNS do domínio aponta para o servidor e as portas 80 e 443 estão abertas. O Caddyfile inteiro tem duas linhas:

bot.suaempresa.com.br
reverse_proxy 127.0.0.1:8000

Atualize a URL de callback no painel para https://bot.suaempresa.com.br/webhook.

Checklist antes de colocar em produção

  • Deduplicação persistente. O set em memória some a cada reinício e não é compartilhado entre processos. Guarde os IDs processados no Redis ou no banco, com expiração.
  • Fila. Se a resposta depende de IA, banco ou sistemas lentos, troque o BackgroundTasks por uma fila (Redis, RabbitMQ, SQS) com worker separado.
  • Um processo por enquanto. Com o set em memória, rode um único worker. Ao escalar, mova o estado para fora do processo.
  • Logs dos erros da API. O código registra o corpo de erro da Meta. A tabela de erros da API do WhatsApp ajuda a interpretar os códigos.
  • Segredos fora do código. Token e chave secreta só em variáveis de ambiente ou num cofre de segredos.
  • Mídia. Imagens, áudios e documentos chegam no webhook só com um ID; o download é uma chamada separada à API de mídia.

Como fazer com a Vectalk

A Vectalk é um proxy da WhatsApp Cloud API oficial: os payloads de envio e os eventos de webhook são os mesmos da Meta, então o main.py funciona sem mudanças de lógica. Mudam três coisas:

  1. URL e token. Defina API_URL=https://apis.vectalk.com.br/api/messages e use em ACCESS_TOKEN um token de instância gerado na Vectalk. Com ele, o phone_number_id é resolvido automaticamente.
  2. Cadastro do webhook por API. Você registra a URL com POST https://apis.vectalk.com.br/api/webhooks, informando instance_id, url e name, e pode ter várias URLs por instância.
  3. Proteção do endpoint. A documentação de webhooks da Vectalk não descreve um cabeçalho de assinatura. Deixe APP_SECRET vazio, defina WEBHOOK_KEY com um valor aleatório longo e cadastre a URL como https://bot.suaempresa.com.br/webhook?key=SEU_VALOR.
curl -X POST https://apis.vectalk.com.br/api/webhooks \
  -H "Authorization: Bearer SEU_TOKEN_VECTALK" \
  -H "Content-Type: application/json" \
  -d '{"instance_id": "SUA_INSTANCIA", "url": "https://bot.suaempresa.com.br/webhook?key=SEU_VALOR", "name": "Bot Python"}'

A Vectalk espera 200 em menos de 20 segundos e reenvia o evento se não receber, o que o código já trata com a deduplicação. Crie sua conta em apps.vectalk.com.br/register e consulte a documentação para os demais endpoints. Se o próximo passo é responder com IA, o tutorial ChatGPT na API oficial do WhatsApp parte de uma base parecida com esta.

Perguntas frequentes

Preciso de uma biblioteca específica de WhatsApp para Python?

Não. A Cloud API é HTTP com JSON, então FastAPI para receber e httpx (ou requests) para enviar resolvem. Bibliotecas de terceiros podem poupar código, mas acompanhar a API direto deixa você livre para usar recursos novos assim que a Meta os lança.

O bot pode iniciar a conversa?

Texto livre e botões só funcionam dentro da janela de 24 horas aberta pela última mensagem do cliente. Para iniciar uma conversa ou responder depois desse prazo, é preciso enviar um template aprovado.

Quanto custa manter um bot respondendo?

Respostas dentro da janela de atendimento são mensagens de serviço. Desde 1º de outubro de 2026, cada número tem uma franquia de 1.000 mensagens de serviço por mês; a partir da 1.001ª, a tarifa para destinatários no Brasil é de R$ 0,0350 por mensagem.

Por que a verificação do webhook falha no painel?

As causas mais comuns são: a URL não é HTTPS com certificado válido, o VERIFY_TOKEN do servidor é diferente do digitado no painel, ou a rota devolve o desafio como JSON em vez de texto puro. O código acima usa PlainTextResponse justamente por isso.

Posso usar Flask ou Django em vez de FastAPI?

Pode. O que importa é a mesma sequência: responder ao GET de verificação, validar o HMAC sobre o corpo bruto, devolver 200 rápido e chamar o endpoint de mensagens. Em Flask, o corpo bruto vem de request.get_data().


Fontes oficiais consultadas em 28/10/2026: Create a webhook endpoint (Meta), Webhooks overview (Meta), Interactive reply buttons messages (Meta), Interactive messages webhook (Meta), Typing indicators (Meta), Business-scoped user IDs (Meta), Preços (Meta), FastAPI, Caddy: reverse proxy quick-start, Vectalk: webhooks, Vectalk: autenticação, Vectalk: botões de resposta.

Teste a API oficial do WhatsApp hoje

Crie sua conta, gere o token e envie a primeira mensagem seguindo o guia da documentação. Mesmos payloads e webhooks da Cloud API da Meta.