Dev

Cómo recibir y descargar audio, imagen y PDF de WhatsApp por webhook

Para descargar un audio, una imagen o un PDF recibido en WhatsApp, el webhook avisa que llegó el mensaje y tú llamas a POST /api/v1/{instanceId}/message/download con el campo id del mensaje. La API devuelve la URL del archivo (fileURL) y el mimetype, o el contenido en base64 si lo pides. Abajo está el flujo completo: configurar el evento, identificar el tipo, descargar, guardar y, si hace falta, transcribir.

Publicado el · 7 min de lectura

Este artículo también está disponible en: Português · English

Cómo funciona el flujo de multimedia recibida

Son tres pasos. Uno: el evento messages llega a tu webhook con el mensaje; cuando hay multimedia, el evento describe el tipo y trae el ID del mensaje. Dos: tu servidor llama a /message/download con ese id. Tres: copias el archivo a tu almacenamiento y lo procesas.

La separación existe por un motivo práctico: el webhook debe ser liviano y responder 2xx rápido, y un archivo es pesado. Por eso el webhook entrega el aviso, y la descarga ocurre en tu worker, fuera del camino de la respuesta HTTP. Para el diseño general del webhook, mira la guía de webhook en producción.

Cómo configurar el webhook para recibir mensajes

Registra la URL pública (HTTPS) de la instancia con POST a /webhook, enviando url, enabled y la lista events. Los eventos vienen desactivados por defecto; para multimedia, activa messages. Los eventos de alto volumen como presence e history solo actívalos si vas a tratarlos.

Prueba la entrega con POST /instances/{id}/webhook/test: envía un evento de prueba e informa el status HTTP y el tiempo de respuesta de tu endpoint. Si algo no llega, GET /instances/{id}/webhook/deliveries muestra las últimas entregas y GET /webhook/errors lista las fallas.

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

Qué mirar en el evento para saber si es audio, imagen o PDF

La forma exacta del payload debe comprobarse en un evento real de tu instancia: guarda el cuerpo crudo en un log, envía desde el celular una foto, un audio y un PDF y compara. Los nombres de campo varían según el tipo de mensaje, y es mejor mapear a partir de lo que viste que de un ejemplo genérico.

Lo que necesitas extraer en todos los casos: el ID del mensaje, el tipo (texto, imagen, audio, documento), el remitente y si el mensaje lo enviaste tú (fromMe). Descarta fromMe al comienzo. Los ejemplos de abajo usan lectura defensiva con valores alternativos; ajusta los nombres después de ver tu payload.

Cómo descargar la multimedia con /message/download en Node.js

La llamada recibe id y flags opcionales. Con return_link true (por defecto), la respuesta trae fileURL y mimetype. Con return_base64 true, trae el contenido en base64 en la propia respuesta; úsalo solo para archivos pequeños. generate_mp3 convierte el audio a MP3 y transcribe pide la transcripción, que la documentación describe como best-effort y puede no venir.

El servidor de abajo responde 200 antes de descargar, extrae el ID, llama a la descarga y escribe el archivo en disco. En producción, cambia la escritura en disco por una subida a tu almacenamiento de objetos y no trates la fileURL como permanente: copia el archivo a tu lado.

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);

Cómo descargar la multimedia con /message/download en Python

La misma lógica en Python con Flask y requests. El punto de atención es el mismo: responder rápido y descargar en segundo plano. El ejemplo usa un hilo simple para ser corto; en producción usa una cola (Celery, RQ, Cloud Tasks o equivalente).

Si solo necesitas el texto de un audio, pide transcribe true en la llamada de descarga, sabiendo que es best-effort, o usa el módulo de IA de la instancia para una transcripción garantizada, como indica la propia referencia del endpoint.

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

Cómo localizar un mensaje antiguo y descargar su multimedia

Si perdiste el evento (servidor caído, por ejemplo), recupera el mensaje con POST /message/find. Filtra por id para un mensaje específico o por chatid para el historial de una conversación, con limit y offset para paginar. Luego usa el id devuelto en /message/download.

Esto también resuelve el reprocesamiento: si la descarga falló, el id quedó guardado y repites la llamada después, sin depender de un nuevo evento del webhook.

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

Qué formato elegir: enlace, base64, MP3 o transcripción

NecesidadParámetroObservación
Guardar el archivo en tu almacenamientoreturn_link trueDescarga de la fileURL y copia a tu lado
Archivo pequeño directo en la respuestareturn_base64 trueEvítalo para archivos grandes
Reproducir audio en el navegador o convertirlogenerate_mp3 trueDevuelve el audio en MP3
Texto del audiotranscribe trueBest-effort, puede no devolverse
Transcripción garantizadaMódulo de IA de la instanciaVer la referencia del endpoint

Ante la duda, empieza por return_link y trata lo demás como optimización. El PDF y la imagen no necesitan conversión; el audio de voz suele ser el único que pide MP3 para sonar en cualquier lugar.

Cómo diseñar la cola entre el webhook y el worker de descarga

En producción, el endpoint del webhook hace tres cosas y solo tres: valida el formato del evento, guarda el id del mensaje y el tipo en una cola (base de datos, Redis, cola gestionada) y responde 2xx. Un worker aparte consume la cola, llama a /message/download, copia el archivo a tu almacenamiento y marca el ítem como concluido.

Este diseño resuelve tres problemas a la vez. Una descarga fallida se vuelve un reintento con backoff sin perder el evento. Un pico de mensajes (una campaña que genera muchas respuestas) no tumba el endpoint. Y la deduplicación queda simple: el id del mensaje es la clave única de la cola, y un segundo evento igual se ignora.

EtapaDónde correSi falla
Recibir el eventoEndpoint del webhookLa API reenvía o consultas GET /webhook/errors
Guardar en la colaEndpoint del webhookResponder 5xx para provocar la reentrega
Descargar la multimediaWorkerReintento con backoff; guardar el id para intentar después
Copiar al almacenamientoWorkerRepetir; la URL de origen puede expirar, no demores
Procesar (OCR, transcripción, IA)WorkerMarcar como error y alertar

Buenas prácticas: idempotencia, seguridad y privacidad

Idempotencia: el mismo evento puede llegar dos veces. Guarda el id del mensaje e ignora los ya procesados, para no descargar ni procesar el mismo archivo dos veces. Seguridad: trata todo archivo recibido como no confiable. Valida el tipo real del archivo (no solo la extensión), limita el tamaño y nunca ejecutes ni abras documentos en el servidor sin antivirus o aislamiento.

Privacidad: el audio, la foto y el PDF de los clientes son datos personales y pueden ser sensibles. Define por cuánto tiempo los guardas, quién accede y cómo se borran, y registra la base legal según la ley de protección de datos. La guía de envío masivo y protección de datos trae el razonamiento general. Para depurar la entrega del webhook, consulta la documentación de webhooks y la referencia de message/download.

Resumen rápido

  • El webhook avisa que llegó multimedia; el archivo viene de POST /message/download con el id del mensaje.
  • Activa el evento messages en POST /webhook y responde 2xx antes de descargar.
  • Usa return_link para guardar el archivo; base64 solo para archivos pequeños; transcribe es best-effort.
  • POST /message/find recupera mensajes perdidos por id o por chatid.
  • Trata el archivo como no confiable, guarda el id para la idempotencia y define retención por la protección de datos.

Lee también

Preguntas frecuentes

¿Cómo descargo una imagen recibida en WhatsApp por la API?

Toma el ID del mensaje del evento del webhook y llama a POST /message/download con id y return_link true. La respuesta trae fileURL y mimetype; descarga el archivo de esa URL y cópialo a tu almacenamiento.

¿El webhook ya envía el archivo junto con el evento?

El evento anuncia la llegada y describe el mensaje; el archivo en sí viene por la llamada de descarga. Eso mantiene el webhook liviano y permite descargar fuera del camino de la respuesta HTTP.

¿Cómo transcribo un audio recibido?

La llamada de descarga acepta transcribe true, pero la referencia lo describe como best-effort: puede no devolverse. Para una transcripción garantizada, usa el módulo de IA de la instancia.

¿Cuál es el límite de tamaño del archivo?

Para el envío, la referencia indica unos 190 MB para imagen, video y audio y unos 500 MB para documentos cuando se envían por URL, y unos 2 MB en base64. Para la descarga, prefiere return_link en vez de base64.

¿La fileURL es permanente?

No lo asumas. Copia el archivo a tu almacenamiento apenas lo descargues. Si necesitas el archivo después, guarda el id del mensaje y llama a la descarga de nuevo.

Recibe multimedia de WhatsApp en tu sistema

Crea la cuenta, verifica tu teléfono y recibe el crédito de bienvenida, sin depositar (uno por teléfono, sujeto a la campaña vigente). Conecta por código QR, activa el evento messages y prueba con una foto.