WakaStart
Using the API

Using the API — REST Conventions

Authenticating requests, pagination, filters, automatic multi-tenancy, and unified error format.

Version v1.05 min read

Using the API — REST Conventions

All post-auth requests go through the Public API (the public API). This chapter covers REST conventions, request authentication, pagination, and multi-tenancy.

Key concepts

  • Bearer + enriched: dual authentication required for frontend calls.
  • API Key: alternative for server-to-server backends.
  • WID: short identifier preferred in URLs and parameters.
  • Automatic filtering: the backend filters according to the token scope — you never filter manually.
  • Pagination: page-based (page + limit), not cursor-based.

Request authentication

Method 1 — Bearer JWT + enriched token (frontend)

A request to downstream services must carry two tokens:

http
Authorization: Bearer <keycloak_access_token> (RS256, émis par Keycloak) x-enriched-token: <wakastart_token> (géré par WakaStart, format opaque côté Wakapp)

Without x-enriched-token, the response is 401 Token enrichi manquant ou invalide.

Recommended implementation — Next.js server proxy:

typescript
// app/api/proxy/[...path]/route.ts import { cookies } from "next/headers"; import { NextRequest, NextResponse } from "next/server"; export async function GET( req: NextRequest, { params }: { params: { path: string[] } } ) { const cookieStore = await cookies(); const accessToken = cookieStore.get("keycloak_token")?.value; const enrichedToken = cookieStore.get("wakastart_token")?.value; if (!accessToken || !enrichedToken) { return NextResponse.json({ error: "Not authenticated" }, { status: 401 }); } const apiUrl = `${process.env.WAKASTART_API_URL}/api/${params.path.join("/")}`; const url = new URL(apiUrl); req.nextUrl.searchParams.forEach((value, key) => { url.searchParams.set(key, value); }); const response = await fetch(url.toString(), { method: req.method, headers: { "Authorization": `Bearer ${accessToken}`, "x-enriched-token": enrichedToken, "Content-Type": "application/json", }, }); const data = await response.json(); return NextResponse.json(data, { status: response.status }); }

From your React frontend:

typescript
// lib/api.ts — utilise le proxy qui gère l'auth async function apiGet<T>(path: string, params?: Record<string, string>): Promise<T> { const url = new URL(`/api/proxy/${path}`, window.location.origin); if (params) { Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v)); } const res = await fetch(url.toString()); if (!res.ok) throw new ApiError(res.status, await res.json()); return res.json(); } // Usage : const users = await apiGet<UserList>("config/users", { page: "1", limit: "20" });

Method 2 — API Key (server-to-server backend)

API Keys entirely replace the Bearer+enriched pair. They are scoped to a Customer.

typescript
const response = await fetch(`${WAKASTART_API_URL}/api/config/users`, { headers: { "x-api-key": "sk_live_...", "Content-Type": "application/json", }, });

API Keys are secrets — never expose them on the frontend, never commit them.


REST conventions

ConventionDetail
HTTP verbsGET (read), POST (create), PATCH (partial update), PUT (replace), DELETE (delete)
Date formatISO 8601 with timezone: 2026-05-19T10:30:00.000Z
Identifiers in URLsPrefer WIDs (/config/users/WKST05) over UUIDs
Content-Typeapplication/json on all POST/PATCH/PUT
EncodingUTF-8 required
NullabilityAbsent fields are not returned (no explicit null unless documented)

Unified error format

json
{ "statusCode": 403, "message": "Niveau d'administration insuffisant. Requis: CustomerAdmin, Actuel: User", "error": "Forbidden" }

For validation errors (400):

json
{ "statusCode": 400, "message": [ "email must be an email", "firstName should not be empty" ], "error": "Bad Request" }

Pagination

All list endpoints support page-based pagination:

http
GET /api/config/users?page=1&limit=20
json
{ "data": [ { "id": "uuid", "wid": "WKST05", "email": "john@acme.com" } ], "meta": { "total": 142, "page": 1, "limit": 20, "totalPages": 8 } }

Parameters:

ParamDefaultMaxDescription
page1—Page number (1-indexed)
limit20100Number of items per page

Pattern with TanStack Query:

typescript
import { useQuery } from "@tanstack/react-query"; function useUsers(page: number) { return useQuery({ queryKey: ["users", page], queryFn: () => apiGet<UserList>("config/users", { page: String(page), limit: "20" }), placeholderData: (prev) => prev, }); }

Common filters

http
GET /api/config/users?search=john&isActive=true&page=1&limit=20 GET /api/config/customers?networkId=NET001&page=1 GET /api/invitations/sent?status=PENDING&page=1&limit=10

Automatic multi-tenancy

Data is automatically filtered according to the token scope:

adminLevelScope of returned data
WakaAdminThe entire platform
OwnerAdminTheir entire Partner
NetworkAdminTheir Network + all its Customers
CustomerAdminTheir Customer only
UserTheir own profile

You do not need to filter manually. If a CustomerAdmin calls GET /api/config/users, they only see the users of their Customer.


Runtime config

To load your app's theme, languages, and branding in a single call:

http
GET /api/apps/{appWid}/runtime-config Authorization: Bearer <token> x-enriched-token: <wakaToken>
json
{ "theme": { "id": "uuid", "name": "Default", "cssUrl": "https://storage.wakastart.app/themes/default.css" }, "languages": [ { "code": "fr", "name": "Français", "isDefault": true, "i18nUrl": "https://storage.wakastart.app/i18n/fr.json" } ], "branding": { "logoLightUrl": "https://storage.wakastart.app/logos/acme-light.png", "logoDarkUrl": "https://storage.wakastart.app/logos/acme-dark.png", "faviconUrl": "https://storage.wakastart.app/favicons/acme.ico" } }

Best practices

  • Server proxy: never let the browser handle cookies or tokens directly.
  • Handling 401s: attempt a token refresh, then redirect to login. Never an infinite loop.
  • Prefer WIDs: in your routes and parameters, WIDs (WKST05) are more readable than UUIDs.
  • Limit max 100: never exceed limit=100 per page.
  • TanStack Query: use staleTime to avoid unnecessary re-fetches (5-30s depending on criticality).

Common pitfalls

  • Missing x-enriched-token header: all frontend requests return 401.
  • Unnecessary manual filtering: manually adding customerId=xxx when the backend already filters.
  • limit=1000: guaranteed timeout on large tenants. Paginate properly.
  • CORS on direct calls: if your frontend calls the API directly without a proxy, you will get CORS errors.

Go further