Using the API — REST Conventions
Authenticating requests, pagination, filters, automatic multi-tenancy, and unified error format.
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:
httpAuthorization: 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.
typescriptconst 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
| Convention | Detail |
|---|---|
| HTTP verbs | GET (read), POST (create), PATCH (partial update), PUT (replace), DELETE (delete) |
| Date format | ISO 8601 with timezone: 2026-05-19T10:30:00.000Z |
| Identifiers in URLs | Prefer WIDs (/config/users/WKST05) over UUIDs |
| Content-Type | application/json on all POST/PATCH/PUT |
| Encoding | UTF-8 required |
| Nullability | Absent 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:
httpGET /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:
| Param | Default | Max | Description |
|---|---|---|---|
page | 1 | — | Page number (1-indexed) |
limit | 20 | 100 | Number of items per page |
Pattern with TanStack Query:
typescriptimport { 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
httpGET /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:
adminLevel | Scope of returned data |
|---|---|
WakaAdmin | The entire platform |
OwnerAdmin | Their entire Partner |
NetworkAdmin | Their Network + all its Customers |
CustomerAdmin | Their Customer only |
User | Their 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:
httpGET /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=100per page. - TanStack Query: use
staleTimeto avoid unnecessary re-fetches (5-30s depending on criticality).
Common pitfalls
- Missing
x-enriched-tokenheader: all frontend requests return401. - Unnecessary manual filtering: manually adding
customerId=xxxwhen 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
- Headers & Rate limits: required headers, idempotence, CORS, throttling
- Endpoint reference: full table of all routes
- HTTP errors: detailed HTTP codes and messages </content>