Erros
Formato canônico de error response da Hub API, códigos HTTP utilizados, semântica de cada status e boas práticas para implementar handlers de erro resilientes em clientes.
Erros
A Hub API retorna erros em um formato JSON único, alinhado ao schema OpenAPI Error registrado em apps/api/src/lib/openapi/schemas.ts. Esta página descreve o contrato exato, os códigos HTTP usados em produção e como construir um handler resiliente do lado do cliente.
Em condições de erro, o servidor pode retornar Content-Type: text/plain se a falha ocorrer antes do route handler (ex.: timeout do proxy, erro do ingress). Sempre verifique response.ok antes de assumir que o corpo é JSON.
Formato canônico (JSON)
Todo erro retornado pelos route handlers segue este shape mínimo:
{
"error": "string descrevendo o erro",
"details": {}
}O campo details é opcional e usado principalmente em respostas de validação. Para erros de schema (Zod), o middleware validateBody em apps/api/src/lib/middleware/validate.ts preenche details com o mapa fieldErrors retornado por result.error.flatten().
Exemplo de erro de validação:
{
"error": "Validation error",
"details": {
"email": ["Invalid email"],
"password": ["Password must be at least 8 characters"]
}
}Exemplo de erro de permissão emitido por requirePermission:
{
"error": "Insufficient permissions",
"required": "channels:edit"
}A mensagem em error é destinada a operadores e logs, não ao usuário final. Não a exiba diretamente em UIs voltadas ao cliente — mapeie por status HTTP + contexto da chamada.
Códigos HTTP utilizados
A Hub API utiliza um subconjunto enxuto de códigos HTTP. Cada um tem semântica fixa e o cliente deve tratá-los de forma distinta:
| Código | Significado | Quando ocorre |
|---|---|---|
400 | Bad Request | Payload inválido, parâmetro obrigatório ausente, JSON malformado, enum fora do conjunto permitido |
401 | Unauthorized | Header Authorization ausente, token expirado, API key revogada, refresh token inválido |
403 | Forbidden | Usuário autenticado mas sem a permissão exigida pelo recurso (Insufficient permissions) |
404 | Not Found | Recurso não existe ou não pertence à org do caller |
409 | Conflict | Violação de unique constraint — slug em uso, key duplicada, membership já existente |
410 | Gone | Token efêmero (QR link, invitation) expirado ou consumido |
413 | Payload Too Large | Upload acima do limite (ex.: import CSV de campanha > 5MB) |
429 | Too Many Requests | Rate limit excedido — endpoints de auth e webhooks |
500 | Internal Server Error | Falha não tratada, dependência interna mal configurada |
502 | Bad Gateway | Upstream falhou (Langfuse, provider Pipefy, Evolution API) |
O middleware global em apps/api/src/middleware.ts protege rotas sensíveis (/auth/*, /webhooks/*) com rate limit de 10 requisições por IP a cada 5 minutos. Quando bloqueia, retorna 429 com header Retry-After: 300.
Respostas a permissão
Quando uma rota exige uma permissão específica e o caller não a possui, a resposta inclui o par resource:action esperado:
{
"error": "Insufficient permissions",
"required": "inbox:edit"
}Esse formato vem de requirePermission em apps/api/src/lib/middleware/auth.ts e permite ao cliente sugerir ao admin quais escopos conceder à membership.
Respostas a auth
| Cenário | Status | Mensagem em error |
|---|---|---|
Header Authorization ausente | 401 | Missing authorization header |
| Token JWT inválido ou expirado | 401 | Invalid or expired token |
| API key revogada | 401 | Invalid or revoked API key |
| API key expirada | 401 | API key has expired |
| Refresh token ausente (cookie) | 401 | No refresh token |
| Bearer não confere com segredo interno | 401 | Unauthorized |
Códigos de erro internos
A Hub API não utiliza um campo code enumerado em todas as respostas. A identificação do erro é feita pela combinação de status HTTP + error (string canônica). Algumas strings recorrentes aparecem em múltiplos endpoints e podem ser tratadas como códigos lógicos:
Strings emitidas pelos middlewares de autenticação e autorização.
Missing authorization header
Missing authorization
Invalid or expired token
Invalid or revoked API key
API key has expired
No refresh token
Insufficient permissions
Unauthorized
Internal auth not configured
Cron auth not configuredStrings emitidas por validateBody e checks de parâmetro obrigatório nos route handlers.
Validation error
Invalid JSON body
orgId is required
phoneNumber required
Token is required
Confirmation requiredStrings de 404 / 409 / 410 emitidas pelos handlers de domínio.
Not found
Lead not found
Channel not found
Session not found
Link not found
Link expired
Link already used
Slug already taken
Key already exists in org
Name already exists
Pipeline already exists for this provider + externalId
Channel is not connected
Not a member
Not a member of organization
Not a QR_CODE channel
Not a WWABA channelStrings emitidas quando uma dependência externa falha (status 500 ou 502).
Langfuse not configured
Failed to save to Langfuse
Failed to fetch from Langfuse
Langfuse upstream error
Provider fetch failed
Provider discovery failed
No credential set found
No credential set found for providerEstas strings são contratos informais — podem ser usadas para roteamento de UI ou métricas, mas não há garantia de estabilidade entre versões. Sempre priorize o status HTTP como sinal canônico e use a string apenas como dica complementar.
Boas práticas para handler de erro
Um cliente bem comportado deve seguir estes princípios ao consumir a Hub API.
Sempre cheque response.ok antes do parse
Erros do ingress, do proxy ou de timeout podem chegar como HTML ou texto puro. Confie no status code, não no Content-Type.
const res = await fetch(`${HUB_API}/orgs/${orgId}/channels`, {
headers: { Authorization: `Bearer ${token}` },
});
if (!res.ok) {
const body = await res.text();
let parsed: { error?: string; details?: unknown } = {};
try {
parsed = JSON.parse(body);
} catch {
// resposta não-JSON (HTML do ingress, plain text de proxy)
}
throw new HubApiError(res.status, parsed.error ?? body, parsed.details);
}
const data = await res.json();Roteie pelo status, não pela string
Crie um switch sobre response.status e mapeie cada faixa para uma ação. A string em error é útil para logs e Sentry/GlitchTip, mas não deve ser usada como chave de UI.
switch (status) {
case 400: return showFieldErrors(details);
case 401: return redirectToLogin();
case 403: return showForbiddenBanner(parsed.required);
case 404: return showNotFound();
case 409: return showConflictDialog(parsed.error);
case 410: return refreshEphemeralToken();
case 413: return showFileTooLarge();
case 429: return scheduleRetry(parseRetryAfter(headers));
case 502: return showUpstreamDegraded();
default: return showGenericError();
}Respeite Retry-After em respostas 429
O rate limiter global retorna Retry-After em segundos. Implemente backoff exponencial com jitter sobre esse valor — nunca faça retry imediato.
function parseRetryAfter(headers: Headers): number {
const raw = headers.get("Retry-After");
const seconds = raw ? parseInt(raw, 10) : 60;
const jitter = Math.random() * 1000;
return seconds * 1000 + jitter;
}Trate 401 separando expiração de revogação
Tokens JWT expiram a cada 15 minutos e devem ser renovados via POST /api/v1/auth/refresh. Já um Invalid or revoked API key indica ação humana — não tente refresh, force novo login ou regere a key.
Não exponha error ao usuário final
Mensagens como Insufficient permissions ou Pipeline already exists for this provider + externalId são operacionais. Traduza para mensagens de produto em pt-BR no cliente, usando o status HTTP como discriminante.
Inclua o corpo do erro em logs estruturados
Ao reportar para GlitchTip ou Sentry, anexe status, error, details e o request-id (header x-request-id quando presente). Isso acelera correlação com traces no Tempo e logs no Loki.