Exact wire contract of the StudyLyon API, derived from source
(ResponseEnvelopeInterceptor, HttpExceptionFilter, PaginationQueryDto,
rate-limit guards, WsModule). Module docs 12_API_Mapping.md map screens to
endpoints using only these shapes .
Base URL: https://api.<domain>/api/v1 (version prefix v1; see version.constants.ts).
Content-Type: application/json; multipart for file uploads.
Auth: Authorization: Bearer <accessToken>; refresh via POST /auth/refresh.
Request ID: server echoes x-request-id; client sends its own UUID when absent.
API keys (X-API-Key) for M2M — not used by the client app.
{
"success": true,
"message": "OK",
"data": { },
"meta": { },
"timestamp": "2026-08-02T10:00:00.000Z",
"requestId": "..."
}
Paginated endpoints return data = array, meta = { page, limit, totalItems, totalPages, hasNext, hasPrevious }.
Non-paginated endpoints omit meta.
{
"success": false,
"message": "Validation failed.",
"error": { "code": "VALIDATION_ERROR", "details": [{ "field": "email", "message": "..." }] },
"timestamp": "...",
"requestId": "..."
}
HTTP Code Meaning
400 VALIDATION_ERRORInvalid input (details = per-field)
401 UNAUTHENTICATEDMissing/expired/invalid token
403 PERMISSION_DENIEDAuthed but not allowed
404 RESOURCE_NOT_FOUNDMissing resource (also for cross-tenant IDs — do not leak existence)
409 DUPLICATE_RESOURCEUnique constraint hit
422 BUSINESS_RULE_VIOLATIONBusiness rule refused
429 RATE_LIMITEDRate limit exceeded
5xx INTERNAL_SERVER_ERRORGeneric; never expose internals
Tier Limit Notes
auth 10/min login, register, reset — client shows countdown, no auto-retry
api 100/min normal app usage; client backoff on 429
admin 500/min admin endpoints
Query: page (1-based), limit (1–100, default 20), sort (field or -field), q
(global search term where the controller supports it).
Filters are controller-specific query params (documented per module).
Every request carries JWT with tenantId claim; server derives tenant from token —
client never sends tenantId in the body .
Guard: Public() decorator exempts auth endpoints only.
Cross-tenant access → 403/404; client treats as permission/not-found.
Server: Redis cache sl:{tenantId}:{key}, TTL per module; dashboard KPIs cached.
Client: last-good cache + stale-while-revalidate (06_State_Management.md §3.3).
No Cache-Control guarantees from API — client caching is advisory.
Gateway: WS upgrade with Authorization: Bearer <token> (or first-message auth).
Channels: per-user room user:{userId}; topics notification.new, message.new,
announcement.published, attendance.changed, invoice.updated, results.published.
Message shape: { type: topic, tenantId, data, timestamp }.
Disconnect → buffered in-app notifications delivered via REST on next fetch.
Safe mutations: PATCH read, status toggles → optimistic + rollback.
Write-once ops (payment, publish, submit) → show server result, no local write.
Client sends Idempotency-Key header (UUID) on critical POSTs where the module API
supports dedup; retry-safe by design otherwise.
Reads: last-good cache, offline banner, Retry on every failure.
Writes: only module-defined queues (attendance bulk, homework draft) with local
persistence + flush on reconnect + idempotency keys.
Files: uploads resume-capable (chunked) only where backend supports it — otherwise
queue with retry + DLQ visibility.
Tokens in secure storage (Keychain/Keystore), never logs.
Biometric gate for sensitive screens (payments, admin settings) — forward-looking
(backend 2FA exists; client-side gate is app-level).
TLS only; pinning per org policy.
All error UI derived from server codes — never render raw server messages that may
contain internals (use i18n keys, fall back to message for 4xx business text only).
v1 current; breaking changes → new version prefix; client negotiates via
Accept: application/vnd.studylyon.v1+json if backend adds it (flag: not in code yet).