WakaStart
Using the API

Errors — HTTP Codes

Unified error format, HTTP codes 400 to 5xx, causes and handling strategies for a robust Wakapp.

Version v1.04 min read

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:

FieldTypeDescription
statusCodenumberHTTP code
messagestring | string[]Human-readable description of the problem
errorstringStandard 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 (teamLevel must be NONE | 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:

  1. x-enriched-token header absent — check the server-side proxy
  2. wakastart_token cookie not set (the call to /api/auth/enrich failed)
  3. Enriched token expired — proceed with a refresh
  4. organization claim absent from the Keycloak JWT (Keycloak Organizations not configured)
  5. keycloak_id in the DB does not match the Keycloak sub

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 401 HTTP 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 a 403 Forbidden, not a 404. This is what the scope guards do (assertTeamScope, assertNetworkScope, assertPartnerScope, assertAppFeatureScope in public-api's config-api.service.ts): an explicit rejection via HttpStatus.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
  • PENDING invitation 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.

typescript
const 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 });

typescript
class 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