Como enviar imagem, áudio e documento pela API do WhatsApp
Mandar um arquivo são duas chamadas, não uma: primeiro o arquivo sobe e vira um id, depois o id vai no corpo da mensagem. Este guia cobre os dois passos, a alternativa por URL, e as duas rotas que quase ninguém lembra que existem — baixar e remover a mídia.
O que você precisa
- Uma chave de API (
hub_pk_...) com o escopomessages:send— o mesmo escopo cobre o upload e o envio. Crie em /painel/chaves. - O id do canal. Liste os canais da organização em
GET /api/v1/channels. - Uma janela de atendimento aberta com o contato. Mídia avulsa é mensagem livre: fora das 24 horas, só sai dentro de um template.
Passo 1: subir o arquivo
O corpo desta rota é multipart/form-data, não JSON. O arquivo vai no campo file. Não existe campo JSON com o arquivo em base64 — e nem adiantaria: o payload de uma mensagem enviada é limitado a 256 KiB, então um arquivo embutido no JSON estouraria o limite antes de chegar à Meta.
curl -X POST https://crprohub.com/api/v1/channels/CHANNEL_UUID/media \ -H "Authorization: Bearer hub_pk_EXEMPLO_NAO_REAL" \ -F "file=@foto.jpg"
{
"data": {
"media_id": "1234567890123456",
"mime_type": "image/jpeg",
"size": 82190
}
}O media_id é um número de 5 a 30 dígitos, atribuído pela Meta. Ele expira em poucos dias do lado da Meta. Guardar esse id no seu banco como referência permanente do arquivo é o erro clássico aqui: seis meses depois ele não resolve mais nada. Guarde o arquivo do seu lado e suba de novo quando precisar reenviar.
Idempotency-Key — o header é exigido só no envio da mensagem. Repetir o upload sobe o arquivo outra vez e devolve outro media_id; nenhuma mensagem é enviada por isso, mas vale evitar o desperdício em retentativa automática.Passo 2: enviar a mensagem
O envio é o mesmo endpoint de qualquer outra mensagem. Muda o type e o objeto que acompanha ele — image, audio, video, document ou sticker — com o id devolvido no passo 1.
curl -X POST https://crprohub.com/api/v1/channels/CHANNEL_UUID/messages \
-H "Authorization: Bearer hub_pk_EXEMPLO_NAO_REAL" \
-H "Idempotency-Key: 7c1f0b2e-9a44-4f61-b0d3-8e5c2a1d4f90" \
-H "Content-Type: application/json" \
-d '{"to":"5521999999999","type":"image","image":{"id":"1234567890123456"}}'{
"data": {
"message_id": "wamid.EXEMPLO123",
"status": "accepted"
}
}A resposta é 202: aceita e enfileirada, não entregue. Vale tudo que está em enviar mensagem — a Idempotency-Key é obrigatória, e o estado final chega pelo evento message.status no webhook. O nome do objeto tem que casar com o type: mandar type: "image" com um objeto document responde 400 INVALID_MESSAGE.
ID ou URL: qual usar
O mesmo objeto aceita uma URL https pública no lugar do id. Aí não tem passo 1 — é uma chamada só.
curl -X POST https://crprohub.com/api/v1/channels/CHANNEL_UUID/messages \
-H "Authorization: Bearer hub_pk_EXEMPLO_NAO_REAL" \
-H "Idempotency-Key: 1d8e5a37-6b02-4c99-9f14-3a7b8c0d2e56" \
-H "Content-Type: application/json" \
-d '{"to":"5521999999999","type":"document","document":{"link":"https://exemplo.com/contrato.pdf"}}'A diferença não é de estilo. Com id, o arquivo já está nos servidores da Meta antes de a mensagem existir: o envio não depende do seu host estar de pé, nem de a URL continuar pública, nem da latência de download no momento do disparo. Com link, a Meta busca o arquivo na hora — e uma URL atrás de autenticação, com redirecionamento, ou que expira, faz a mensagem falhar depois do 202, quando o seu código já respondeu que deu certo.
A exceção é arquivo grande. O upload por POST .../media passa pelo host do CRPRO Hub, que pode ter um limite de tamanho de requisição menor do que o aceito diretamente pela Meta. Para um PDF de dezenas de megabytes, a URL é o caminho confiável.
Tamanhos aceitos
- Imagem —
5 MiB - Sticker (webp) —
500 KiB - Áudio e vídeo —
16 MiB - Documento (PDF e outros formatos de escritório) —
100 MiB
Esses limites são validados pelo próprio Hub, contra uma lista fechada de tipos aceitos: um arquivo grande demais ou de um tipo fora da lista responde 400 INVALID_MEDIA antes de qualquer chamada à Meta. A tabela completa, com os limites de taxa, está em limites.
Baixar e remover
Quando um cliente manda uma foto, o webhook de mensagem recebida traz o media_id — não o arquivo. Para ter os bytes, use GET /api/v1/channels/{id}/media/{mediaId}. Repare que o escopo aqui é channels:read, não messages:send: uma chave que só lê consegue baixar mídia recebida sem poder enviar nada.
curl https://crprohub.com/api/v1/channels/CHANNEL_UUID/media/1234567890123456 \ -H "Authorization: Bearer hub_pk_EXEMPLO_NAO_REAL" \ -o arquivo-baixado
Esta é a única rota da API que não devolve JSON. Não há envelope { "data": ... } — vem o conteúdo binário do arquivo, com o Content-Type real da mídia. Passar essa resposta por um .json() genérico do seu cliente HTTP quebra; trate como stream.
O DELETE na mesma rota apaga o arquivo dos servidores da Meta.
curl -X DELETE https://crprohub.com/api/v1/channels/CHANNEL_UUID/media/1234567890123456 \ -H "Authorization: Bearer hub_pk_EXEMPLO_NAO_REAL"
media_id deixam de conseguir baixá-lo. Se a intenção é liberar espaço, não faça nada — o id expira sozinho em poucos dias. O DELETE é para quando o conteúdo não pode continuar existindo lá.Quando falha
401— a chave está errada, revogada, ou você mandou o valor sem o prefixoBearer.403— falta escopo.messages:sendpara subir, enviar e remover;channels:readpara baixar.404— o canal ou a mídia não existe, ou pertence a outra organização. A API responde 404 nos dois casos de propósito: 403 revelaria que o recurso existe.409—Idempotency-Keyreaproveitada com corpo diferente. Acontece bastante aqui: o mesmo arquivo subiu duas vezes, gerou doismedia_id, e o segundo envio reusou a chave do primeiro.400 INVALID_MESSAGE— o corpo é válido como JSON mas inválido como mensagem:typesem o objeto correspondente,media_idfora do formato numérico de 5 a 30 dígitos, número malformado.429— limite de taxa. O envio é 60 por minuto por canal; a chave inteira tem 120 requisições por minuto, e cada upload consome uma. Veja limites.
Toda resposta traz um header x-request-id. Guarde-o no seu log: é o que liga a sua chamada ao registro em /painel/logs. A tabela completa de códigos está em erros.
hub_ch_...) autentica as três rotas de mídia, como alternativa à chave de API. Ele vale só para o canal que o emitiu e nunca autoriza subir, baixar ou remover mídia de outro canal da organização. O contrato completo dos endpoints está em referência de mídia.