Nuntis documentação Referência da API

Operação

Erros

Todo erro do contrato /v1 tem exatamente a mesma forma, com quatro campos. Você nunca precisa interpretar prosa nem adivinhar a partir do status HTTP.

json
{
  "code": "CHANNEL_UNREACHABLE",
  "message": "O canal não respondeu a tempo. Nenhuma ação foi aplicada.",
  "retryable": true,
  "source": "canal"
}
CampoPara que serve
code Identificador estável. É por ele — e só por ele — que o seu código decide o que fazer.
message Prosa para humano. Livre para mudar a qualquer momento; nunca é contratual.
retryable Se vale tentar de novo. Responde a pergunta sem heurística sobre status HTTP.
source nuntis (problema nosso) ou canal (problema do provedor do canal).
Case por code, nunca por message

Comparar substring do texto quebra na primeira vez que alguém melhora a redação — e essa quebra acontece silenciosamente, em produção, no seu ramo de tratamento de erro. O code existe exatamente para você não precisar disso.

retryable é por ocorrência, não por código

A tabela abaixo mostra o comportamento padrão de cada código, mas uma resposta específica pode divergir — o mesmo código pode nascer de causa transitória ou permanente. Leia sempre o retryable da resposta, não a tabela.

code HTTP retryable Significa
VALIDATION_FAILED 400 não Payload rejeitado: campo ausente, tipo errado, combinação proibida, campo desconhecido. Corrija e reenvie.
UNAUTHORIZED 401 não API key ausente, inválida, expirada, sem permissão para a rota, ou enviada por um meio não aceito.
NOT_FOUND 404 não Recurso inexistente ou de outro produto. Indistinguíveis de propósito.
IDEMPOTENCY_IN_PROGRESS 409 sim A mesma Idempotency-Key está sendo processada agora por outra requisição. A chave não queimou — tente de novo com backoff.
IDEMPOTENCY_KEY_BURNED 409 não A chave ficou em estado ambíguo e o sistema falha fechado. Use uma chave nova; nunca vira sucesso silencioso.
RESOURCE_ALREADY_EXISTS 409 não Já existe um recurso ativo seu, do mesmo tipo, com o externalRef informado. O recurso existente não é revelado: só você sabe se foi retry ou colisão. Desativar libera o valor.
RESOURCE_NOT_EMPTY 409 não O recurso ainda tem filhos ativos e não pode ser desativado. Não há cascata por desenho: desative de baixo para cima.
PHONE_NUMBER_IN_USE 409 não O telefone declarado já pertence a um número ativo desta plataforma — a checagem é global, e a resposta não diz de quem é. Desativar o número que o ocupa libera o valor.
IDEMPOTENCY_KEY_REUSED 422 não A mesma chave foi usada com payload diferente dentro da janela. Reenvie o payload original ou use chave nova.
CHANNEL_CAPABILITY_UNSUPPORTED 422 não O canal do número remetente não declara a capacidade exigida (tipo de mídia, resposta citada, nota de voz…). Consulte GET /v1/channels.
OUTSIDE_MESSAGING_WINDOW 422 não Houve conversa, mas a janela expirou. Em canais que suportam, o caminho é um template aprovado.
CONVERSATION_NOT_GROUP 422 não A conversa é 1:1, e a rota pediu a lista de participantes, que só existe em conversa de grupo.
CONVERSATION_NOT_INITIATED 422 não Nunca houve conversa. Nenhum template resolve — o único caminho é a pessoa enviar a primeira mensagem. O consentimento não expira.
BROADCAST_DESTINATION_BLOCKED 422 não Destino de broadcast/status recusado por política.
NUMBER_DEACTIVATED 422 não O número foi desativado (DELETE /v1/numbers/{id}). Ele continua visível com active: false e continua respondendo no status, mas não pareia, não sinaliza presença e não envia. Reativar não é self-service.
GROUP_MEMBERSHIP_ENDED 422 não O número não é mais participante daquele grupo, e a lista de participantes não pode ser consultada por ele. A conversa e o histórico continuam disponíveis.
NUMBER_NOT_CONNECTED 422 não O número está ativo, mas não tem sessão conectada no canal — e consultas que dependem da sessão (como a foto de perfil de um contato) não podem ser respondidas. Não confunda com CHANNEL_UNREACHABLE: aqui retry não resolve, porque nenhuma quantidade de tentativas pareia um telefone. Pareie o número e confira GET /v1/numbers/{id}/status.
QUOTA_EXCEEDED 422 não Um teto de provisionamento configurado para o seu produto foi atingido. Retry não resolve — desative o que não usa (desativado devolve quota) ou fale com o suporte.
PAYLOAD_TOO_LARGE 413 não O corpo da requisição passou de 10 MB. Como media.base64 infla o binário em ~33%, o limite prático do arquivo é ~7,5 MB. Retry não resolve: reduza o arquivo ou use media.url.
MEDIA_TOO_LARGE 422 não O arquivo recebido passa do teto de armazenamento de mídia do Nuntis e não será guardado. A decisão é nossa (source: nuntis), não do canal — e é final. Não confunda com PAYLOAD_TOO_LARGE, que fala do corpo da requisição que você envia. O recurso traz media.unavailable.reason: "too_large" com o size original. Detalhes.
MEDIA_EXPIRED 410 não O canal já não serve o binário desta mídia — o arquivo expirou do lado do WhatsApp. Estado final: nenhuma quantidade de tentativas o traz de volta, e nenhuma outra rota o recupera. O restante da mensagem (texto, legenda, metadados) continua íntegro, e o recurso traz media.unavailable.reason: "expired". Detalhes.
MEDIA_RETENTION_EXPIRED 410 não A política de retenção do Nuntis removeu o binário desta mídia: o arquivo de mensagem fica disponível por 90 dias a partir do timestamp da mensagem. A decisão é nossa (source: nuntis), não do canal. Mesmo status do MEDIA_EXPIRED — pare do mesmo jeito —, culpa oposta. O restante da mensagem continua íntegro, e o recurso traz media.unavailable.reason: "retention". Detalhes.
RATE_LIMITED 429 sim Limite de requisições da sua API key excedido — ou, na lista de participantes, o orçamento de consultas ao canal daquele número (a mensagem do erro diz qual). Respeite o header Retry-After — e não o X-RateLimit-Remaining-*, que é espelho e não garantia. O orçamento (minuto e hora), o formato de Reset e a janela de calendário estão em Orçamento de requisições.
INTERNAL 500 sim Falha inesperada do lado do Nuntis. Nenhum detalhe interno é exposto.
CHANNEL_UNREACHABLE 503 sim O canal está inalcançável ou não respondeu a tempo. Nenhuma ação foi aplicada. Tente de novo com backoff. No redownload de mídia ele significa exatamente uma coisa — não foi possível perguntar agora; “o arquivo não existe mais” é MEDIA_EXPIRED.
MEDIA_STORAGE_UNAVAILABLE 503 sim O armazenamento de mídia do Nuntis não respondeu ao subir o binário de media.base64. Nenhuma ação foi aplicada: a mensagem não foi criada e a sua Idempotency-Key continua válida — repita a mesma requisição com a mesma chave. Não confunda com CHANNEL_UNREACHABLE: aqui source é nuntis, a falha é nossa, e o canal pode estar perfeitamente de pé. Detalhes.
PAIRING_QR_TIMEOUT 504 sim O canal não produziu um código de pareamento dentro do tempo. Tente de novo.
Códigos novos entram de forma aditiva

Trate um code que você não conhece como falha genérica e decida o retry pelo retryable da resposta. Um switch que lança exceção no default vai quebrar no dia em que um código novo aparecer.

OUTSIDE_MESSAGING_WINDOW × CONVERSATION_NOT_INITIATED

Parecem o mesmo erro e não são — a diferença muda o que você deve fazer:

OUTSIDE_MESSAGING_WINDOWCONVERSATION_NOT_INITIATED
Houve conversa?Sim, mas a janela expirou.Não, nunca houve.
Template resolve?Sim, em canais que suportam.Não. Nenhum template reabre este caso.
O que fazerEnviar por template, ou esperar o contato responder.Levar a pessoa a mandar a primeira mensagem por outro caminho.

Estratégia de retry

javascript
const MAX_TENTATIVAS = 5;

async function comRetry(operacao, { chaveDeEnvio } = {}) {
  let atraso = 500; // ms

  for (let tentativa = 1; tentativa <= MAX_TENTATIVAS; tentativa++) {
    try {
      // A MESMA Idempotency-Key em todas as tentativas — é isso que
      // impede a mensagem de sair duas vezes.
      return await operacao(chaveDeEnvio);
    } catch (erro) {
      // Decida SÓ por `retryable`. Nunca por status HTTP, nunca por texto.
      if (!erro.retryable || tentativa === MAX_TENTATIVAS) throw erro;

      // Jitter evita que mil clientes voltem no mesmo instante.
      const espera = atraso + Math.random() * atraso;
      await new Promise((r) => setTimeout(r, espera));
      atraso *= 2;
    }
  }
}
  • Backoff exponencial com jitter. Retry imediato em laço transforma uma indisponibilidade curta numa longa.
  • Sempre a mesma Idempotency-Key em todas as tentativas do mesmo envio. Sem isso, o retry duplica a mensagem no aparelho do destinatário.
  • Respeite o Retry-After quando vier 429.
  • Não retente retryable: false. O resultado será exatamente o mesmo, e você só gasta o seu limite.

Falha depois do aceite

Os códigos acima são erros da requisição. Uma mensagem já aceita (202) pode falhar depois, no caminho até o canal — nesse caso você recebe message.status com status: "failed" e um bloco failure próprio:

json
"failure": {
  "code": "CHANNEL_DISCONNECTED",
  "retryable": true,
  "at": "2026-08-13T18:12:04.000Z"
}

Vale a mesma regra: decida por retryable, trate código desconhecido como falha genérica. A exceção que merece cuidado está descrita em Estados de entrega — DISPATCH_AMBIGUOUS significa que a mensagem pode ter sido entregue, e reenviar pode duplicá-la.

Se há rede de reconciliação depois de um DISPATCH_AMBIGUOUS, quem diz é o canal

O Nuntis reconcilia esse caso por dois mecanismos declarados em GET /v1/channels: idDeMensagemReservavel (o Nuntis escolhe o id da mensagem antes de mandar, e depois reconhece a mensagem por ele) e echoDeSaida (o canal devolve um eco do que saiu). Onde os dois são true, uma mensagem que de fato saiu reaparece sozinha na conversa e o estado se resolve.

Onde ambos são false, não há rede: o DISPATCH_AMBIGUOUS é terminal, e retryable: false é a palavra final. Não existe consulta que desempate — a incerteza é do canal, não nossa. Reenviar por conta própria ali pode duplicar a mensagem no aparelho de quem recebe.

Se o seu produto não pode conviver com esse desfecho, o lugar de tratá-lo é o seu: registre o caso e deixe a decisão de reenviar com uma pessoa, em vez de automatizá-la.