Dev

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

Este artigo também está disponível em: English · Español

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.

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.