Nuntis documentação Referência da API

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.

bash — sobe o arquivo
curl -X POST https://api.nuntis.com.br/v1/media \
  -H "x-api-key: $NUNTIS_API_KEY" \
  -F "file=@contrato.pdf;type=application/pdf"
json — 201 Created
{
  "mediaId": "med_9182",
  "mimetype": "application/pdf",
  "size": 2481234,
  "filename": "contrato.pdf",
  "expiresAt": "2026-09-09T18:12:03.000Z",
  "expiresAtUnix": 1789012323
}
json — e depois, no envio
{
  "numberId": "num_4711",
  "conversationId": "conv_991",
  "type": "document",
  "media": {
    "mediaId": "med_9182",
    "caption": "Segue o contrato para assinatura."
  }
}
O 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.

Este upload não é idempotente

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.

json — documento por URL
{
  "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."
  }
}
json — áudio como nota de voz, em base64
{
  "numberId": "num_4711",
  "conversationId": "conv_991",
  "type": "audio",
  "media": {
    "base64": "SUQzBAAAAAAA...",
    "mimetype": "audio/ogg",
    "ptt": true
  }
}
CampoDescriçã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.
Teto de 10 MB no corpo da requisição

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:

json — 503
{
  "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"
}
O 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é.

Mandar por 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/channelsResponde
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 gravaO 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).
Safari grava em outro codec, não só outro container

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:

json — falha de codec incompatível
{
  "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:

json — áudio com forma de onda declarada
{
  "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.

CampoDescriçã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.
Se você não mandar, o Nuntis tenta calcular — mas o seu valor sempre vence

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:

json — trecho de message.received
{
  "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
  }
}
Nunca persista a 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.

EstadoComo reconhecerO 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

json — mídia que o canal já não serve
"media": {
  "mimetype": "image/jpeg",
  "size": 184320,
  "pending": true,
  "unavailable": {
    "reason": "expired",
    "at": "2026-09-02T18:41:07.000Z"
  }
}
reasonO que aconteceuDe 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.

json — mídia removida pela retenção
"media": {
  "mimetype": "application/pdf",
  "filename": "contrato.pdf",
  "size": 184320,
  "pending": true,
  "unavailable": {
    "reason": "retention",
    "at": "2026-11-26T05:10:00.000Z"
  }
}
  • A url deixa de vir. at é quando a política foi aplicada, não quando a mensagem chegou.
  • O redownload responde 410 MEDIA_RETENTION_EXPIRED, com retryable: false e source: 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 updatedAfter não mostra a mudança, porque o filtro é pelo timestamp da mensagem.
Precisa do arquivo por mais tempo? Copie dentro da janela.

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.

O acervo anterior a 28/08/2026 foi marcado por idade

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.

json
"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

bash
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çãoResposta
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.
Não é um proxy de download

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:

json — mensagem que você enviou
{
  "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"
  }
}
Você não precisa guardar cópia do que enviou

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

  1. Chega message.received no seu webhook.
  2. Se media.pending for false, baixe pela url e guarde no seu armazenamento.
  3. Se media.unavailable estiver presente, pare por aqui: registre o motivo e não agende nada. O arquivo não vem.
  4. Se for true e não houver unavailable, agende um redownload com backoff em vez de tentar em laço.
  5. Tomou 410 ou 422 no redownload? Marque a mensagem como sem anexo no seu lado e tire-a da fila de reconciliação — retentar não muda o resultado.
  6. 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 timestamp da mensagem.