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.
{
"code": "CHANNEL_UNREACHABLE",
"message": "O canal não respondeu a tempo. Nenhuma ação foi aplicada.",
"retryable": true,
"source": "canal"
}
| Campo | Para 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). |
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.
Catálogo de códigos
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. |
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_WINDOW | CONVERSATION_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 fazer | Enviar por template, ou esperar o contato responder. | Levar a pessoa a mandar a primeira mensagem por outro caminho. |
Estratégia de retry
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-Keyem todas as tentativas do mesmo envio. Sem isso, o retry duplica a mensagem no aparelho do destinatário. - Respeite o
Retry-Afterquando vier429. - 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:
"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.