API de WhatsApp em Python: enviar com httpx e receber com FastAPI
Em Python você integra a API de WhatsApp da D-API com o cliente HTTP que já usa, requests ou httpx, porque tudo é REST com JSON. O ponto que mais dá problema não é o envio, é o webhook: ele precisa responder rápido e deixar o processamento para depois, e é nisso que este guia foca.
O que você precisa ter em mãos
Não existe pacote Python oficial, e ele não faz falta. A API de WhatsApp da D-API responde em https://api.d-api.cloud, recebe JSON e autentica pelo header Authorization com a sua API Key pura, sem Bearer. Qualquer biblioteca HTTP funciona. Antes de escrever código, separe:
- a API Key do painel, em uma variável de ambiente como
DAPI_API_KEY; - o
sessionIdde um número já conectado (a conexão é feita lendo um QR Code, como no WhatsApp Web); - uma URL pública para o webhook. Em desenvolvimento, um túnel como ngrok ou cloudflared expõe o seu localhost.
Os exemplos usam Python 3.10 ou superior, por causa da sintaxe de tipos com barra vertical nos modelos Pydantic. Em versões anteriores, troque por Optional do módulo typing e o resto continua igual.
Enviar mensagem com requests
Para um script, uma tarefa agendada ou um Django síncrono, requests resolve. O detalhe que costuma passar batido: requests não define timeout por conta própria. Sem ele, um problema de rede deixa o worker parado esperando para sempre.
import os
import requests
API = "https://api.d-api.cloud"
HEADERS = {"Authorization": os.environ["DAPI_API_KEY"]}
resp = requests.post(
f"{API}/api/v1/messages/send/text",
json={"sessionId": "financeiro", "to": "5511999999999", "text": "Seu boleto vence amanhã."},
headers=HEADERS,
timeout=(3, 10), # 3s para conectar, 10s para ler a resposta
)
if not resp.ok:
erro = resp.json() if "application/json" in resp.headers.get("content-type", "") else {}
raise RuntimeError(f"D-API {resp.status_code}: {erro.get('error', resp.text)}")Um client reutilizável com httpx
Em código assíncrono, e em qualquer aplicação que envia com frequência, vale ter um client único com pool de conexões. O httpx oferece a mesma API em versão síncrona e assíncrona, e aceita base_url, headers padrão e timeout configurados uma vez só. A função abaixo converte o corpo de erro da API, que tem o formato {"success": false, "error": "...", "statusCode": 400}, em uma exceção própria.
import os
import httpx
class DApiError(Exception):
def __init__(self, status: int, message: str):
super().__init__(f"{status}: {message}")
self.status = status
dapi = httpx.AsyncClient(
base_url="https://api.d-api.cloud",
headers={"Authorization": os.environ["DAPI_API_KEY"]},
timeout=httpx.Timeout(10.0, connect=3.0),
)
async def enviar(caminho: str, payload: dict) -> dict:
try:
r = await dapi.post(caminho, json=payload)
except httpx.TimeoutException as exc:
raise DApiError(408, "sem resposta dentro do timeout") from exc
if r.is_error:
corpo = r.json() if r.headers.get("content-type", "").startswith("application/json") else {}
raise DApiError(r.status_code, corpo.get("error", r.text))
return r.json()
# texto
await enviar("/api/v1/messages/send/text",
{"sessionId": "financeiro", "to": "5511999999999", "text": "Pagamento confirmado."})
# PDF por URL pública
await enviar("/api/v1/messages/send/document",
{"sessionId": "financeiro", "to": "5511999999999",
"document": "https://seu-app.com/boletos/4812.pdf", "fileName": "boleto-4812.pdf"})Imagem, áudio e vídeo seguem o mesmo padrão, trocando o caminho e o nome do campo. A lista completa de tipos e formatos aceitos está em como enviar mídia pela API.
Receber o webhook com FastAPI sem travar
Cada entrega do webhook espera uma resposta HTTP. Se o seu endpoint demorar, a D-API trata a tentativa como falha e reenvia mais tarde, e você passa a receber o mesmo evento repetido. Por isso a rota não deve chamar banco lento, IA ou outra API antes de responder. Ela valida, entrega o trabalho para outro lugar e devolve 200.
import hmac
import os
from typing import Any
from fastapi import BackgroundTasks, FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI()
WEBHOOK_TOKEN = os.environ["WEBHOOK_TOKEN"]
class Evento(BaseModel):
event: str
sessionId: str
timestamp: str
traceId: str | None = None
data: dict[str, Any]
@app.post("/webhooks/whatsapp/{token}")
async def webhook(token: str, evento: Evento, tarefas: BackgroundTasks):
if not hmac.compare_digest(token, WEBHOOK_TOKEN):
raise HTTPException(status_code=401)
if evento.event == "messages.received" and not evento.data.get("fromMe"):
tarefas.add_task(processar, evento)
return {"ok": True}
async def processar(evento: Evento) -> None:
dados = evento.data
numero = dados["from"]["jid"].split("@")[0]
if dados["type"] == "text" and "segunda via" in dados["message"].lower():
await enviar("/api/v1/messages/send/text",
{"sessionId": evento.sessionId, "to": numero,
"text": "Vou gerar sua segunda via, só um instante."})O token secreto no caminho substitui a assinatura, que o webhook de WhatsApp não tem. A comparação com hmac.compare_digest só evita que o tempo da comparação revele o token; não há HMAC no corpo da requisição. Para os campos de cada evento, consulte a página de webhook de WhatsApp.
Quando BackgroundTasks deixa de ser suficiente
O BackgroundTasks roda depois da resposta, no mesmo processo do Uvicorn. É ótimo para começar, mas tem limites claros:
| Situação | BackgroundTasks | Fila externa (Celery, RQ, arq) |
|---|---|---|
| Poucas mensagens por minuto | Atende bem | Exagero |
| Resposta gerada por IA, vários segundos por evento | Consome o event loop e a memória do web | Workers separados, escaláveis |
| Deploy ou reinício do servidor | Tarefas em andamento se perdem | Eventos ficam na fila |
| Várias réplicas da API | Cada réplica processa o que recebeu | Qualquer worker pega qualquer evento |
Na migração para fila, a rota continua quase igual: em vez de add_task, ela publica o evento com processar.delay(evento.model_dump()) no Celery ou equivalente. Se o seu caso é um bot que conversa com um modelo de linguagem, o guia de chatbot de WhatsApp com IA mostra como organizar contexto e histórico.
Idempotência e boas práticas no processamento
- Deduplique pelo id da mensagem. Um reenvio pode chegar depois que você já processou o evento. Gravar
data.idcomSET NXno Redis resolve em uma linha. - Ignore o que você mesmo mandou. Mensagens com
fromMeverdadeiro vêm do próprio número em outro aparelho e não pedem resposta automática. - Registre o traceId. Coloque o campo nos logs estruturados do worker; com ele fica fácil cruzar o que o seu sistema fez com o que a D-API entregou.
- Não bloqueie o loop. Dentro de função assíncrona, use o client httpx assíncrono. Chamar requests ali trava todas as outras rotas enquanto a resposta não chega.
Com o fluxo funcionando em um número, a mesma estrutura atende dezenas: o sessionId no evento diz de qual conexão veio a mensagem. A cobrança da D-API é por conexão, não por mensagem; os planos estão em preços.
Perguntas frequentes
Existe biblioteca Python oficial da D-API?
Não. O SDK oficial é só para Node.js. Em Python a integração é REST pura: requests, httpx ou aiohttp resolvem, porque cada operação é uma requisição HTTP com JSON e o header Authorization.
Por que minha chamada com requests ficou travada para sempre?
Porque requests não tem timeout padrão. Sem o parâmetro timeout, uma conexão que não responde segura a thread indefinidamente. Passe sempre uma tupla com tempo de conexão e de leitura, por exemplo timeout=(3, 10).
BackgroundTasks do FastAPI serve para produção?
Serve para volume baixo e tarefas curtas. A tarefa roda no mesmo processo e se perde se o servidor reiniciar. Com volume maior ou quando cada evento chama uma IA, grave o evento em uma fila como Celery, RQ ou arq.
Consigo usar Django ou Flask em vez de FastAPI?
Sim. O webhook é só um POST com JSON. Em Django é uma view com csrf_exempt, em Flask uma rota que lê request.get_json(). A regra continua a mesma: validar, enfileirar e responder rápido.
Como evito processar a mesma mensagem duas vezes?
Use o id que vem em data.id como chave. Antes de processar, tente gravar esse id no Redis com SET NX e um prazo de expiração, ou numa tabela com índice único. Se já existir, descarte o evento.
Continue lendo
Teste a API de WhatsApp da D-API
Trial de 3 dias com acesso completo. Sem cartão, sem fidelidade.