DevZap · Guias de integração Voltar ao painel

Erros e retry

O guia mais importante depois do de envio: tratar erro errado é o que transforma uma falha momentânea em mensagem perdida, ou em mensagem duplicada.

Os três envelopes

A API tem três formatos de erro. Não é elegante, mas é o que existe, e conhecer os três evita código que quebra quando encontra o formato inesperado.

1. O erro da v1, o caso comum

A requisição foi entendida, mas a operação não deu certo.

{
  "success": false,
  "error": {
    "code": "INSTANCE_NOT_CONNECTED",
    "message": "Instância não está conectada. Conecte via /qr primeiro."
  }
}

2. O erro global, quando o corpo nem chega ao endpoint

Falha na validação de schema, e também os 429. Repare que aqui error é um booleano, e o código está na raiz:

{
  "error": true,
  "code": "VALIDATION_ERROR",
  "message": "body must have required property 'message'"
}

Fora de produção há também um campo stack. Não conte com ele.

3. O erro de plano (403)

O envelope da v1 com campos extras dizendo o que falta contratar:

{
  "success": false,
  "error": {
    "code": "PLAN_FEATURE_NOT_ALLOWED",
    "message": "...",
    "feature": "message_queue",
    "planCode": "basic",
    "upgradeUrl": "/billing"
  }
}

O upgradeUrl aponta para a página de planos do painel. Os outros caminhos do mesmo envelope (renewUrl, quando o plano expirou, e extraInstancesUrl, quando a cota de instâncias estourou) apontam para o mesmo lugar.

Extraindo o código sem se preocupar com o formato

O mesmo endpoint pode produzir mais de um envelope, POST /messages/send produz os dois primeiros. Um extrator que cobre todos:

function codigoDoErro(body) {
  return body?.error?.code ?? body?.code ?? 'DESCONHECIDO';
}

Decida sempre pelo code, nunca pela message. A mensagem é texto para humanos e muda sem aviso; o código é contrato.

O que retentar

Código Retentar? Por quê
INSTANCE_NOT_CONNECTED ✅ Sim A sessão caiu; costuma voltar sozinha
SEND_RETRIABLE ✅ Sim Falha momentânea do motor de envio
RATE_LIMITED ✅ Sim Aguarde o retryAfter indicado
SEND_ERROR ❌ Não Falha permanente; repetir só gasta cota
SEND_UNCONFIRMED ❌ Nunca automaticamente Não sabemos o desfecho: a mensagem pode ter sido entregue. Repetir duplica na tela de quem recebe
VALIDATION_ERROR ❌ Não O corpo está errado; repetir dá o mesmo resultado
PLAN_CHECK_UNAVAILABLE ✅ Sim 503: a verificação do plano falhou por instabilidade nossa, não é bloqueio
AUTH_UNAVAILABLE ✅ Sim 503: não conseguimos conferir a chave agora (instabilidade nossa). A chave não é inválida: não troque
SUBSCRIPTION_EXPIRED, TRIAL_EXPIRED, SUBSCRIPTION_REQUIRED ❌ Não A conta precisa de plano ativo; repetir não muda nada
PLAN_FEATURE_NOT_ALLOWED ❌ Não O plano não inclui o recurso

SEND_UNCONFIRMED, o caso do meio

Este não é "falhou". É "não sei", e a diferença importa: o WhatsApp às vezes aceita a mensagem e não devolve o comprovante. Quando isso acontece, o servidor tenta achar a mensagem no histórico da conversa; se acha, o envio vira sucesso normal, com messageId. Só quando nem isso confirma é que sai o SEND_UNCONFIRMED, e aí não há messageId, porque ele é justamente o que não foi possível obter.

Reenviar aqui é a pior escolha: o destinatário provavelmente já recebeu, e ele veria a mensagem duas vezes. O certo é marcar para conferência humana.

Regra geral de retry: decida por allowlist, retentando apenas os códigos marcados com ✅ acima. Regras por status HTTP ("todo 500 é transitório") são perigosas, porque SEND_ERROR e SEND_UNCONFIRMED também respondem 500, e a segunda duplica mensagem se for retentada.

A diferença entre SEND_RETRIABLE e SEND_ERROR é justamente essa classificação, o servidor já analisou a falha e está dizendo se vale insistir:

{
  "success": false,
  "error": { "code": "SEND_RETRIABLE", "message": "Session closed" }
}
{
  "success": false,
  "error": { "code": "SEND_ERROR", "message": "Evaluation failed: t" }
}

Os demais códigos (MISSING_MEDIA, NOT_FOUND, WEBHOOK_NOT_FOUND, MISSING_INSTANCE, e os outros listados em /docs) indicam problema permanente: algo faltando ou inexistente. Não retente.

Falha depois do envio: o message.failed

Tudo acima é sobre a resposta do envio. Uma mensagem também pode falhar depois do 200, quando o WhatsApp recusa a entrega; aí o aviso chega pelo webhook message.failed, com o motivo em errorCode:

errorCode Reenviar? Por quê
FIRST_CONTACT_BLOCKED ❌ Nunca automaticamente Erro 463: o contato ainda não tem conversa com o número. Reenviar conta como mais um contato novo
ACCOUNT_RESTRICTED ❌ Nunca automaticamente Erro 463 com o número restrito para iniciar conversas. Insistir prolonga a restrição
DELIVERY_FAILED ❌ Não automaticamente Outra recusa do WhatsApp. Confira número e conteúdo antes de tentar de novo

O DevZap também não reenvia essas mensagens por conta própria, nem pela fila. Veja o que fazer em Erro 463 e acompanhe a situação do número em GET /instances/{instanceId}/limits.

Uma política de retry que funciona

const RETRIAVEIS = new Set(['INSTANCE_NOT_CONNECTED', 'SEND_RETRIABLE', 'RATE_LIMITED']);

async function enviarComRetry(payload, { tentativas = 4 } = {}) {
  for (let n = 0; n < tentativas; n++) {
    const resposta = await fetch(url, { method: 'POST', headers, body: JSON.stringify(payload) });
    if (resposta.ok) return resposta.json();

    const corpo = await resposta.json().catch(() => ({}));
    const codigo = corpo?.error?.code ?? corpo?.code;

    if (!RETRIAVEIS.has(codigo)) {
      throw new Error(`falha permanente: ${codigo}`);  // desiste na hora
    }

    // Respeita o retryAfter do 429; senão, backoff exponencial com jitter
    const esperaBase = corpo.retryAfter ? corpo.retryAfter * 1000 : 2 ** n * 1000;
    await new Promise((r) => setTimeout(r, esperaBase + Math.random() * 1000));
  }
  throw new Error('tentativas esgotadas');
}

Três detalhes que fazem diferença:

Desista cedo no erro permanente. Retentar VALIDATION_ERROR quatro vezes só atrasa a descoberta de que o payload está errado.

Respeite o retryAfter. Ignorá-lo e insistir mantém você no limite indefinidamente.

Use jitter. Sem ele, várias falhas simultâneas retentam juntas e recriam o pico.

Retry e duplicação

O envio direto não é idempotente. Se a resposta se perdeu por timeout de rede, você não sabe se a mensagem saiu, e retentar pode duplicar.

Quando duplicar é inaceitável, use a fila com idempotencyKey:

{
  "to": "5511123456789",
  "idempotencyKey": "cobranca-4711-primeiro-aviso",
  "type": "text",
  "message": "Olá! Consta um débito em aberto."
}

Aí o retry é seguro por construção: a mesma chave nunca gera duas mensagens. A chave precisa vir do seu domínio, um UUID novo por tentativa não protege nada.

Erros de autenticação

Código HTTP O que fazer
MISSING_KEY, INVALID_FORMAT 401 Corrigir o cabeçalho; falta o Bearer ou a chave
KEY_NOT_FOUND, KEY_INACTIVE, KEY_EXPIRED 401 Chave revogada ou vencida; gerar outra
KEY_WRONG_AUDIENCE 401 É a chave que um assistente de IA recebeu pelo login: ela só vale no servidor MCP (/api/v1/mcp). Para a sua integração, crie uma chave em Chaves de API
TENANT_SUSPENDED 403 Conta suspensa; falar com o suporte
SCOPE_DENIED 403 A chave não tem permissão para esta operação
AUTH_UNAVAILABLE 503 Falha nossa ao conferir a chave; repita com backoff, com a mesma chave

Fora o AUTH_UNAVAILABLE, nenhum deles é retriável: repetir com a mesma chave dá o mesmo erro. Vale alertar a operação em vez de insistir.

Erros do assistente de IA

O servidor MCP (/api/v1/mcp) devolve os mesmos códigos dentro do resultado da ferramenta, mais três que são só dele: MCP_DISABLED, MCP_DAILY_CAP_REACHED e MCP_DUPLICATE_SEND. O que fazer com cada um está em Assistentes de IA.


Guia verificado em 05/08/2026 contra a API v1. A tabela de retry é conferida automaticamente contra a classificação do código-fonte em cada build.


← Todos os guias