Dev

Como receber e baixar áudio, imagem e PDF do WhatsApp pelo webhook

Para baixar áudio, imagem ou PDF recebido no WhatsApp, o webhook avisa que a mensagem chegou e você chama POST /api/v1/{instanceId}/message/download com o campo id da mensagem. A API devolve a URL do arquivo (fileURL) e o mimetype, ou o conteúdo em base64 se você pedir. Abaixo está o fluxo inteiro: configurar o evento, identificar o tipo, baixar, guardar e, se precisar, transcrever.

Publicado em · 7 min de leitura

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

Como funciona o fluxo de mídia recebida

São três passos. Um: o evento messages chega ao seu webhook com a mensagem; quando há mídia, o evento descreve o tipo e traz o ID da mensagem. Dois: seu servidor chama /message/download com esse id. Três: você copia o arquivo para o seu armazenamento e processa.

A separação existe por motivo prático: o webhook precisa ser leve e responder 2xx rápido, e arquivo é pesado. Por isso o webhook entrega o aviso, e o download acontece no seu worker, fora do caminho da resposta HTTP. Para o desenho geral do webhook, veja o guia de webhook em produção.

Como configurar o webhook para receber mensagens

Registre a URL pública (HTTPS) da instância com POST em /webhook, enviando url, enabled e a lista events. Os eventos vêm desativados por padrão; para mídia, ative messages. Eventos de alto volume como presence e history só ligue se for tratá-los.

Teste a entrega com POST /instances/{id}/webhook/test: ele manda um evento de teste e informa o status HTTP e o tempo de resposta do seu endpoint. Se algo não chegar, GET /instances/{id}/webhook/deliveries mostra as últimas entregas e GET /webhook/errors lista as falhas.

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

O que olhar no evento para saber se é áudio, imagem ou PDF

A forma exata do payload deve ser conferida num evento real da sua instância: grave o corpo cru em log, envie do celular uma foto, um áudio e um PDF e compare. Os nomes de campo variam conforme o tipo de mensagem, e é melhor mapear a partir do que você viu do que de um exemplo genérico.

O que você precisa extrair em todos os casos: o ID da mensagem, o tipo (texto, imagem, áudio, documento), o remetente e se a mensagem foi enviada por você (fromMe). Descarte fromMe logo no início. Os exemplos abaixo usam leitura defensiva com valores alternativos; ajuste os nomes depois de ver o seu payload.

Como baixar a mídia com /message/download em Node.js

A chamada recebe id e flags opcionais. Com return_link true (padrão), a resposta traz fileURL e mimetype. Com return_base64 true, traz o conteúdo em base64 na própria resposta; use só para arquivos pequenos. generate_mp3 converte áudio para MP3 e transcribe pede a transcrição, que a documentação descreve como best-effort e pode não vir.

O servidor abaixo responde 200 antes de baixar, extrai o ID, chama o download e grava o arquivo em disco. Em produção, troque a gravação em disco por upload para o seu armazenamento de objetos e não trate a fileURL como permanente: copie o arquivo para o seu 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);

Como baixar a mídia com /message/download em Python

A mesma lógica em Python com Flask e requests. O ponto de atenção é o mesmo: responder rápido e baixar em segundo plano. O exemplo usa uma thread simples para ser curto; em produção use uma fila (Celery, RQ, Cloud Tasks ou equivalente).

Se você só precisa do texto de um áudio, peça transcribe true na chamada de download, sabendo que é best-effort, ou use o módulo de IA da instância para transcrição garantida, conforme a própria referência do 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

Como localizar uma mensagem antiga e baixar a mídia dela

Se você perdeu o evento (servidor fora do ar, por exemplo), recupere a mensagem com POST /message/find. Filtre por id para uma mensagem específica ou por chatid para o histórico de uma conversa, com limit e offset para paginar. Depois use o id retornado em /message/download.

Isso também resolve o caso de reprocessamento: se o download falhou, o id ficou guardado e você repete a chamada depois, sem depender de novo evento do 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}'
NecessidadeParâmetroObservação
Guardar o arquivo no seu storagereturn_link trueBaixe da fileURL e copie para o seu lado
Arquivo pequeno direto na respostareturn_base64 trueEvite para arquivos grandes
Tocar áudio em navegador ou convertergenerate_mp3 trueDevolve o áudio em MP3
Texto do áudiotranscribe trueBest-effort, pode não retornar
Transcrição garantidaMódulo de IA da instânciaVer a referência do endpoint

Na dúvida, comece por return_link e trate o resto como otimização. O PDF e a imagem não precisam de conversão; o áudio de voz costuma ser o único que pede MP3 para tocar em qualquer lugar.

Como desenhar a fila entre o webhook e o worker de download

Em produção, o endpoint do webhook faz três coisas e só três: valida o formato do evento, grava o id da mensagem e o tipo em uma fila (banco, Redis, fila gerenciada) e responde 2xx. Um worker separado consome a fila, chama /message/download, copia o arquivo para o seu armazenamento e marca o item como concluído.

Esse desenho resolve três problemas de uma vez. Falha de download vira retry com backoff sem perder o evento. Pico de mensagens (uma campanha que gera muitas respostas) não derruba o endpoint. E a deduplicação fica simples: o id da mensagem é a chave única da fila, e um segundo evento igual é ignorado.

EtapaOnde rodaSe falhar
Receber o eventoEndpoint do webhookA API reenvia ou você consulta GET /webhook/errors
Gravar na filaEndpoint do webhookResponder 5xx para provocar reentrega
Baixar a mídiaWorkerRetry com backoff; guardar o id para tentar depois
Copiar para o storageWorkerRepetir; a URL de origem pode expirar, então não demore
Processar (OCR, transcrição, IA)WorkerMarcar como erro e alertar

Boas práticas: idempotência, segurança e privacidade

Idempotência: o mesmo evento pode chegar duas vezes. Guarde o id da mensagem e ignore ids já processados, para não baixar e processar o mesmo arquivo em dobro. Segurança: trate todo arquivo recebido como não confiável. Valide o tipo real do arquivo (não só a extensão), limite o tamanho, nunca execute nem abra documentos no servidor sem antivírus ou isolamento.

Privacidade: áudio, foto e PDF de clientes são dados pessoais e podem ser sensíveis. Defina por quanto tempo guarda, quem acessa e como apaga, e registre a base legal conforme a LGPD. O guia de disparo e LGPD traz o raciocínio geral. Para depurar a entrega do webhook, consulte a documentação de webhooks e a referência de message/download.

Resumo rápido

  • O webhook avisa que chegou mídia; o arquivo vem de POST /message/download com o id da mensagem.
  • Ative o evento messages em POST /webhook e responda 2xx antes de baixar.
  • Use return_link para guardar o arquivo; base64 só para arquivos pequenos; transcribe é best-effort.
  • POST /message/find recupera mensagens perdidas por id ou por chatid.
  • Trate o arquivo como não confiável, guarde o id para idempotência e defina retenção por causa da LGPD.

Perguntas frequentes

Como baixar uma imagem recebida no WhatsApp pela API?

Pegue o ID da mensagem no evento do webhook e chame POST /message/download com id e return_link true. A resposta traz fileURL e mimetype; baixe o arquivo dessa URL e copie para o seu armazenamento.

O webhook já envia o arquivo junto?

O evento avisa a chegada e descreve a mensagem; o arquivo em si vem pela chamada de download. Isso mantém o webhook leve e permite baixar fora do caminho da resposta HTTP.

Como transcrever um áudio recebido?

A chamada de download aceita transcribe true, mas a referência descreve como best-effort: pode não retornar. Para transcrição garantida, use o módulo de IA da instância.

Qual o limite de tamanho do arquivo?

Para envio, a referência indica cerca de 190 MB para imagem, vídeo e áudio e cerca de 500 MB para documento quando enviados por URL, e cerca de 2 MB quando em base64. Para o download, prefira return_link em vez de base64.

A fileURL é permanente?

Não assuma isso. Copie o arquivo para o seu armazenamento assim que baixar. Se precisar do arquivo depois, guarde o id da mensagem e chame o download de novo.

Receba mídia do WhatsApp no seu sistema

Crie a conta, verifique o telefone e receba o crédito de boas-vindas, sem depositar (um por telefone, sujeito à campanha vigente). Conecte por QR code, ative o evento messages e teste com uma foto.