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
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:
{
"code": "rate_limited",
"message": "Limite de envio do número atingido",
"retryable": true,
"retryAfter": 30,
"traceId": "tr_01hz..."
}code/message/retryablesempre presentes.retryAfter(segundos) só emrate_limitedequeue_saturated— os dois únicos códigos de back-pressure.traceIdsempre presente — a borda do broker carimba toda requisição com umtrace_id(o mesmox-trace-iddo chamador, quando enviado) e o middleware de erro preenchetraceIdem toda resposta que ainda não o carregue, inclusive um 500 desconhecido. Registre-o nos seus logs para agilizar o suporte.errors[]só aparece emvalidation_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
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 carregaerrors[].
Exemplo real — enviar um template sem o campo obrigatório `language`:
{
"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:
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
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).
| code | HTTP | retryable | significado |
|---|---|---|---|
outside_24h_window | 409 | não | Janela de 24h expirada — só template |
invalid_recipient | 422 | não | Número não encontrado no WhatsApp |
template_not_approved | 409 | não | Template não aprovado para envio |
template_call_invalid | 422 | não | Chamada ao template malformada (parâmetros) |
channel_disconnected | 503 | sim | Número desconectado |
channel_quarantined | 409 | não | Número em quarentena de onboarding (janela após a 1ª conexão) |
channel_limited | 409 | não | Número limitado por rajada de falhas — requer liberação manual |
channel_restricted | 409 | não | Número restrito pelo WhatsApp p/ contatos novos — auto-expira no prazo |
rate_limited | 429 | sim | Limite de envio do número (com retryAfter) |
queue_saturated | 429 | sim | Fila do tenant acima do limite (com retryAfter) |
recipient_suppressed | 409 | não | Contato optou por não receber (código legado) |
recipient_opted_out | 422 | não | Destinatário optou por não receber — opt-out (Compliance v1) |
message_expired | 410 | não | Mensagem expirou antes do envio |
media_url_expired | 422 | não | URL de mídia expira antes do prazo |
unsupported_capability | 422 | não | Recurso não suportado por este número |
provider_error | 502 | sim | Falha na comunicação com o provider |
provider_rejected | 422 | não | Provider rejeitou a requisição (4xx definitivo) |
channel_not_found | 404 | não | Número inexistente ou de outro tenant |
channel_ambiguous | 409 | não | O número casa mais de um canal vivo do tenant — passe o channelId (ch_…) |
channel_already_exists | 409 | não | Já há um número para este telefone neste app da plataforma oficial |
number_in_use | 409 | não | Número já ativo em outra sessão |
reconnect_number_mismatch | 409 | não | QR escaneado revelou um telefone diferente do número |
attempt_not_cancellable | 409 | não | Tentativa de conexão já promovida a canal — cancele o canal por DELETE /v1/channels/:id |
risk_acceptance_required | 422 | não | Aceite de risco necessário para número não-oficial |
unauthorized | 401 | não | API key ausente ou inválida |
payload_too_large | 413 | não | Corpo acima do limite |
invalid_signature | 403 | não | Assinatura de webhook inválida |
quota_exceeded | 402 | não | Cota de mensagens do período atingida |
api_key_last_active | 409 | não | Não revoga a última key ativa |
template_not_found | 404 | não | Template inexistente ou de outro tenant |
template_not_editable | 409 | não | Só approved/rejected podem ser editados |
delivery_not_replayable | 409 | não | Replay só a partir da DLQ (admin) |
delivery_not_discardable | 409 | não | Descarte só se aplica a entregas mortas na DLQ (admin) |
resource_not_found | 404 | não | Recurso administrativo inexistente (admin) |
insufficient_permission | 403 | não | Credencial válida, mas sem a permissão exigida pelo endpoint |
tenant_suspended | 403 | não | Tenant suspenso — acesso a /v1/* bloqueado até reativar |
member_not_found | 404 | não | Membro inexistente ou de outra organização |
last_owner | 409 | não | Não remove/rebaixa o último dono do tenant — promova outro antes |
meta_graph_token_unavailable | 422 | não | Nenhum token de Graph configurado para a WABA (System User ou canal já pareado) |
waba_number_already_onboarded | 409 | não | Número já onboardado — já existe um canal vinculado a ele no snapshot da WABA |
waba_number_unavailable | 409 | não | Número já conectado em outro workspace |
proxy_in_use | 409 | não | Proxy já atribuído a outro canal (admin) |
no_waha_instance_available | 503 | sim | Nenhuma instância não-oficial disponível para hospedar o número |
waha_instance_not_found | 404 | não | Instância não-oficial do pool inexistente (admin) |
instance_has_live_sessions | 409 | não | Instância com sessões vivas — drene antes de remover (admin) |
warming_number_already_enrolled | 409 | não | Número já matriculado numa estratégia de aquecimento |
warming_tier_exceeded | 422 | não | Limite de números do maior nível de aquecimento atingido |
warming_enrollment_not_found | 404 | não | Matrícula de aquecimento inexistente ou de outro tenant |
no_available_channel | 409 | sim | Nenhum número disponível no pool para o disparo |
pool_name_taken | 409 | não | Já existe um pool com esse nome |
pool_slug_taken | 409 | não | Já existe um pool com esse identificador (slug) |
pool_slug_invalid | 422 | não | Identificador (slug) inválido — use minúsculas, números e hífens |
report_not_found | 404 | não | Relatório inexistente, de outro tenant, ou ainda sem artefato pronto |
webhook_endpoint_limit | 409 | não | Limite de destinos de webhook atingido para este workspace |
webhook_endpoint_url_taken | 409 | não | Já existe um destino de webhook com esta URL neste tenant |
webhook_endpoint_not_found | 404 | não | Destino de webhook inexistente ou de outro tenant |
validation_error | 422 | não | Corpo/query malformado — detalhe por campo em errors[] |
Isolamento entre tenants
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.
| code | retryable | significado |
|---|---|---|
network_error | sim | A conexão nunca se estabeleceu (DNS, recusa). Repetir é seguro. |
timeout | não | A requisição saiu mas a resposta não voltou. Indeterminado — pode ter sido processada; reconcilie, não reenvie automaticamente. |
Timeout é incerteza, não falha
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 BrokerApiErrorantes de lercode. - Respeite
retryAfterem 429 — não faça busy-retry. - Registre
traceIdnos seus logs para agilizar o suporte. - Trate o
defaultdo switch porretryable, para lidar com códigos futuros com segurança.