Pular para o conteúdo principal

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) e packages/api-client/src/errors/error-codes.ts (EM)
  • Classifier: classifyApiError({ status, data, rawBody, url }) em @cactus-agents/api-client retorna { endpointCode, errorCode, ref, ... } onde ref = "EPXXXX-EMXXXX"
  • Onde aparece: banner de session-expired no LoginModal do base, toast pós-401, beacon POST /api/logs/auth-logout gravado em CF Observability com ref estruturado

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ódigoMétodoEndpoint
EP0001POST/auth/login
EP0002POST/auth/logout
EP0003GET/auth/user-profile
EP0004POST/users/refresh-token
EP0005POST/bff/register-simplified
EP0006POST/documents/validate
EP0007POST/auth/passwords/reset/options
EP0008POST/auth/passwords/reset/by-email
EP0009POST/auth/passwords/reset/by-sms
EP0010POST/auth/passwords/reset/validate-code
EP0011POST/auth/passwords/reset/confirm

User / Account (EP0020 – EP0069)

CódigoMétodoEndpoint
EP0020POST/users/update/{id}
EP0021GET/bff/users/address-by-user
EP0022PATCH/bff/users/self-email
EP0023PATCH/bff/users/add-phone
EP0024PATCH/bff/users/update-address
EP0025PATCH/bff/users/self-contracts
EP0026PATCH/bff/users/self-mkt
EP0027PATCH/bff/users/update-user-info-metadata
EP0028POST/users/change-password/{id}
EP0029POST/bff/users/check-password
EP0030PATCH/bff/users/self-two-factor
EP0031/bff/users/account-social (GET/POST/DELETE)
EP0032GET/bff/users/login-history
EP0033GET/bff/games/user-last-casino-games-dl
EP0034GET/users/wallet
EP0035GET/bonus/rollover
EP0036GET/bonus/rollover-accomplished
EP0037POST/documents/{type}
EP0038PATCH/bff/users/update-limits
EP0039PATCH/bff/users/timeout-limits
EP0040PATCH/bff/users/self-exclusion
EP0041POST/apicep
EP0042POST/income-report/generate
EP0043GET/income-report/available-years
EP0044GET/income-report/{id}
EP0045PATCH/bff/users/add-address
EP0046PATCH/bff/users/add-initial-data
EP0047PATCH/bff/users/self-phone
EP0048POST/bff/users/send-email
EP0049POST/bff/users/validade-email-code (typo preservado do BFF)
EP0050POST/bff/users/send-sms
EP0051POST/bff/users/validade-sms-code (typo preservado do BFF)
EP0052POST/bff/users/list-referrals
EP0053PATCH/bff/users/update-pending-data
EP0054GET/bff/users/zendesk/create-or-update-user

Wallet (EP0070 – EP0089)

CódigoMétodoEndpoint
EP0070POST/bff/transactions
EP0071GET/transactions/cashback
EP0072POST/bonus/transfer
EP0073POST/cashback/transfer
EP0074GET/withdraw/{id}/generate

Payments (EP0090 – EP0119)

CódigoMétodoEndpoint
EP0090GET/payment-providers
EP0091POST/wallet/add-credit
EP0092GET/wallet/charge/{id}
EP0093POST/new-withdraws
EP0094GET/bff/users/bank-list
EP0095POST/pix-keys/user-key
EP0096POST/pix-keys/update-user-key-v2
EP0097GET/mex-bank-accounts/user-account
EP0098POST/mex-bank-accounts/store
EP0099GET/generic-bank-accounts/user-account
EP0100POST/generic-bank-accounts/store

KYC (EP0120 – EP0129)

CódigoMétodoEndpoint
EP0120/bff/users/kyc* (cobre query params + sub-rotas /status)

Gamification / Rewards (EP0130 – EP0139)

CódigoMétodoEndpoint
EP0130GET/bff/gamification/rewards
EP0131POST/bff/gamification/redeem

Games / Casino (EP0140 – EP0189)

CódigoMétodoEndpoint
EP0140GET/casino-games/page/{page}
EP0141GET/casino-games/home
EP0142GET/casino-games/list/base
EP0143GET/casino-games/list
EP0144GET/casino-games
EP0145GET/casino-games/filter (legacy)
EP0146GET/start-game-v2
EP0147GET/bff/games/top-wins-dl
EP0148GET/bff/games/top-wins-game-dl/{slug}
EP0149GET/bff/games/last-wins
EP0150GET/bff/games/game-high-payers-dl
EP0151GET/bff/games/statistics-by-period-dl/{slug}
EP0152GET/bff/games/statistics-dl/{slug}
EP0153GET/bff/games/game-vote/{slug}
EP0154GET/bff/games/game-vote-count/{slug}
EP0155POST/casino-game-votes/store
EP0156DELETE/casino-game-votes/destroy/{id}
EP0157GET/bff/favorite-games
EP0158PUT/bff/favorite-games/toggle

Sports / Sportsbook (EP0190 – EP0199)

CódigoMétodoEndpoint
EP0190GET/cactus-sportbook/search
EP0191GET/cactus-sportbook/launch
EP0192GET/cactus-sportbook/anonymous-launch
EP0193POST/betby/get-jwt
EP0194GET/alternar/token (typo preservado do BFF — deveria ser "altenar")

Brand / Platform (EP0200 – EP0209)

CódigoMétodoEndpoint
EP0200GET/appearance
EP0201GET/bff/features
EP0202POST/bookmaker-settings
EP0203GET/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ódigoMétodoEndpoint
EP0700POST/api/auth/login
EP0701POST/api/auth/logout
EP0702GET/api/auth/profile
EP0703POST/api/auth/register
EP0704POST/api/auth/refresh
EP0705POST/api/auth/recovery
EP0706/api/auth/social/{provider}
EP0707POST/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/*
EP0720POST/api/logs/auth-logout
EP0721POST/api/auth/recheck-spa
EP0799/api/* (fallback genérico quando nada acima bate)

Unknown (EP0000)

CódigoDescrição
EP0000URL 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: reasoncodeerrormessagedetail.*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ódigoMatch na BFFDescrição no UI (pt-br)
EM0001no_token, missing_token, token_required, User not logged inSua sessão terminou. Faça login novamente.
EM0002wrong_token, wrong_ha, wrong_hf1, wrrong_hf2 (typo do BFF), invalid_token, malformed, bad_signature, InvalidoSua sessão é inválida.
EM0003expired, token_expired, qualquer exp*Sua sessão expirou.
EM0004revoked, blacklist, blocked, bannedSeu acesso foi revogado.
EM0005(fallback 401 quando nenhum outro bate)Sua sessão não é mais válida.
EM0006ip_changed, ip_mismatchDetectamos acesso de um IP diferente.
EM0007device_changed, device_mismatchDetectamos acesso de um dispositivo diferente.
EM0008location_changed, geo_mismatchDetectamos acesso de uma localização diferente.
EM0009liveness_required, kyc_requiredVerificação de segurança necessária.
EM0010deprecated_version, legacy_version, obsolete_versionSessão de versão antiga.
EM0012user_blocked, user_inactive, bloqueado/a, não autorizadoUsuário bloqueado, contate o suporte.
EM0013security_error, failed_auth_checkErro de segurança ao validar sua sessão.
EM0014Wrong 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ódigoMatch na BFFDescrição no UI
EM0011qualquer 403Você não tem permissão para acessar este recurso.

409 / 429 (EM0021 – EM0029)

CódigoMatch na BFFDescrição no UI
EM0021qualquer 429Muitas tentativas em pouco tempo.
EM0022qualquer 409Conflito ao processar sua solicitação.

5xx (EM0031 – EM0039)

CódigoMatch na BFFDescrição no UI
EM0031status 500Ocorreu um erro no servidor.
EM0032status 502 / 503 / 504O serviço está indisponível no momento.

Transporte / timing (EM0041 – EM0049)

CódigoMatchDescrição no UI
EM0041status 202 (convenção ApiClient)Sua sessão de jogo atingiu o limite de tempo.
EM0042status 0 (erro de rede, sem resposta)Falha de rede ao validar sua sessão.

Unknown (EM0099)

CódigoDescrição
EM0099Status 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. Aceita init.headers (pra Set-Cookie em rotas com rate-limit) e init.extraBody (pra campos custom como limitError no 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 handleUnauthorized no front). 409, 429 e 5xx mantêm envelope. Se no futuro algum desses começar a disparar UX automático, estender AUTH_SENSITIVE_STATUSES no 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 retornar FormErrorCode específico via mapBff*Error. Não disparam handleUnauthorized.
  • /api/auth/logout também não usa — não tem catch BFF; sempre retorna 200 com cookies limpos.

Adicionar rota nova

  1. Importar os helpers: import { proxyErrorResponse, unauthorizedNoToken } from "~/utils/proxy-error.server";
  2. Token check no início: if (!token) return unauthorizedNoToken();
  3. Catch genérico: } catch (err) { return proxyErrorResponse(err, "<short_failure_name>"); }
  4. Se a rota tem Set-Cookie no erro (ex: rate-limit), passar via init.headers.
  5. Se a rota tem campo extra no envelope que o client consome em status não-auth (ex: limitError no deposit), passar via init.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

  1. Usuário reporta #EP0120-EM0003 (via toast / banner / screenshot).
  2. Bater EP0120 nesta página → /bff/users/kyc* → é o endpoint de KYC.
  3. Bater EM0003 nesta página → 401 com reason: expired → token JWT expirou legitimamente.
  4. 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:

  1. Usuário reporta #EP0000-EM0099.
  2. EP0000 → endpoint não reconhecido. Bug do classifier ou 401 vazando de host estranho (ex: WordPress, ad tech que caiu no interceptor).
  3. Checar whitelist em app/utils/api-fetch.client.ts::shouldReactToUnauthorized.

Como adicionar um endpoint/erro novo

Endpoint novo

  1. Editar packages/api-client/src/errors/endpoint-codes.ts
  2. Appendar a entry no grupo semântico correto (Auth, User, Payments, etc.)
  3. Usar o próximo código livre no range daquele grupo (gaps aceitáveis)
  4. Nunca reordenar entries existentes
  5. Adicionar teste em endpoint-codes.test.ts
  6. Atualizar esta doc + regenerar via mão mesmo (não automatizamos pra forçar review humano)
  7. Changeset @cactus-agents/api-client patch

Erro novo

  1. Editar packages/api-client/src/errors/error-codes.ts
  2. Adicionar regex específico antes do fallback genérico daquele status
  3. Próximo EM livre no range do status (EM0001-09 para 401, EM0011-19 para 403, etc.)
  4. Adicionar i18n key em packages/i18n/locales/*/auth.json sob session_error.<key>
  5. Adicionar teste em error-codes.test.ts cobrindo o novo match
  6. Atualizar esta doc
  7. Changeset @cactus-agents/api-client patch + @cactus-agents/i18n patch

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.