Errors — HTTP Codes
Unified error format, HTTP codes 400 to 5xx, causes and handling strategies for a robust Wakapp.
Errors — HTTP Codes
The WakaStart API uses a unified error format (NestJS standard). Each HTTP code has a precise semantics.
Unified error format
json{ "statusCode": 403, "message": "Niveau d'administration insuffisant. Requis: CustomerAdmin, Actuel: User", "error": "Forbidden" }
For validation errors (400) with several invalid fields:
json{ "statusCode": 400, "message": [ "email must be an email", "firstName should not be empty", "profileId must be a UUID" ], "error": "Bad Request" }
Fields:
| Field | Type | Description |
|---|---|---|
statusCode | number | HTTP code |
message | string | string[] | Human-readable description of the problem |
error | string | Standard HTTP label |
400 — Bad Request
Invalid data. The body did not pass DTO validation.
Common causes:
- Invalid email
- Disposable-domain email (yopmail, mailinator…)
- Missing or malformed UUID
- Required field missing
- Value outside the enum (
teamLevelmust beNONE | VIEWER | CONTRIBUTOR | MEMBER | MANAGER | ADMIN) - Invalid/expired/already-used invitation token (on
/accept)
Action: Fix the request body and retry.
401 — Unauthorized
Missing or invalid authentication.
Case 1 — Missing enriched token
json{ "statusCode": 401, "message": "Token enrichi manquant ou invalide", "error": "Unauthorized" }
Causes:
x-enriched-tokenheader absent — check the server-side proxywakastart_tokencookie not set (the call to/api/auth/enrichfailed)- Enriched token expired — proceed with a refresh
organizationclaim absent from the Keycloak JWT (Keycloak Organizations not configured)keycloak_idin the DB does not match the Keycloaksub
Action: Attempt a token refresh (once). If the refresh fails → redirect to login.
Case 2 — Expired or invalid Bearer token
Action: Proactive refresh or redirect to login.
Case 3 — Invalid API Key
json{ "statusCode": 401, "message": "Cle API invalide", "error": "Unauthorized" }
The exact message varies depending on the cause (missing, invalid, or expired key — see Business errors). Don't test a literal message value: base your logic on the
401HTTP code.
Action: Check the API key (format sk_live_... or sk_test_...).
402 — Payment Required
Insufficient credits for an operation that consumes credits.
json{ "statusCode": 402, "message": "Insufficient credits", "error": "Payment Required" }
Action: Redirect to the credits purchase page (/billing/credits).
403 — Forbidden
The user is authenticated but lacks the necessary rights.
Case 1 — Insufficient admin level
json{ "statusCode": 403, "message": "Niveau d'administration insuffisant. Requis: CustomerAdmin, Actuel: User", "error": "Forbidden" }
Case 2 — Missing application right
json{ "statusCode": 403, "message": "Droit requis : users.create", "error": "Forbidden" }
Case 3 — Feature disabled
json{ "statusCode": 403, "message": "La fonctionnalité invitation n'est pas activée pour ce customer", "error": "Forbidden" }
Action: Display a clear message to the user. Do not retry automatically.
404 — Not Found
The requested identifier (WID or UUID) does not exist in the database.
A multi-tenant scope violation returns 403, not 404. A
CustomerAdmin(or any scoped token) attempting to access a Team, Network, Partner, or App outside their scope receives a403 Forbidden, not a404. This is what the scope guards do (assertTeamScope,assertNetworkScope,assertPartnerScope,assertAppFeatureScopeinpublic-api'sconfig-api.service.ts): an explicit rejection viaHttpStatus.FORBIDDEN. There is no "anti-BOLA 404" pattern on the API side — see 403 — Forbidden above for this case.
Action: Check that the identifier (WID or UUID) is correct.
409 — Conflict
A uniqueness constraint is violated.
Common cases:
- Email already used within the Customer
PENDINGinvitation already exists for this email + Customer- Subdomain already used by another Network
Action: Adapt the UI to inform the user of the conflict.
429 — Too Many Requests
Rate limit exceeded.
Headers: Retry-After: 2 (duration in seconds before you can retry)
Action: Respect the Retry-After header and implement an exponential backoff.
typescriptconst retryAfter = parseInt(res.headers.get("Retry-After") ?? "2", 10); await new Promise(r => setTimeout(r, retryAfter * 1000)); // Only 1 retry, then fail cleanly
5xx — Server errors
Possible causes:
- Unreachable downstream service (Keycloak, the WakaStart configuration service, the WakaStart authentication service)
- Prisma error (unexpected DB constraint)
- Internal timeout
- Keycloak provisioning issue (during invitation
/accept)
Action: Retry with exponential backoff (max 3 attempts). If persistent, contact support with the timestamp, the route called, and the error response body (x-request-id is not implemented to date).
typescript// No x-request-id: log route + method + status + body for support logger.error("API 500", { url, method, status: res.status });
Recommended frontend error handling
typescriptclass ApiError extends Error { constructor( public readonly status: number, public readonly body: { statusCode: number; message: string | string[]; error: string } ) { super(Array.isArray(body.message) ? body.message.join(", ") : body.message); } get isForbidden() { return this.status === 403; } get isUnauthorized(){ return this.status === 401; } get isConflict() { return this.status === 409; } get isRateLimit() { return this.status === 429; } get isServerError() { return this.status >= 500; } }
Go further
- Business errors: custom business codes, exact messages
- Common pitfalls: diagnosing frequent errors
- Rate limits: limits per endpoint