Códigos de erro (EP / EM)
Esta página lista os códigos estáveis que o front-end usa para classificar
qualquer falha de API que derrube a sessão do usuário. Serve como
contrato de triagem com o time de back-end: quando um usuário reporta
"fui deslogado, código #EP0120-EM0003", o time sabe exatamente qual
endpoint bateu e qual família de erro gerou o logout.
Os códigos são estáveis e imutáveis — uma vez publicados, nunca mudam de valor. Novos códigos entram sempre ao final do registro (com gaps aceitáveis). Edit/remove é proibido.
- Fonte de verdade:
packages/api-client/src/errors/endpoint-codes.ts(EP) epackages/api-client/src/errors/error-codes.ts(EM) - Classifier:
classifyApiError({ status, data, rawBody, url })em@cactus-agents/api-clientretorna{ endpointCode, errorCode, ref, ... }onderef = "EPXXXX-EMXXXX" - Onde aparece: banner de session-expired no
LoginModaldo base, toast pós-401, beaconPOST /api/logs/auth-logoutgravado em CF Observability comrefestruturado
Formato do código de referência
#EP0120-EM0003
│ │
│ └─ EM → qual família de erro (401 expired, 401 ip_changed, etc.)
└───────── EP → qual endpoint do BFF estava sendo chamado
Tanto EP quanto EM têm 4 dígitos com zero-padding. O separador é
sempre -. O prefixo # é opcional na apresentação visual (usamos no
banner de UI, não guardamos no log).
EP — Endpoint Codes
Mapeia URL do BFF → código EP. Registry em ordem histórica (nunca
reordenar). O matcher aplica o primeiro pattern que bater, com query
string ignorada e prefixos de versão (/v1/, /v2/, /api/v2/) strippados
automaticamente.
Auth (EP0001 – EP0019)
| Código | Método | Endpoint |
|---|---|---|
EP0001 | POST | /auth/login |
EP0002 | POST | /auth/logout |
EP0003 | GET | /auth/user-profile |
EP0004 | POST | /users/refresh-token |
EP0005 | POST | /bff/register-simplified |
EP0006 | POST | /documents/validate |
EP0007 | POST | /auth/passwords/reset/options |
EP0008 | POST | /auth/passwords/reset/by-email |
EP0009 | POST | /auth/passwords/reset/by-sms |
EP0010 | POST | /auth/passwords/reset/validate-code |
EP0011 | POST | /auth/passwords/reset/confirm |
User / Account (EP0020 – EP0069)
| Código | Método | Endpoint |
|---|---|---|
EP0020 | POST | /users/update/{id} |
EP0021 | GET | /bff/users/address-by-user |
EP0022 | PATCH | /bff/users/self-email |
EP0023 | PATCH | /bff/users/add-phone |
EP0024 | PATCH | /bff/users/update-address |
EP0025 | PATCH | /bff/users/self-contracts |
EP0026 | PATCH | /bff/users/self-mkt |
EP0027 | PATCH | /bff/users/update-user-info-metadata |
EP0028 | POST | /users/change-password/{id} |
EP0029 | POST | /bff/users/check-password |
EP0030 | PATCH | /bff/users/self-two-factor |
EP0031 | — | /bff/users/account-social (GET/POST/DELETE) |
EP0032 | GET | /bff/users/login-history |
EP0033 | GET | /bff/games/user-last-casino-games-dl |
EP0034 | GET | /users/wallet |
EP0035 | GET | /bonus/rollover |
EP0036 | GET | /bonus/rollover-accomplished |
EP0037 | POST | /documents/{type} |
EP0038 | PATCH | /bff/users/update-limits |
EP0039 | PATCH | /bff/users/timeout-limits |
EP0040 | PATCH | /bff/users/self-exclusion |
EP0041 | POST | /apicep |
EP0042 | POST | /income-report/generate |
EP0043 | GET | /income-report/available-years |
EP0044 | GET | /income-report/{id} |
EP0045 | PATCH | /bff/users/add-address |
EP0046 | PATCH | /bff/users/add-initial-data |
EP0047 | PATCH | /bff/users/self-phone |
EP0048 | POST | /bff/users/send-email |
EP0049 | POST | /bff/users/validade-email-code (typo preservado do BFF) |
EP0050 | POST | /bff/users/send-sms |
EP0051 | POST | /bff/users/validade-sms-code (typo preservado do BFF) |
EP0052 | POST | /bff/users/list-referrals |
EP0053 | PATCH | /bff/users/update-pending-data |
EP0054 | GET | /bff/users/zendesk/create-or-update-user |
Wallet (EP0070 – EP0089)
| Código | Método | Endpoint |
|---|---|---|
EP0070 | POST | /bff/transactions |
EP0071 | GET | /transactions/cashback |
EP0072 | POST | /bonus/transfer |
EP0073 | POST | /cashback/transfer |
EP0074 | GET | /withdraw/{id}/generate |
Payments (EP0090 – EP0119)
| Código | Método | Endpoint |
|---|---|---|
EP0090 | GET | /payment-providers |
EP0091 | POST | /wallet/add-credit |
EP0092 | GET | /wallet/charge/{id} |
EP0093 | POST | /new-withdraws |
EP0094 | GET | /bff/users/bank-list |
EP0095 | POST | /pix-keys/user-key |
EP0096 | POST | /pix-keys/update-user-key-v2 |
EP0097 | GET | /mex-bank-accounts/user-account |
EP0098 | POST | /mex-bank-accounts/store |
EP0099 | GET | /generic-bank-accounts/user-account |
EP0100 | POST | /generic-bank-accounts/store |
KYC (EP0120 – EP0129)
| Código | Método | Endpoint |
|---|---|---|
EP0120 | — | /bff/users/kyc* (cobre query params + sub-rotas /status) |
Gamification / Rewards (EP0130 – EP0139)
| Código | Método | Endpoint |
|---|---|---|
EP0130 | GET | /bff/gamification/rewards |
EP0131 | POST | /bff/gamification/redeem |
Games / Casino (EP0140 – EP0189)
| Código | Método | Endpoint |
|---|---|---|
EP0140 | GET | /casino-games/page/{page} |
EP0141 | GET | /casino-games/home |
EP0142 | GET | /casino-games/list/base |
EP0143 | GET | /casino-games/list |
EP0144 | GET | /casino-games |
EP0145 | GET | /casino-games/filter (legacy) |
EP0146 | GET | /start-game-v2 |
EP0147 | GET | /bff/games/top-wins-dl |
EP0148 | GET | /bff/games/top-wins-game-dl/{slug} |
EP0149 | GET | /bff/games/last-wins |
EP0150 | GET | /bff/games/game-high-payers-dl |
EP0151 | GET | /bff/games/statistics-by-period-dl/{slug} |
EP0152 | GET | /bff/games/statistics-dl/{slug} |
EP0153 | GET | /bff/games/game-vote/{slug} |
EP0154 | GET | /bff/games/game-vote-count/{slug} |
EP0155 | POST | /casino-game-votes/store |
EP0156 | DELETE | /casino-game-votes/destroy/{id} |
EP0157 | GET | /bff/favorite-games |
EP0158 | PUT | /bff/favorite-games/toggle |
Sports / Sportsbook (EP0190 – EP0199)
| Código | Método | Endpoint |
|---|---|---|
EP0190 | GET | /cactus-sportbook/search |
EP0191 | GET | /cactus-sportbook/launch |
EP0192 | GET | /cactus-sportbook/anonymous-launch |
EP0193 | POST | /betby/get-jwt |
EP0194 | GET | /alternar/token (typo preservado do BFF — deveria ser "altenar") |
Brand / Platform (EP0200 – EP0209)
| Código | Método | Endpoint |
|---|---|---|
EP0200 | GET | /appearance |
EP0201 | GET | /bff/features |
EP0202 | POST | /bookmaker-settings |
EP0203 | GET | /getlegalterm |
Internal proxies do Base (EP0700 – EP0799)
Rotas /api/* same-origin do template React. Matcham quando o 401 saiu do
proxy do Base antes de chegar no BFF (geralmente quando o proxy fez auth
gate ou o token não estava presente).
| Código | Método | Endpoint |
|---|---|---|
EP0700 | POST | /api/auth/login |
EP0701 | POST | /api/auth/logout |
EP0702 | GET | /api/auth/profile |
EP0703 | POST | /api/auth/register |
EP0704 | POST | /api/auth/refresh |
EP0705 | POST | /api/auth/recovery |
EP0706 | — | /api/auth/social/{provider} |
EP0707 | POST | /api/auth/validate-document |
EP0710 | — | /api/wallet/* |
EP0711 | — | /api/payments/* |
EP0712 | — | /api/user/* |
EP0713 | — | /api/kyc/* |
EP0714 | — | /api/rewards/* |
EP0715 | — | /api/validation/* |
EP0716 | — | /api/sports/* |
EP0717 | — | /api/games/* |
EP0718 | — | /api/favorites |
EP0719 | — | /api/income-report/* |
EP0720 | POST | /api/logs/auth-logout |
EP0721 | POST | /api/auth/recheck-spa |
EP0799 | — | /api/* (fallback genérico quando nada acima bate) |
Unknown (EP0000)
| Código | Descrição |
|---|---|
EP0000 | URL não reconhecida (nem BFF catalog nem /api/*). Geralmente indica bug no classifier, 401 vindo de host estranho, ou endpoint novo ainda não registrado. |
EM — Error Codes
Classifica o tipo de falha independentemente do endpoint. O matcher
combina HTTP status + regex no campo de "reason" extraído do body (ordem
de prioridade: reason → code → error → message → detail.* →
status top-level string → rawBody).
Primeiro match vence — entries específicos (status + regex) sempre vêm antes dos genéricos (só status).
401 Unauthorized (EM0001 – EM0019)
| Código | Match na BFF | Descrição no UI (pt-br) |
|---|---|---|
EM0001 | no_token, missing_token, token_required, User not logged in | Sua sessão terminou. Faça login novamente. |
EM0002 | wrong_token, wrong_ha, wrong_hf1, wrrong_hf2 (typo do BFF), invalid_token, malformed, bad_signature, Invalido | Sua sessão é inválida. |
EM0003 | expired, token_expired, qualquer exp* | Sua sessão expirou. |
EM0004 | revoked, blacklist, blocked, banned | Seu acesso foi revogado. |
EM0005 | (fallback 401 quando nenhum outro bate) | Sua sessão não é mais válida. |
EM0006 | ip_changed, ip_mismatch | Detectamos acesso de um IP diferente. |
EM0007 | device_changed, device_mismatch | Detectamos acesso de um dispositivo diferente. |
EM0008 | location_changed, geo_mismatch | Detectamos acesso de uma localização diferente. |
EM0009 | liveness_required, kyc_required | Verificação de segurança necessária. |
EM0010 | deprecated_version, legacy_version, obsolete_version | Sessão de versão antiga. |
EM0012 | user_blocked, user_inactive, bloqueado/a, não autorizado | Usuário bloqueado, contate o suporte. |
EM0013 | security_error, failed_auth_check | Erro de segurança ao validar sua sessão. |
EM0014 | Wrong auth validation (1, 2, 3), Wrong auth refresh! | Sua sessão não passou na validação de segurança. |
Origem no BFF (Laravel):
- EM0002, EM0006–EM0010 →
app/Models/User.php::isValidJwtTokenPayload()- EM0012, EM0013, EM0014 →
app/Http/Middleware/CheckBlocked.php- EM0001 →
app/Http/Middleware/CheckUserLoggedIn.php
403 Forbidden (EM0011)
| Código | Match na BFF | Descrição no UI |
|---|---|---|
EM0011 | qualquer 403 | Você não tem permissão para acessar este recurso. |
409 / 429 (EM0021 – EM0029)
| Código | Match na BFF | Descrição no UI |
|---|---|---|
EM0021 | qualquer 429 | Muitas tentativas em pouco tempo. |
EM0022 | qualquer 409 | Conflito ao processar sua solicitação. |
5xx (EM0031 – EM0039)
| Código | Match na BFF | Descrição no UI |
|---|---|---|
EM0031 | status 500 | Ocorreu um erro no servidor. |
EM0032 | status 502 / 503 / 504 | O serviço está indisponível no momento. |
Transporte / timing (EM0041 – EM0049)
| Código | Match | Descrição no UI |
|---|---|---|
EM0041 | status 202 (convenção ApiClient) | Sua sessão de jogo atingiu o limite de tempo. |
EM0042 | status 0 (erro de rede, sem resposta) | Falha de rede ao validar sua sessão. |
Unknown (EM0099)
| Código | Descrição |
|---|---|
EM0099 | Status HTTP fora dos buckets acima ou body sem campo de reason reconhecível. Fallback final — se aparecer muito, adicionar matcher novo. |
Envelope das rotas internas — preservar reason em 401/403
Rotas /api/* no Base que fazem proxy de chamadas BFF NÃO podem envelopar erros 401/403 em { ok: false, error: "x_failed", detail: "..." }. O classifier do front prioriza reason → code → error → message → detail.* (ver extractReasonString em error-codes.ts), então um envelope com error top-level sempre vence sobre o detail que carrega a reason real do BFF — resultado: toda 401 vira EM0005 (catch-all) e toda 403 vira EM0011 (catch-all), perdendo a granularidade EM0001 no_token, EM0002 wrong_token, EM0003 expired, EM0006 ip_changed, etc.
Como funcionou (caso reportado em 2026-05)
Usuário no cassino.bet.br recebeu o banner #EP0710-EM0005 ao jogar gates-of-olympus. O log no CF Observability mostrava reason: "wallet_fetch_failed", exatamente o error do envelope que o app/routes/api/wallet/refresh.ts retornava no catch:
// ❌ ANTES (envelope mascara reason real do BFF)
} catch (err) {
const { status, message } = extractApiError(err);
return Response.json(
{ ok: false, error: "wallet_fetch_failed", detail: message },
{ status: status ?? 500 },
);
}
O BFF tinha respondido com reason: "expired" (que classificaria como EM0003), mas o classifier nunca chegou a olhar detail — pegou error: "wallet_fetch_failed" primeiro, não bateu nenhum regex, caiu em EM0005.
Como deve funcionar agora
Usar os helpers em app/utils/proxy-error.server.ts:
proxyErrorResponse(err, fallbackName, init?)— em 401/403, preserva o body do BFF intacto. Nos demais status, mantém o envelope tradicional. Aceitainit.headers(praSet-Cookieem rotas com rate-limit) einit.extraBody(pra campos custom comolimitErrorno deposit, só nos status não-auth).unauthorizedNoToken()— emite{ reason: "no_token" }(status 401) quando a action detecta que o cookie JWT está ausente antes mesmo de chamar o BFF. Bate o regex de EM0001 (não vira EM0005 genérico).
// ✅ DEPOIS (BFF body passa intacto em 401/403, classifier lê reason real)
import { proxyErrorResponse, unauthorizedNoToken } from "~/utils/proxy-error.server";
export async function action({ request, context }: Route.ActionArgs) {
const token = getTokenFromRequest(request);
if (!token) return unauthorizedNoToken();
try {
// ... chama BFF
return Response.json({ ok: true, data });
} catch (err) {
return proxyErrorResponse(err, "wallet_fetch_failed");
}
}
Regras
- Toda rota
/api/*que faz proxy do BFF deve usar os helpers. Auditado em ~55 rotas (wallet, payments, user, kyc, rewards, income-report, validation, sports, games, address, favorites, auth/profile, auth/refresh, auth/recheck-spa). - Hoje o helper só preserva 401/403 (status auth-sensitive — os que disparam
handleUnauthorizedno front). 409, 429 e 5xx mantêm envelope. Se no futuro algum desses começar a disparar UX automático, estenderAUTH_SENSITIVE_STATUSESno helper. - Exceções legítimas — rotas de fluxo pre-auth (
/api/auth/login,/api/auth/register,/api/auth/recovery,/api/auth/social/*,/api/auth/validate-document) não usam o helper porque o usuário ainda não está autenticado e o catch precisa retornarFormErrorCodeespecífico viamapBff*Error. Não disparamhandleUnauthorized. /api/auth/logouttambém não usa — não tem catch BFF; sempre retorna 200 com cookies limpos.
Adicionar rota nova
- Importar os helpers:
import { proxyErrorResponse, unauthorizedNoToken } from "~/utils/proxy-error.server"; - Token check no início:
if (!token) return unauthorizedNoToken(); - Catch genérico:
} catch (err) { return proxyErrorResponse(err, "<short_failure_name>"); } - Se a rota tem
Set-Cookieno erro (ex: rate-limit), passar viainit.headers. - Se a rota tem campo extra no envelope que o client consome em status não-auth (ex:
limitErrorno deposit), passar viainit.extraBody.
Implementação canônica: ver app/routes/api/wallet/refresh.ts. Casos com headers e extraBody: ver app/routes/api/auth/recheck-spa.ts e app/routes/api/payments/deposit.ts.
Testes de regressão no classifier core: repos/front-cactus-core/packages/api-client/src/__tests__/error-codes.test.ts (suite "Cactus internal proxy routes (regression guard)").
Como fazer triagem de um ticket
- Usuário reporta
#EP0120-EM0003(via toast / banner / screenshot). - Bater
EP0120nesta página →/bff/users/kyc*→ é o endpoint de KYC. - Bater
EM0003nesta página → 401 comreason: expired→ token JWT expirou legitimamente. - Conclusão: usuário teve o token expirando enquanto visitava a
página de KYC. UX esperada (fluxo de sessão expirada). Se o
reportante diz "acabei de logar", então o problema é provavelmente do
lado do BFF (JWT emitido com TTL errado) — investigar no Laravel
config/jwt.php+ logs de emissão.
Exemplo de triagem onde o front está errado:
- Usuário reporta
#EP0000-EM0099. EP0000→ endpoint não reconhecido. Bug do classifier ou 401 vazando de host estranho (ex: WordPress, ad tech que caiu no interceptor).- Checar whitelist em
app/utils/api-fetch.client.ts::shouldReactToUnauthorized.
Como adicionar um endpoint/erro novo
Endpoint novo
- Editar
packages/api-client/src/errors/endpoint-codes.ts - Appendar a entry no grupo semântico correto (Auth, User, Payments, etc.)
- Usar o próximo código livre no range daquele grupo (gaps aceitáveis)
- Nunca reordenar entries existentes
- Adicionar teste em
endpoint-codes.test.ts - Atualizar esta doc + regenerar via mão mesmo (não automatizamos pra forçar review humano)
- Changeset
@cactus-agents/api-clientpatch
Erro novo
- Editar
packages/api-client/src/errors/error-codes.ts - Adicionar regex específico antes do fallback genérico daquele status
- Próximo EM livre no range do status (
EM0001-09para 401,EM0011-19para 403, etc.) - Adicionar i18n key em
packages/i18n/locales/*/auth.jsonsobsession_error.<key> - Adicionar teste em
error-codes.test.tscobrindo o novo match - Atualizar esta doc
- Changeset
@cactus-agents/api-clientpatch +@cactus-agents/i18npatch
Observabilidade
Todo logout forçado no front dispara um beacon estruturado:
POST /api/logs/auth-logout
{
"ref": "EP0120-EM0003",
"endpointCode": "EP0120",
"errorCode": "EM0003",
"status": 401,
"reason": "token_expired",
"url": "https://stage1-api-new.bs2bet.com/v2/bff/users/kyc?source=self",
"initiator": "fetch-interceptor",
"pathname": "/user/profile",
"timestamp": 1746543210123,
"hadCookie": true,
"userAgentShort": "Mozilla/5.0 …"
}
Server-side ele vira log.warn("forced-logout", {...}) no Worker
Cloudflare — filtrável no dashboard do CF Observability via
"forced-logout" ou pelo próprio ref. Sampling: 50% via
[observability.logs] head_sampling_rate do wrangler.toml.