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.
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_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.
RATE_LIMITED 429 sim Limite de requisições da sua API key excedido. Respeite o header Retry-After.
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.
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 entregaDISPATCH_AMBIGUOUS significa que a mensagem pode ter sido entregue, e reenviar pode duplicá-la.