Seções

Erros

A taxonomia fechada de códigos de erro do broker e como tratá-los.

Toda falha do broker chega ao SDK como um BrokerApiError, nunca um erro cru de fetch ou payload de provider. O code pertence a uma união fechada — você pode tratá-lo exaustivamente com switch.

A forma do erro

ts
import { BrokerApiError } from '@wabroker/sdk'

try {
  await wa.messages.send('5511999999999', input) // pelo número do canal
} catch (err) {
  if (err instanceof BrokerApiError) {
    err.code        // BrokerApiErrorCode (união fechada)
    err.httpStatus  // status HTTP; 0 quando não houve resposta (rede/timeout)
    err.retryable   // seguro repetir automaticamente?
    err.retryAfter  // segundos a esperar (só em rate_limited/queue_saturated)
    err.traceId     // correlacionador — sempre presente numa resposta do broker
  }
  throw err
}

A WabrokerApiException (.NET) expõe Code, HttpStatus, Retryable e RetryAfter — mas não um TraceId: o correlacionador só existe no traceId do BrokerApiError (TS).

O envelope de erro

Todo erro HTTP do broker (/v1/* e /admin/v1/*) responde com o mesmo envelope JSON — aqui um rate_limited (429), que ilustra retryAfter:

json
{
  "code": "rate_limited",
  "message": "Limite de envio do número atingido",
  "retryable": true,
  "retryAfter": 30,
  "traceId": "tr_01hz..."
}
  • code / message / retryable sempre presentes.
  • retryAfter (segundos) só em rate_limited e queue_saturated — os dois únicos códigos de back-pressure.
  • traceId sempre presente — a borda do broker carimba toda requisição com um trace_id (o mesmo x-trace-id do chamador, quando enviado) e o middleware de erro preenche traceId em toda resposta que ainda não o carregue, inclusive um 500 desconhecido. Registre-o nos seus logs para agilizar o suporte.
  • errors[] só aparece em validation_error: uma lista de { field, rule, message } — field é o dot-path do campo no corpo (ex.: content.language), rule é a regra do validador que falhou (ex.: invalid_type, too_small), message é a descrição curada em pt-BR (nunca o texto cru do validador, nunca payload de provider).

traceId no cliente vs no transporte

Um timeout ou network_error (ver Códigos do lado cliente, abaixo) acontece antes de qualquer resposta do broker — nesses dois, traceId não existe porque nenhum envelope chegou a ser lido.

validation_error vs unsupported_capability

Os dois respondem 422, mas significam coisas diferentes:

  • validation_error — o corpo está malformado: campo ausente, tipo errado, enum inválido. Detectado pela validação do schema, antes de qualquer regra de negócio. errors[] aponta exatamente quais campos corrigir.
  • unsupported_capability — o corpo está bem formado, mas o canal não suporta o recurso pedido (ex.: enviar mídia de vídeo por um provider não-oficial sem esse suporte). Não há campo a corrigir — o chamador troca de estratégia (outro tipo de conteúdo, outro canal). Nunca carrega errors[].

Exemplo real — enviar um template sem o campo obrigatório `language`:

json
{
  "code": "validation_error",
  "message": "Campo inválido: content.language",
  "retryable": false,
  "traceId": "tr_...",
  "errors": [
    { "field": "content.language", "rule": "invalid_type", "message": "Campo obrigatório" }
  ]
}

Tratando `errors[]` nos dois SDKs:

ts
if (err instanceof BrokerApiError && err.code === 'validation_error') {
  for (const e of err.errors ?? []) {
    console.error(`${e.field}: ${e.message} (${e.rule})`)
  }
}

Tratar por código

ts
if (err instanceof BrokerApiError) {
  switch (err.code) {
    case 'rate_limited':
    case 'queue_saturated':
      await sleep((err.retryAfter ?? 1) * 1000)
      // ... reenfileire e tente de novo
      break
    case 'outside_24h_window':
      // troque para um template aprovado
      break
    case 'channel_disconnected':
      // reconecte o canal; a mensagem pode ser retentada
      break
    case 'unauthorized':
      // API key inválida — não adianta repetir com a mesma key
      break
    default:
      if (err.retryable) scheduleRetry()
      else reportPermanentFailure(err)
  }
}

Códigos do broker

retryable indica se repetir a mesma requisição pode ter sucesso. rate_limited e queue_saturated sempre acompanham retryAfter.

Na Wabroker.Sdk (.NET), os mesmos códigos aparecem como o enum WabrokerErrorCode (PascalCase — ex.: recipient_suppressed vira WabrokerErrorCode.RecipientSuppressed).

codeHTTPretryablesignificado
outside_24h_window409nãoJanela de 24h expirada — só template
invalid_recipient422nãoNúmero não encontrado no WhatsApp
template_not_approved409nãoTemplate não aprovado para envio
template_call_invalid422nãoChamada ao template malformada (parâmetros)
channel_disconnected503simNúmero desconectado
channel_quarantined409nãoNúmero em quarentena de onboarding (janela após a 1ª conexão)
channel_limited409nãoNúmero limitado por rajada de falhas — requer liberação manual
channel_restricted409nãoNúmero restrito pelo WhatsApp p/ contatos novos — auto-expira no prazo
rate_limited429simLimite de envio do número (com retryAfter)
queue_saturated429simFila do tenant acima do limite (com retryAfter)
recipient_suppressed409nãoContato optou por não receber (código legado)
recipient_opted_out422nãoDestinatário optou por não receber — opt-out (Compliance v1)
message_expired410nãoMensagem expirou antes do envio
media_url_expired422nãoURL de mídia expira antes do prazo
unsupported_capability422nãoRecurso não suportado por este número
provider_error502simFalha na comunicação com o provider
provider_rejected422nãoProvider rejeitou a requisição (4xx definitivo)
channel_not_found404nãoNúmero inexistente ou de outro tenant
channel_ambiguous409nãoO número casa mais de um canal vivo do tenant — passe o channelId (ch_…)
channel_already_exists409nãoJá há um número para este telefone neste app da plataforma oficial
number_in_use409nãoNúmero já ativo em outra sessão
reconnect_number_mismatch409nãoQR escaneado revelou um telefone diferente do número
attempt_not_cancellable409nãoTentativa de conexão já promovida a canal — cancele o canal por DELETE /v1/channels/:id
risk_acceptance_required422nãoAceite de risco necessário para número não-oficial
unauthorized401nãoAPI key ausente ou inválida
payload_too_large413nãoCorpo acima do limite
invalid_signature403nãoAssinatura de webhook inválida
quota_exceeded402nãoCota de mensagens do período atingida
api_key_last_active409nãoNão revoga a última key ativa
template_not_found404nãoTemplate inexistente ou de outro tenant
template_not_editable409nãoSó approved/rejected podem ser editados
delivery_not_replayable409nãoReplay só a partir da DLQ (admin)
delivery_not_discardable409nãoDescarte só se aplica a entregas mortas na DLQ (admin)
resource_not_found404nãoRecurso administrativo inexistente (admin)
insufficient_permission403nãoCredencial válida, mas sem a permissão exigida pelo endpoint
tenant_suspended403nãoTenant suspenso — acesso a /v1/* bloqueado até reativar
member_not_found404nãoMembro inexistente ou de outra organização
last_owner409nãoNão remove/rebaixa o último dono do tenant — promova outro antes
meta_graph_token_unavailable422nãoNenhum token de Graph configurado para a WABA (System User ou canal já pareado)
waba_number_already_onboarded409nãoNúmero já onboardado — já existe um canal vinculado a ele no snapshot da WABA
waba_number_unavailable409nãoNúmero já conectado em outro workspace
proxy_in_use409nãoProxy já atribuído a outro canal (admin)
no_waha_instance_available503simNenhuma instância não-oficial disponível para hospedar o número
waha_instance_not_found404nãoInstância não-oficial do pool inexistente (admin)
instance_has_live_sessions409nãoInstância com sessões vivas — drene antes de remover (admin)
warming_number_already_enrolled409nãoNúmero já matriculado numa estratégia de aquecimento
warming_tier_exceeded422nãoLimite de números do maior nível de aquecimento atingido
warming_enrollment_not_found404nãoMatrícula de aquecimento inexistente ou de outro tenant
no_available_channel409simNenhum número disponível no pool para o disparo
pool_name_taken409nãoJá existe um pool com esse nome
pool_slug_taken409nãoJá existe um pool com esse identificador (slug)
pool_slug_invalid422nãoIdentificador (slug) inválido — use minúsculas, números e hífens
report_not_found404nãoRelatório inexistente, de outro tenant, ou ainda sem artefato pronto
webhook_endpoint_limit409nãoLimite de destinos de webhook atingido para este workspace
webhook_endpoint_url_taken409nãoJá existe um destino de webhook com esta URL neste tenant
webhook_endpoint_not_found404nãoDestino de webhook inexistente ou de outro tenant
validation_error422nãoCorpo/query malformado — detalhe por campo em errors[]

Isolamento entre tenants

Um recurso de outro tenant responde 404 (channel_not_found / template_not_found), nunca 403 — o broker não confirma sequer a existência de recursos alheios. As mensagens de cota/uso são genéricas: nunca vazam limite ou uso de outro tenant.

Códigos do lado cliente

Além dos códigos do broker, o SDK reporta duas falhas que acontecem antes de o broker responder em contrato. Nesses casos httpStatus é 0.

coderetryablesignificado
network_errorsimA conexão nunca se estabeleceu (DNS, recusa). Repetir é seguro.
timeoutnãoA requisição saiu mas a resposta não voltou. Indeterminado — pode ter sido processada; reconcilie, não reenvie automaticamente.

Timeout é incerteza, não falha

Um timeout pode ter sido processado pelo broker. Nunca faça retry automático dele: use a idempotencyKey e reconcilie (consulte messages.get) para não duplicar.

Boas práticas

  • Sempre cheque err instanceof BrokerApiError antes de ler code.
  • Respeite retryAfter em 429 — não faça busy-retry.
  • Registre traceId nos seus logs para agilizar o suporte.
  • Trate o default do switch por retryable, para lidar com códigos futuros com segurança.