Dev

How to receive and download WhatsApp audio, images and PDFs via webhook

To download audio, an image or a PDF received on WhatsApp, the webhook tells you the message arrived and you call POST /api/v1/{instanceId}/message/download with the message's id field. The API returns the file URL (fileURL) and the mimetype, or the content in base64 if you ask. Below is the whole flow: configure the event, identify the type, download, store and, if needed, transcribe.

Published on · 6 min read

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

How the incoming media flow works

There are three steps. One: the messages event reaches your webhook with the message; when there is media, the event describes the type and carries the message ID. Two: your server calls /message/download with that id. Three: you copy the file to your own storage and process it.

The split exists for a practical reason: the webhook has to be light and answer 2xx fast, and files are heavy. So the webhook delivers the notice, and the download happens in your worker, outside the HTTP response path. For the overall webhook design, see the guide on webhooks in production.

How to configure the webhook to receive messages

Register the instance's public (HTTPS) URL with POST to /webhook, sending url, enabled and the events list. Events are off by default; for media, enable messages. High-volume events such as presence and history should only be turned on if you will handle them.

Test delivery with POST /instances/{id}/webhook/test: it sends a test event and reports your endpoint's HTTP status and response time. If something does not arrive, GET /instances/{id}/webhook/deliveries shows the latest deliveries and GET /webhook/errors lists the failures.

cURL

curl -X POST https://api.zapzapapi.com/api/v1/SEU_INSTANCE_ID/webhook \
  -H "Content-Type: application/json" \
  -H "x-api-key: SUA_API_KEY" \
  -H "x-api-secret: SEU_API_SECRET" \
  -d '{"url":"https://seu-servidor.com/webhook/whatsapp","enabled":true,"events":["messages"]}'

What to look at in the event to tell audio, image or PDF

Check the exact payload shape in a real event from your instance: write the raw body to a log, send a photo, an audio clip and a PDF from your phone and compare. Field names vary by message type, and it is better to map from what you saw than from a generic example.

What you need to extract in every case: the message ID, the type (text, image, audio, document), the sender and whether the message was sent by you (fromMe). Discard fromMe right at the start. The examples below read defensively with fallback values; adjust the names after you see your payload.

How to download the media with /message/download in Node.js

The call takes id and optional flags. With return_link true (the default), the response carries fileURL and mimetype. With return_base64 true, it carries the content in base64 in the response itself; use that only for small files. generate_mp3 converts audio to MP3 and transcribe asks for the transcript, which the documentation describes as best-effort, so it may not come back.

The server below answers 200 before downloading, extracts the ID, calls the download and writes the file to disk. In production, replace the disk write with an upload to your object storage, and do not treat the fileURL as permanent: copy the file to your side.

Node.js

import express from "express";
import { writeFile } from "node:fs/promises";

const app = express();
app.use(express.json({ limit: "2mb" }));

const BASE = "https://api.zapzapapi.com/api/v1/" + process.env.ZAPZAP_INSTANCE_ID;
const headers = {
  "Content-Type": "application/json",
  "x-api-key": process.env.ZAPZAP_API_KEY,
  "x-api-secret": process.env.ZAPZAP_API_SECRET,
};

app.post("/webhook/whatsapp", (req, res) => {
  res.sendStatus(200);
  tratar(req.body).catch((e) => console.error("handler error", e));
});

async function tratar(evento) {
  const msg = evento?.message ?? evento?.data ?? evento;
  if (msg?.fromMe) return;

  const id = msg?.messageid ?? msg?.id;
  const tipo = String(msg?.messageType ?? msg?.type ?? "");
  if (!id || !/image|audio|ptt|document|video/i.test(tipo)) return;

  const r = await fetch(BASE + "/message/download", {
    method: "POST",
    headers,
    body: JSON.stringify({ id, return_link: true }),
  });
  if (!r.ok) throw new Error("download failed " + r.status + ": " + (await r.text()));
  const { fileURL, mimetype } = await r.json();

  const arquivo = await fetch(fileURL);
  const bytes = Buffer.from(await arquivo.arrayBuffer());
  const ext = (mimetype || "application/octet-stream").split("/")[1]?.split(";")[0] || "bin";
  await writeFile("./midia-" + id.replace(/[^a-zA-Z0-9_-]/g, "_") + "." + ext, bytes);
}

app.listen(3000);

How to download the media with /message/download in Python

The same logic in Python with Flask and requests. The point of attention is the same: answer fast and download in the background. The example uses a simple thread to stay short; in production use a queue (Celery, RQ, Cloud Tasks or equivalent).

If you only need the text of an audio clip, pass transcribe true in the download call, knowing it is best-effort, or use the instance's AI module for guaranteed transcription, as the endpoint's own reference says.

Python

import os
import threading
import requests
from flask import Flask, request

app = Flask(__name__)
BASE = "https://api.zapzapapi.com/api/v1/" + os.environ["ZAPZAP_INSTANCE_ID"]
HEADERS = {
    "Content-Type": "application/json",
    "x-api-key": os.environ["ZAPZAP_API_KEY"],
    "x-api-secret": os.environ["ZAPZAP_API_SECRET"],
}


def tratar(evento):
    msg = evento.get("message") or evento.get("data") or evento
    if msg.get("fromMe"):
        return
    msg_id = msg.get("messageid") or msg.get("id")
    tipo = str(msg.get("messageType") or msg.get("type") or "")
    if not msg_id or not any(t in tipo.lower() for t in ("image", "audio", "ptt", "document", "video")):
        return

    r = requests.post(BASE + "/message/download", json={"id": msg_id, "return_link": True}, headers=HEADERS, timeout=60)
    r.raise_for_status()
    dados = r.json()

    arquivo = requests.get(dados["fileURL"], timeout=60)
    arquivo.raise_for_status()
    ext = dados.get("mimetype", "application/octet-stream").split("/")[-1].split(";")[0]
    with open("midia-%s.%s" % (str(msg_id).replace(":", "_"), ext), "wb") as f:
        f.write(arquivo.content)


@app.post("/webhook/whatsapp")
def webhook():
    threading.Thread(target=tratar, args=(request.get_json(force=True),), daemon=True).start()
    return "", 200

How to find an old message and download its media

If you missed the event (server down, for instance), recover the message with POST /message/find. Filter by id for a specific message or by chatid for a conversation's history, with limit and offset to paginate. Then use the returned id in /message/download.

This also covers reprocessing: if the download failed, the id was saved and you repeat the call later, without depending on a new webhook event.

cURL

curl -X POST https://api.zapzapapi.com/api/v1/SEU_INSTANCE_ID/message/find \
  -H "Content-Type: application/json" \
  -H "x-api-key: SUA_API_KEY" \
  -H "x-api-secret: SEU_API_SECRET" \
  -d '{"chatid":"5511999999999@s.whatsapp.net","limit":20,"offset":0}'
NeedParameterNote
Store the file in your own storagereturn_link trueDownload from fileURL and copy to your side
Small file directly in the responsereturn_base64 trueAvoid for large files
Play audio in a browser or convert itgenerate_mp3 trueReturns the audio as MP3
Text of the audiotranscribe trueBest-effort, may not return
Guaranteed transcriptThe instance's AI moduleSee the endpoint reference

When in doubt, start with return_link and treat the rest as optimization. PDFs and images need no conversion; voice audio is usually the only one that needs MP3 to play everywhere.

How to design the queue between the webhook and the download worker

In production, the webhook endpoint does three things and only three: it validates the event format, writes the message id and type to a queue (database, Redis, managed queue) and answers 2xx. A separate worker consumes the queue, calls /message/download, copies the file to your storage and marks the item as done.

This design solves three problems at once. A failed download becomes a retry with backoff without losing the event. A spike of messages (a campaign that triggers many replies) does not take the endpoint down. And deduplication gets simple: the message id is the queue's unique key, and a second identical event is ignored.

StepWhere it runsIf it fails
Receive the eventWebhook endpointThe API redelivers or you check GET /webhook/errors
Write to the queueWebhook endpointAnswer 5xx to trigger redelivery
Download the mediaWorkerRetry with backoff; keep the id to try later
Copy to storageWorkerRepeat; the source URL may expire, so do not wait long
Process (OCR, transcription, AI)WorkerMark as error and alert

Good practices: idempotency, security and privacy

Idempotency: the same event may arrive twice. Keep the message id and ignore ids already processed, so you do not download and process the same file twice. Security: treat every received file as untrusted. Validate the file's real type (not just the extension), limit the size and never execute or open documents on the server without antivirus or isolation.

Privacy: customers' audio, photos and PDFs are personal data and can be sensitive. Define how long you keep them, who accesses them and how they are deleted, and record the legal basis under data protection law. The bulk messaging and data protection guide covers the general reasoning. To debug webhook delivery, see the webhook docs and the message/download reference.

Quick summary

  • The webhook tells you media arrived; the file comes from POST /message/download with the message id.
  • Enable the messages event in POST /webhook and answer 2xx before downloading.
  • Use return_link to store the file; base64 only for small files; transcribe is best-effort.
  • POST /message/find recovers missed messages by id or by chatid.
  • Treat the file as untrusted, keep the id for idempotency and set a retention period because of data protection law.

Also read

Frequently asked questions

How do I download an image received on WhatsApp through the API?

Take the message ID from the webhook event and call POST /message/download with id and return_link true. The response carries fileURL and mimetype; download the file from that URL and copy it to your storage.

Does the webhook already send the file with the event?

The event announces the arrival and describes the message; the file itself comes from the download call. This keeps the webhook light and lets you download outside the HTTP response path.

How do I transcribe a received audio clip?

The download call accepts transcribe true, but the reference describes it as best-effort: it may not return. For guaranteed transcription, use the instance's AI module.

What is the file size limit?

For sending, the reference indicates about 190 MB for image, video and audio and about 500 MB for documents when sent by URL, and about 2 MB when sent in base64. For downloads, prefer return_link over base64.

Is the fileURL permanent?

Do not assume so. Copy the file to your storage as soon as you download it. If you need the file later, keep the message id and call the download again.

Receive WhatsApp media in your system

Create your account, verify your phone and get the welcome credit, no deposit needed (one per phone, subject to the current campaign). Connect by QR code, enable the messages event and test with a photo.