Enviar WhatsApp com Python: exemplo de API REST (requests e httpx)
Para enviar WhatsApp com Python você faz um POST com requests em /api/v1/{instanceId}/send/text, com os cabeçalhos x-api-key e x-api-secret e um JSON com number e text. São cerca de dez linhas e nenhuma biblioteca específica de WhatsApp. Este guia segue do envio básico até retry, envio de arquivo, lote com intervalo e um webhook em FastAPI para receber as respostas.
Publicado em · 4 min de leitura
O que você precisa antes de escrever código
Uma conta no ZapZap, uma instância conectada por QR code, a chave e o segredo da API e o ID da instância. A autenticação usa dois cabeçalhos, x-api-key e x-api-secret, em toda requisição. O número de destino vai com DDI e só dígitos, por exemplo 5511999999999.
Teste primeiro com cURL. Se o cURL funciona e o Python não, o erro está no código; se o cURL também falha, o problema está na conta, na instância ou no número.
cURL
curl -X POST https://api.zapzapapi.com/api/v1/SEU_INSTANCE_ID/send/text \
-H "Content-Type: application/json" \
-H "x-api-key: SUA_API_KEY" \
-H "x-api-secret: SEU_API_SECRET" \
-d '{"number":"5511999999999","text":"Olá! Teste de envio pela API."}'Como enviar uma mensagem de texto com Python e requests
Instale com pip install requests e guarde as credenciais em variáveis de ambiente, nunca no código. Use requests.Session para reaproveitar a conexão e sempre passe timeout: sem ele, uma chamada pode ficar pendurada para sempre.
A resposta de sucesso traz messageId e status. Guarde o messageId se quiser cruzar depois com os eventos de entrega que chegam pelo webhook.
Python (requests)
import os
import requests
BASE = "https://api.zapzapapi.com/api/v1/" + os.environ["ZAPZAP_INSTANCE_ID"]
sessao = requests.Session()
sessao.headers.update({
"x-api-key": os.environ["ZAPZAP_API_KEY"],
"x-api-secret": os.environ["ZAPZAP_API_SECRET"],
})
def enviar_texto(numero: str, texto: str) -> dict:
r = sessao.post(
BASE + "/send/text",
json={"number": numero, "text": texto},
timeout=15,
)
r.raise_for_status()
return r.json() # {"messageId": "...", "status": "sent"}
if __name__ == "__main__":
print(enviar_texto("5511999999999", "Olá! Seu pedido saiu para entrega."))Como enviar imagem, PDF e áudio com Python
A rota de mídia é /send/media. O campo type define o tipo (image, video, audio, ptt, document, sticker e outros), file recebe a URL pública do arquivo, text vira a legenda e docName dá o nome do documento. Para PDF, use type document.
O arquivo precisa estar numa URL acessível pela internet. Se o seu arquivo está só no disco, suba para um armazenamento com link temporário e passe esse link.
Python (requests)
def enviar_pdf(numero: str, url: str, nome: str, legenda: str = "") -> dict:
r = sessao.post(
BASE + "/send/media",
json={"number": numero, "type": "document", "file": url,
"docName": nome, "text": legenda},
timeout=60,
)
r.raise_for_status()
return r.json()Como fazer chamadas assíncronas com httpx e retry com backoff
Em aplicações assíncronas (FastAPI, workers), use httpx.AsyncClient. Faça retry só para erros transitórios (429 e 5xx) e com espera crescente; erros 400, 401 e 403 indicam problema no pedido ou nas credenciais e repetir não resolve.
Limite a concorrência com asyncio.Semaphore. Disparar centenas de chamadas de uma vez é ruim para a sua aplicação e para o número, porque mensagens em rajada são um padrão que o WhatsApp observa.
Python (httpx)
import asyncio, os, random
import httpx
BASE = "https://api.zapzapapi.com/api/v1/" + os.environ["ZAPZAP_INSTANCE_ID"]
HEADERS = {
"x-api-key": os.environ["ZAPZAP_API_KEY"],
"x-api-secret": os.environ["ZAPZAP_API_SECRET"],
}
limite = asyncio.Semaphore(3)
async def enviar(cliente: httpx.AsyncClient, numero: str, texto: str) -> dict:
for tentativa in range(4):
async with limite:
r = await cliente.post(BASE + "/send/text",
json={"number": numero, "text": texto})
if r.status_code < 400:
return r.json()
if r.status_code not in (429, 500, 502, 503, 504):
r.raise_for_status() # erro do pedido: não adianta repetir
await asyncio.sleep(2 ** tentativa + random.random())
r.raise_for_status()
async def main(contatos: dict[str, str]):
async with httpx.AsyncClient(headers=HEADERS, timeout=15) as c:
for numero, texto in contatos.items():
print(await enviar(c, numero, texto))
await asyncio.sleep(random.uniform(2, 6)) # intervalo entre mensagens
asyncio.run(main({"5511999999999": "Olá! Confirmando sua consulta de amanhã."}))Como receber as respostas com um webhook em FastAPI
Configure a URL em configurar webhook e ligue o evento messages. Cada entrega chega como POST com um envelope JSON; a mensagem recebida fica em data.message, com o texto em text, o contato em chatid e a flag fromMe. Ignore as mensagens com fromMe verdadeiro, senão você responde a si mesmo.
Responda 2xx rápido e processe depois. A validação da assinatura X-ZapZap-Signature, a deduplicação e o retry estão no guia de webhook em produção. Confira o formato real do evento em um log antes de mapear campos.
Python (FastAPI)
from fastapi import BackgroundTasks, FastAPI, Request
app = FastAPI()
def tratar(evento: dict):
msg = (evento.get("data") or {}).get("message") or {}
if evento.get("event") != "messages" or msg.get("fromMe"):
return
print("mensagem de", msg.get("chatid"), ":", msg.get("text"))
@app.post("/webhooks/zapzap")
async def webhook(req: Request, tarefas: BackgroundTasks):
evento = await req.json()
tarefas.add_task(tratar, evento) # trabalha depois de responder
return {"ok": True}Checklist antes de colocar em produção
Credenciais em variáveis de ambiente; timeout em toda chamada; retry só em 429 e 5xx; concorrência limitada; intervalo entre mensagens de um mesmo lote; log do status e do corpo das falhas; webhook com assinatura validada; e só enviar para quem aceitou receber. A página de anti-bloqueio explica o que o ZapZap faz por padrão (digitando e atraso) e o que continua sendo responsabilidade sua.
Para outras linguagens, veja os guias de Node.js, PHP, Ruby, Go e Java. A coleção completa de endpoints está na referência da API.
Resumo rápido
- Um POST em /send/text com x-api-key, x-api-secret e um JSON com number e text já envia a mensagem.
- Use Session ou AsyncClient, sempre com timeout, e retry só para 429 e 5xx.
- Mídia vai por /send/media com type, file (URL pública) e docName.
- No webhook, ignore fromMe, responda rápido e valide a assinatura.
Continue lendo
Perguntas frequentes
Existe uma biblioteca Python oficial do ZapZap?
Não é necessária. A API é REST, então requests ou httpx bastam. Se você prefere um cliente gerado, o arquivo OpenAPI em /openapi/zapzap-openapi.pt.json pode alimentar um gerador de SDK.
Como enviar para um grupo?
O campo number aceita o identificador do grupo no lugar de um telefone. Confira o formato na referência de send/text antes de automatizar.
Posso enviar sem o Python esperar a resposta?
Pode: coloque o envio numa fila (Celery, RQ, arq) ou use httpx com asyncio. O mais seguro é a sua aplicação enfileirar o pedido e um worker fazer a chamada, assim lentidão no envio não derruba a requisição do usuário.
Usar a API não oficial pode levar a bloqueio do número?
Pode. Uma API não oficial viola os termos do WhatsApp e o número pode ser limitado ou bloqueado, ainda que existam práticas que reduzem o risco: opt-in, ritmo humano, aquecimento e lista limpa. Não existe garantia, e quem precisa de canal homologado deve usar a API oficial da Meta.
Teste o código na sua instância
Crie a conta, conecte um número por QR code e rode o exemplo. R$15 por número ativo ao mês, sem cobrança por mensagem.
