Enviar e receber
Mídia
Imagem, áudio, vídeo e documento passam pelo mesmo
POST /v1/messages. O binário nunca trafega no recurso da
mensagem: o que você recebe são metadados e uma URL assinada e temporária.
Suba o arquivo antes (recomendado)
POST /v1/media recebe o arquivo em multipart/form-data
e devolve um mediaId opaco. Você o usa em
media.mediaId no envio.
Existe porque arquivo e mensagem têm ciclos de vida diferentes:
o arquivo é imutável e reusável, a mensagem é um evento único. Na prática, três
coisas mudam — o binário não viaja no corpo do envio (nada de base64 inflando
~33%), o teto passa a ser o do canal (16 MB, em vez de ~7,5 MB),
e o mesmo mediaId serve N conversas e N números do
seu tenant.
curl -X POST https://api.nuntis.com.br/v1/media \
-H "x-api-key: $NUNTIS_API_KEY" \
-F "file=@contrato.pdf;type=application/pdf"
{
"mediaId": "med_9182",
"mimetype": "application/pdf",
"size": 2481234,
"filename": "contrato.pdf",
"expiresAt": "2026-09-09T18:12:03.000Z",
"expiresAtUnix": 1789012323
}
{
"numberId": "num_4711",
"conversationId": "conv_991",
"type": "document",
"media": {
"mediaId": "med_9182",
"caption": "Segue o contrato para assinatura."
}
}
mediaId vale por 24 horas
O prazo vem em expiresAt. Depois dele, referenciar o
mediaId responde 404 NOT_FOUND e o arquivo é
apagado. Referenciar não estende o prazo — ele só precisa
valer no instante do envio; a mensagem já criada segue intacta.
O mediaId vale só para o seu tenant. Continua
valendo se você rotacionar a API key, e não vale para nenhum outro cliente
do Nuntis. Expirado, de outro cliente ou inexistente respondem igual:
404.
O header Idempotency-Key não é aceito nesta
rota: cada chamada produz um mediaId novo, mesmo com o mesmo
arquivo. Não há risco em repetir — o mediaId que você não usar
expira sozinho em 24h.
O Content-Type que você declara na parte file é
conferido contra a assinatura dos bytes, e o upload é recusado com
400 quando os dois se contradizem — é por esse tipo que o canal
escolhe a rota de envio. Isso não é varredura de vírus:
assinatura diz que forma o arquivo tem, jamais se ele é seguro.
Enviar mídia
Troque type e use o bloco media no lugar de
text. A fonte do arquivo é exatamente uma entre
mediaId, url e base64.
{
"numberId": "num_4711",
"to": { "phone": "+5511999990000" },
"type": "document",
"media": {
"url": "https://cdn.seuproduto.com.br/contratos/2026-08/exemplo.pdf",
"filename": "contrato.pdf",
"caption": "Segue o contrato para assinatura."
}
}
{
"numberId": "num_4711",
"conversationId": "conv_991",
"type": "audio",
"media": {
"base64": "SUQzBAAAAAAA...",
"mimetype": "audio/ogg",
"ptt": true
}
}
| Campo | Descrição |
|---|---|
mediaId |
Id devolvido por POST /v1/media. O caminho recomendado. Exclusivo mútuo com url e base64. |
url |
URL http(s) pública do arquivo. Exclusivo mútuo com mediaId e base64. |
base64 |
O arquivo embutido. Exige mimetype. Exclusivo mútuo com mediaId e url. |
mimetype |
No formato tipo/subtipo. Obrigatório com base64. Com mediaId ele vem do upload — informar um divergente devolve 400. |
filename |
Nome apresentado ao destinatário. Até 255 caracteres. |
caption |
Legenda exibida junto da mídia. Até 1024 caracteres. |
ptt |
Envia o áudio como nota de voz. Só se aplica a type: audio, e só em canal que declare audioPtt: true. |
Vale para base64: o teto é do corpo inteiro, não
do arquivo, e como base64 infla o binário em cerca de um terço, o limite
prático fica em ~7,5 MB. Passar disso devolve
413 PAYLOAD_TOO_LARGE, que não é retentável.
Por mediaId esse teto não se
aplica: o binário não viaja no corpo do envio, e o limite passa a ser
o do canal — 16 MB. É a razão principal para preferir o
upload direto.
O que acontece com base64 antes do 202
O binário é enviado ao armazenamento do Nuntis durante a sua
requisição, antes de você receber o 202. Isso torna o
202 uma promessa forte: se ele voltou, os bytes estão guardados e
a mensagem vai sair.
A contrapartida é que a requisição pode falhar por causa do armazenamento. Se isso acontecer, você recebe:
{
"code": "MEDIA_STORAGE_UNAVAILABLE",
"message": "Nao foi possivel armazenar a midia agora. Nenhuma mensagem foi criada — tente de novo com a mesma Idempotency-Key.",
"retryable": true,
"source": "nuntis"
}
503 tem efeito zero — e é por isso que dá para repetir
Quando ele acontece, nada foi gravado: não existe mensagem
criada, e a sua Idempotency-Key não foi consumida.
Repita a mesma requisição, com a mesma chave, depois de um backoff.
Isso não é um detalhe de implementação: é a razão de o upload acontecer antes do registro de idempotência. Se fosse depois, uma falha momentânea do armazenamento queimaria a sua chave — e o retry correto, com a mesma chave, passaria a devolver erro para sempre.
source: "nuntis" diz que a falha é nossa, não do
canal. O WhatsApp pode estar perfeitamente de pé.
url? O host precisa ser público
O Nuntis busca a URL que você informar. Para isso, o host tem que ter forma
de nome público: endereço de rede interna, localhost, IP
literal e domínios que só resolvem dentro da sua rede são recusados — e
redirect é validado a cada salto (no máximo 3). Teto de
16 MB no arquivo.
Se você precisa mandar algo que só existe atrás da sua VPN, use
base64.
Confira a capacidade antes
Cada canal declara quais tipos de mídia aceita no envio e se
suporta nota de voz. Consulte GET /v1/channels e leia
midia e audioPtt. Enviar algo que o canal não
declara devolve 422 CHANNEL_CAPABILITY_UNSUPPORTED.
Campo de GET /v1/channels | Responde |
|---|---|
midia |
O que este canal aceita enviar. É o campo que decide o que o seu clipe de anexo oferece. |
midiaRecebimento |
O que este canal sabe receber e entregar a você. É
deliberadamente mais largo: sticker chega
e não é enviável por rota nenhuma. Use-o para decidir o que a sua
interface precisa saber exibir.
|
Áudio: formatos, conversão automática e forma de onda
O envelope (container) do arquivo de áudio que você manda muitas vezes não é o que o navegador de quem gravou escolheu por padrão — e isso não deveria ser problema seu. O Nuntis converte o container automaticamente quando necessário, sem alterar o áudio: os pacotes de som que chegam ao destinatário são idênticos aos que você enviou.
| Navegador (gravação nativa) | O que ele grava | O que o Nuntis faz |
|---|---|---|
| Firefox | Ogg/Opus | Passa direto — já é o formato nativo de nota de voz. |
| Chrome, Edge, Brave, Opera (Chromium) | WebM/Opus | Convertido para Ogg/Opus antes do envio — só o envelope muda, os pacotes de áudio são os mesmos. |
| Safari (macOS e todo navegador no iOS/iPadOS) | AAC em MP4 | Enviado como está — chega e reproduz normalmente, mas sem nota de voz "de verdade" (ver aviso abaixo). |
Trocar o envelope não ajuda aqui: o MediaRecorder do Safari
(e de qualquer navegador rodando sobre WebKit, inclusive no iOS) grava em
AAC, nunca em Opus — que é o codec que a nota de voz do
WhatsApp usa. O arquivo AAC chega e toca normalmente como áudio, mas hoje
não vira uma nota de voz com as mesmas barrinhas de amplitude. Se seus
usuários gravam áudio no navegador, recomendamos orientar o uso de
Chrome, Edge ou Firefox para essa funcionalidade.
Um arquivo cujo container não tem Opus dentro dele (por exemplo, um WebM gravado com Vorbis, ou um envelope corrompido) não tem como virar nota de voz do WhatsApp — nesse caso a mensagem falha, de forma nomeada e definitiva:
{
"status": "failed",
"failure": {
"code": "MEDIA_CODEC_UNSUPPORTED",
"message": "O audio nao contem um codec suportado para nota de voz.",
"retryable": false
}
}
Isso é diferente de "formato de áudio recusado em geral": Ogg/Opus, Ogg/Vorbis, MP3, WAV e AAC-em-MP4 continuam passando normalmente. O código acima só aparece quando o Nuntis abriu o envelope e não achou dentro dele o que precisa para virar nota de voz.
Forma de onda (media.waveform)
Uma nota de voz do WhatsApp mostra barrinhas de amplitude na barra de progresso — sem elas, o áudio "parece encaminhado" em vez de gravado agora. Você pode mandar essa forma de onda já calculada, ou deixar o Nuntis calcular a partir dos bytes:
{
"numberId": "num_4711",
"conversationId": "conv_991",
"type": "audio",
"media": {
"base64": "T2dnUwAC...",
"mimetype": "audio/ogg",
"ptt": true,
"waveform": [12, 34, 58, 71, 40, 5, 0, 0]
}
}
O exemplo acima mostra só as 8 primeiras fatias — o array completo tem sempre 64 posições.
| Campo | Descrição |
|---|---|
media.waveform |
Opcional, só em type: audio. Exatamente 64
inteiros entre 0 e 100 — a amplitude
relativa de cada uma das 64 fatias do áudio. Fora dessa forma, o
envio é recusado com 400.
|
Quando você tem o áudio original em mãos no momento da gravação (por exemplo, via Web Audio API no navegador), calcular a forma de onda aí é mais fiel — é por isso que, quando presente, o valor que você manda sempre prevalece sobre qualquer cálculo do Nuntis.
Se você não mandar, o Nuntis calcula a partir dos bytes do áudio — mas só
quando é nota de voz (ptt: true) em Opus/Ogg e a duração está
dentro de um teto interno. Fora dessas condições, ou se o cálculo falhar
por qualquer motivo, o áudio é enviado normalmente, só sem as barrinhas —
isso nunca impede a entrega.
Receber mídia
Numa mensagem recebida, o bloco media traz os metadados e — se o
arquivo já estiver disponível — uma URL assinada:
{
"id": "msg_18291",
"conversationId": "conv_991",
"numberId": "num_4711",
"direction": "inbound",
"type": "image",
"status": "received",
"media": {
"mimetype": "image/jpeg",
"filename": "foto.jpg",
"size": 184320,
"caption": "É esse o comprovante?",
"url": "https://…<assinada e temporária>",
"urlExpiresAt": "2026-08-13T19:12:03.000Z",
"pending": false
}
}
url
Ela é assinada e temporária, gerada sob demanda a cada
leitura. Um link guardado no seu banco vira um 403 silencioso
no dia seguinte — a imagem simplesmente para de carregar na sua interface,
sem erro nenhum no seu log.
Baixe o arquivo para o seu próprio armazenamento assim que
receber, ou peça a URL de novo com
GET /v1/messages/{id} na hora de exibir. O campo
urlExpiresAt diz quando ela deixa de valer.
Quando o arquivo ainda não chegou (ou não vai chegar)
Depois que a mensagem existe, o binário dela está em um de três estados — e a diferença entre os dois últimos decide se vale a pena pedir de novo.
| Estado | Como reconhecer | O que fazer |
|---|---|---|
| Disponível | media.url preenchida, sem pending. |
Baixe e guarde no seu armazenamento. |
| Pendente | media.pending: true, sem media.unavailable. |
Pode chegar. Agende um redownload com backoff. |
| Indisponível | media.unavailable presente. |
Pare. O arquivo não vem — nem agora, nem depois. |
pending continua true no estado indisponível
E isso é de propósito: o campo promete “o binário não está guardado”, e a
promessa continua verdadeira. Quem já lê só pending não quebra
com esta mudança — apenas passa a receber um erro definitivo
(retryable: false) quando pedir o arquivo de novo, em vez de um
“tente mais tarde” eterno.
O bloco media.unavailable
"media": {
"mimetype": "image/jpeg",
"size": 184320,
"pending": true,
"unavailable": {
"reason": "expired",
"at": "2026-09-02T18:41:07.000Z"
}
}
reason | O que aconteceu | De quem é a decisão |
|---|---|---|
expired |
O WhatsApp descartou o arquivo do armazenamento dele. Nenhuma rota o recupera. | Do canal. |
too_large |
O arquivo passa do teto de armazenamento de mídia do Nuntis e não será guardado. | Nossa. O canal serviria o arquivo. |
retention |
A mídia passou de 90 dias a partir do timestamp da mensagem, e a política de retenção removeu o arquivo. Texto, legenda e metadados continuam. Detalhes. |
Nossa. É política do Nuntis, não do canal. |
Retenção de mídia: 90 dias
O arquivo de mídia de uma mensagem, recebida ou enviada, fica disponível por
90 dias a partir do timestamp da mensagem. Depois
disso o Nuntis remove o arquivo do armazenamento. A mensagem
continua: texto, legenda, tipo, nome do arquivo, tamanho e demais
metadados. O texto das mensagens não tem retenção por idade.
"media": {
"mimetype": "application/pdf",
"filename": "contrato.pdf",
"size": 184320,
"pending": true,
"unavailable": {
"reason": "retention",
"at": "2026-11-26T05:10:00.000Z"
}
}
- A
urldeixa de vir.até quando a política foi aplicada, não quando a mensagem chegou. - O
redownloadresponde410 MEDIA_RETENTION_EXPIRED, comretryable: falseesource: nuntis. - A marcação acontece uma vez por dia. Uma mídia pode seguir disponível por até um dia depois dos 90.
- Nenhum webhook é disparado quando a mídia expira: o prazo é previsível pelo dado que você já tem (
timestamp+ 90 dias). - A varredura por
updatedAfternão mostra a mudança, porque o filtro é pelotimestampda mensagem.
Depois dos 90 dias o arquivo não pode ser recuperado por nenhuma rota.
Se o seu produto precisa guardá-lo, baixe pela url do
GET /v1/messages/{id} enquanto ela existe.
reason é um enum aditivo
Valores novos podem aparecer sem aviso de versão. Trate um
reason que você não reconhece como o que ele é: o
arquivo não vem. Nunca escreva um else que volte a
pedir.
A mídia recebida antes do armazenamento entrar no ar ficou pending
para sempre. Em vez de fazer você descobrir isso uma mensagem por vez, o
Nuntis marcou esse acervo como expired de uma vez. O resto — mídia
recente que ainda estiver pending sem o bloco — se resolve no
redownload, com o veredito real do canal.
Quando o arquivo ainda não chegou
Falha de mídia nunca derruba a mensagem. Se os metadados
chegaram mas o binário não, a mensagem é persistida normalmente — texto,
legenda e remetente completos — com media.pending: true e sem
url.
"media": {
"mimetype": "application/pdf",
"filename": "comprovante.pdf",
"pending": true
}
Isso é deliberado: descartar a mensagem inteira porque um anexo falhou faria você perder o que o cliente escreveu. O anexo é recuperável; a mensagem perdida não.
Rebaixar o arquivo
POST /v1/messages/{id}/media/redownload
curl -X POST \
-H "x-api-key: $NUNTIS_API_KEY" \
https://api.nuntis.com.br/v1/messages/msg_18291/media/redownload
A resposta é o recurso completo da mensagem, com media.url
preenchida quando a recuperação deu certo.
| Situação | Resposta |
|---|---|
| Mídia recuperada | 200 com media.url e pending: false. |
| Mídia já estava disponível | 200 com o recurso como está. É idempotente — pedir de novo não é erro. |
| Mensagem sem mídia | 400 VALIDATION_FAILED. |
| O canal já não serve o arquivo | 410 MEDIA_EXPIRED, com retryable: false. Pare de tentar. A resposta vem sem consultar o canal quando o veredito já é conhecido. |
| O arquivo passa do teto de armazenamento do Nuntis | 422 MEDIA_TOO_LARGE, com retryable: false. Também é final — a decisão é nossa, não do canal. |
| A mídia passou de 90 dias (retenção) | 410 MEDIA_RETENTION_EXPIRED, com retryable: false e source: nuntis. Final — o canal não é consultado. |
| Não foi possível perguntar ao canal agora | 503 CHANNEL_UNREACHABLE, com retryable: true. Este é o único desfecho que vale retentar, com backoff. |
A rota devolve o recurso da mensagem, com uma URL nova — não os bytes do arquivo. Baixe pela URL devolvida.
A mídia que você enviou também volta assinada
Depois que o Nuntis guarda o binário que você mandou por base64,
GET /v1/messages/{id} de uma mensagem outbound passa
a devolver media.url assinada, no mesmo formato do inbound:
{
"id": "msg_258037",
"direction": "outbound",
"type": "image",
"status": "delivered",
"media": {
"mimetype": "image/png",
"filename": "contrato.png",
"size": 512838,
"url": "https://…<assinada e temporária>",
"urlExpiresAt": "2026-09-02T14:12:03.000Z"
}
}
O arquivo fica no armazenamento do Nuntis e é seu para reler pela rota, do
mesmo jeito que a mídia recebida. Valem as mesmas duas regras: a
url é temporária (peça de novo na hora de
exibir, nunca persista o link), e a retenção é de 90 dias —
depois disso o arquivo sai do armazenamento e o recurso passa a trazer
media.unavailable.reason: "retention", embora a mensagem
continue no histórico (detalhes).
Fluxo recomendado
- Chega
message.receivedno seu webhook. - Se
media.pendingforfalse, baixe pelaurle guarde no seu armazenamento. - Se
media.unavailableestiver presente, pare por aqui: registre o motivo e não agende nada. O arquivo não vem. - Se for
truee não houverunavailable, agende umredownloadcom backoff em vez de tentar em laço. - Tomou
410ou422noredownload? Marque a mensagem como sem anexo no seu lado e tire-a da fila de reconciliação — retentar não muda o resultado. - Na sua interface, sirva sempre do seu armazenamento — nunca da URL do Nuntis. A mídia que você não copiou sai do Nuntis 90 dias depois do
timestampda mensagem.