Enviar WhatsApp con Python: ejemplo de API REST (requests y httpx)
Para enviar WhatsApp con Python haces un POST con requests a /api/v1/{instanceId}/send/text, con los encabezados x-api-key y x-api-secret y un JSON con number y text. Son unas diez líneas y ninguna biblioteca específica de WhatsApp. Esta guía va del envío básico a retry, envío de archivos, lotes con intervalo y un webhook en FastAPI para recibir las respuestas.
Publicado el · 4 min de lectura
Este artículo también está disponible en: Português · English
Qué necesitas antes de escribir código
Una cuenta en ZapZap, una instancia conectada por código QR, la clave y el secreto de la API y el ID de la instancia. La autenticación usa dos encabezados, x-api-key y x-api-secret, en cada solicitud. El número de destino va con código de país y solo dígitos, por ejemplo 5511999999999.
Prueba primero con cURL. Si cURL funciona y Python no, el error está en el código; si cURL también falla, el problema está en la cuenta, la instancia o el 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."}'Cómo enviar un mensaje de texto con Python y requests
Instala con pip install requests y guarda las credenciales en variables de entorno, nunca en el código. Usa requests.Session para reutilizar la conexión y pasa siempre timeout: sin él, una llamada puede quedar colgada para siempre.
La respuesta de éxito trae messageId y status. Guarda el messageId si quieres cruzarlo luego con los eventos de entrega que llegan por el 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."))Cómo enviar imagen, PDF y audio con Python
La ruta de multimedia es /send/media. El campo type define el tipo (image, video, audio, ptt, document, sticker y otros), file recibe la URL pública del archivo, text es el pie y docName da el nombre del documento. Para un PDF, usa type document.
El archivo debe estar en una URL accesible desde internet. Si solo está en disco, súbelo a un almacenamiento con enlace temporal y pasa ese enlace.
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()Cómo hacer llamadas asíncronas con httpx y retry con backoff
En aplicaciones asíncronas (FastAPI, workers) usa httpx.AsyncClient. Repite solo errores transitorios (429 y 5xx) y con espera creciente; 400, 401 y 403 indican un problema en la solicitud o en las credenciales, y repetir no lo resuelve.
Limita la concurrencia con asyncio.Semaphore. Disparar cientos de llamadas a la vez es malo para tu aplicación y para el número, porque las ráfagas son un patrón que 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ã."}))Cómo recibir las respuestas con un webhook en FastAPI
Configura la URL en configurar webhook y activa el evento messages. Cada entrega llega como POST con un sobre JSON; el mensaje recibido está en data.message, con el texto en text, el contacto en chatid y la marca fromMe. Ignora los mensajes con fromMe verdadero, o te responderás a ti mismo.
Responde 2xx rápido y procesa después. La validación de la firma X-ZapZap-Signature, la deduplicación y el retry están en la guía de webhook en producción. Comprueba el formato real del evento en un 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}Lista de verificación antes de producción
Credenciales en variables de entorno; timeout en cada llamada; retry solo en 429 y 5xx; concurrencia limitada; intervalo entre mensajes de un mismo lote; log del estado y del cuerpo de los fallos; webhook con firma validada; y enviar solo a quien aceptó recibir. La página de anti-bloqueo explica lo que ZapZap hace por defecto (escribiendo y retraso) y lo que sigue siendo tu responsabilidad.
Para otros lenguajes, mira las guías de Node.js, PHP, Ruby, Go y Java. La lista completa de endpoints está en la referencia de la API.
Resumen rápido
- Un POST a /send/text con x-api-key, x-api-secret y un JSON con number y text ya envía el mensaje.
- Usa Session o AsyncClient, siempre con timeout, y repite solo 429 y 5xx.
- La multimedia va por /send/media con type, file (URL pública) y docName.
- En el webhook, ignora fromMe, responde rápido y valida la firma.
Sigue leyendo
Lee también
Preguntas frecuentes
¿Existe una biblioteca oficial de ZapZap para Python?
No hace falta. La API es REST, así que requests o httpx bastan. Si prefieres un cliente generado, el archivo OpenAPI en /openapi/zapzap-openapi.es.json puede alimentar un generador de SDK.
¿Cómo envío a un grupo?
El campo number acepta el identificador del grupo en lugar de un teléfono. Revisa el formato en la referencia de send/text antes de automatizar.
¿Puedo enviar sin que Python espere la respuesta?
Sí: pon el envío en una cola (Celery, RQ, arq) o usa httpx con asyncio. Lo más seguro es que tu aplicación encole la solicitud y un worker haga la llamada, así un envío lento no bloquea la solicitud del usuario.
¿Usar una API no oficial puede llevar al bloqueo del número?
Puede. Una API no oficial va contra los términos de WhatsApp y el número puede ser limitado o bloqueado. Opt-in, ritmo humano, calentamiento y lista limpia reducen el riesgo, pero no hay garantía. Para un canal homologado, usa la API oficial de Meta.
Prueba el código en tu instancia
Crea la cuenta, conecta un número por código QR y ejecuta el ejemplo. R$15 por número activo al mes, sin cobro por mensaje.
