Dev

Send WhatsApp with Python: REST API example (requests and httpx)

To send WhatsApp with Python you POST with requests to /api/v1/{instanceId}/send/text, with the x-api-key and x-api-secret headers and a JSON body with number and text. That is about ten lines and no WhatsApp-specific library. This guide goes from the basic send to retry, file sending, batches with a pause and a FastAPI webhook to receive replies.

Published on · 3 min read

This article is also available in: Português · Español

What you need before writing code

A ZapZap account, an instance connected by QR code, the API key and secret and the instance ID. Authentication uses two headers, x-api-key and x-api-secret, on every request. The destination number goes with country code and digits only, for example 5511999999999.

Test with cURL first. If cURL works and Python does not, the bug is in the code; if cURL also fails, the problem is the account, the instance or the number.

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."}'

How to send a text message with Python and requests

Install with pip install requests and keep credentials in environment variables, never in code. Use requests.Session to reuse the connection and always pass a timeout: without it, a call can hang forever.

A successful response carries messageId and status. Keep the messageId if you want to match it later with the delivery events that arrive through the 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."))

How to send an image, PDF and audio with Python

The media route is /send/media. The type field sets the kind (image, video, audio, ptt, document, sticker and others), file takes the public URL of the file, text becomes the caption and docName names the document. For a PDF, use type document.

The file must be at a URL reachable from the internet. If your file is only on disk, upload it to storage with a temporary link and pass that 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()

How to make async calls with httpx and retry with backoff

In async applications (FastAPI, workers), use httpx.AsyncClient. Retry only transient errors (429 and 5xx) and with growing waits; 400, 401 and 403 point to a problem in the request or the credentials, and repeating does not fix it.

Limit concurrency with asyncio.Semaphore. Firing hundreds of calls at once is bad for your app and for the number, because bursts are a pattern WhatsApp watches.

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ã."}))

How to receive replies with a FastAPI webhook

Set the URL in configure webhook and turn on the messages event. Each delivery arrives as a POST with a JSON envelope; the incoming message is in data.message, with the text in text, the contact in chatid and the fromMe flag. Ignore messages where fromMe is true, or you will answer yourself.

Reply 2xx fast and process later. Signature validation (X-ZapZap-Signature), deduplication and retry are in the webhooks in production guide. Check the real event format in a log before mapping fields.

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 before going to production

Credentials in environment variables; a timeout on every call; retry only on 429 and 5xx; limited concurrency; a pause between messages in a batch; logging of status and body on failures; a webhook with validated signature; and sending only to people who agreed to receive. The anti-ban page explains what ZapZap does by default (typing and delay) and what stays your responsibility.

For other languages, see the guides for Node.js, PHP, Ruby, Go and Java. The full endpoint list is in the API reference.

Quick summary

  • One POST to /send/text with x-api-key, x-api-secret and a JSON body with number and text already sends the message.
  • Use Session or AsyncClient, always with a timeout, and retry only 429 and 5xx.
  • Media goes through /send/media with type, file (public URL) and docName.
  • In the webhook, ignore fromMe, reply fast and validate the signature.

Also read

Frequently asked questions

Is there an official ZapZap Python library?

It is not needed. The API is REST, so requests or httpx are enough. If you prefer a generated client, the OpenAPI file at /openapi/zapzap-openapi.en.json can feed an SDK generator.

How do I send to a group?

The number field accepts the group identifier in place of a phone number. Check the format in the send/text reference before automating.

Can I send without Python waiting for the response?

Yes: put the send in a queue (Celery, RQ, arq) or use httpx with asyncio. The safest design is for your app to enqueue the request and a worker to make the call, so a slow send does not block the user's request.

Can using an unofficial API get my number banned?

It can. An unofficial API goes against WhatsApp's terms and the number can be limited or banned. Opt-in, a human pace, warm-up and a clean list reduce the risk, but there is no guarantee. For a sanctioned channel, use Meta's official API.

Test the code on your instance

Create the account, connect a number by QR code and run the example. R$15 per active number per month, no per-message fee.