---
title: "Conventions générales"
description: "Authorization: Bearer <token> Le token est obtenu via POST /api/auth/login et a une durée de vie glissante de 30 jours (renouvelée à chaque requête authent…"
---

- **Base URL (dev)** : `http://hydrogen.test` (Laragon)
- **Préfixe API** : toutes les routes JSON sont sous `/api`
- **Format** : [JSON:API 1.1](https://jsonapi.org/format/1.1/)
  - `Content-Type` réponse : `application/vnd.api+json`
  - Erreurs : tableau `errors[]` avec `status`, `title`, `detail`, optionnel `source.pointer` et `meta`
- **Authentification** : Bearer token via header
  `Authorization: Bearer <token>`
  Le token est obtenu via `POST /api/auth/login` et a une durée de vie glissante de **30 jours** (renouvelée à chaque requête authentifiée).
- **Codes d'erreur communs** :
  - `400` — corps JSON invalide
  - `401` — token manquant / invalide / expiré
  - `403` — compte banni / action interdite
  - `404` — ressource ou route inconnue
  - `405` — méthode non autorisée
  - `422` — attribut manquant ou invalide
  - `500` — erreur serveur (détails uniquement si `APP_DEBUG=true`)
- **Attribut `level` (ressource `users`)** : entier dérivé de `experience` via l'inverse de la courbe quadratique `xp(N) = F · N · (N-1)` avec `F = USER_LEVEL_FACTOR` (25 par défaut). Formule : `level = floor((F + sqrt(F² + 4·F·experience)) / (2·F))`. Avec `F=25` : L1 ≥ 0 XP, L2 ≥ 50, L3 ≥ 150, L4 ≥ 300, L5 ≥ 500, L6 ≥ 750… Le client ne doit jamais le calculer lui-même : l'attribut est toujours présent dans la ressource `users`.
- **Pagination & objet `links` (JSON:API 1.1)** : toute réponse de type *collection* expose un objet `links` au niveau racine du document :
  ```json
  "links": {
    "self":  "https://api.example/api/users/me/media?limit=20",
    "first": "https://api.example/api/users/me/media?limit=20",
    "prev":  null,
    "next":  "https://api.example/api/users/me/media?cursor=MTcxMjM0…&limit=20"
  }
  ```
  - **Keyset (curseur opaque)** — utilisé par tous les listings ordonnés sur un timestamp + UUID (`media`, `notifications`, `followers`, `following`). Les paramètres sont :
    - `?cursor=<opaque>` : page **suivante** (rows strictement plus anciennes que le curseur).
    - `?before=<opaque>` : page **précédente** (rows strictement plus récentes que le curseur). Mutuellement exclusif avec `?cursor` ; `?before` gagne s'ils sont présents tous les deux.
    - `?limit=<int>` : taille de page (bornée par endpoint, défaut 20).
    - `links.last` n'est **pas** émis (le keyset ne sait pas calculer la dernière page sans full scan).
    - Un curseur malformé renvoie `400 Invalid cursor` (jamais de fallback silencieux).
  - **Offset** — utilisé par les listings backend Meilisearch (`/api/users/search`). Paramètres `?offset=<int>` + `?limit=<int>`. Les cinq liens `self/first/prev/next/last` sont émis (le total estimé permet de calculer `last`).
  - **Membres nuls** : les liens non disponibles (ex : `prev` sur la première page) restent présents avec la valeur `null` — c'est conforme JSON:API et permet au client de distinguer « pas de page précédente » de « endpoint sans pagination ».
  - **Collections non paginées** (`/api/auth/sessions`, `/api/users/me/oauth-identities`, `/api/i18n/locales`) : seul `links.self` est émis.
- **`meta.total` — compte total d'éléments d'une collection** : quand le total est calculable à coût borné (compteur dénormalisé sur `user_stats`, `COUNT(*)` indexé par `user_id`, etc.), il est exposé dans `meta.total` aux côtés de `meta.limit`. Sémantique :
  - **Exact** (entier, `meta.total`) : endpoints keyset adossés à MySQL (`/api/users/me/media`, `/api/users/{id}/media`, `/api/users/me/notifications`, `/api/users/{id}/followers|following` et leurs variantes `me`). Le compte reflète le **même filtre que la requête** (ex : `meta.total` de `/api/users/me/notifications?filter=unread` ne compte que les non-lues).
  - **Estimé** (entier, `meta.totalHits`) : endpoints offset adossés à Meilisearch (`/api/users/search`, `/api/media/nearby`, `/api/media/in-bounds`). Le moteur retourne une estimation rapide ; ne pas s'en servir pour des assertions strictes côté client.
  - **Absent** : sur les collections non paginées (`links.self` only) — le client peut utiliser `data.length`.
- **Format des dates (toutes les ressources JSON:API)** : tout champ date/datetime exposé par l'API publique est un **objet structuré**, jamais une chaîne ISO brute. Forme canonique :
  ```json
  "createdAt": {
    "iso":       "2026-06-14T08:42:13+00:00",
    "formatted": {
      "long":     "14 juin 2026 08:42",
      "short":    "14/06/2026 08:42",
      "dateOnly": "14 juin 2026",
      "time":     "08:42"
    },
    "relative":  "il y a 3 minutes"
  }
  ```
  - **Fuseau** : `iso` est toujours en UTC (`+00:00`). Les rendus `formatted.*` et `relative` sont localisés via la même locale que la ressource (en-tête `Accept-Language` du viewer, normalisée par `LocaleResolverMiddleware`).
  - **`relative`** : généré côté serveur via `Carbon::diffForHumans()`. Le client peut l'afficher tel quel ; aucun calcul à faire pour « il y a X jours ».
  - **`formatted.*`** : utilisent les tokens `LL`/`LLL`/`L LT`/`LT` de Carbon — la sortie suit la locale (FR : `14 juin 2026`, EN : `June 14, 2026`).
  - **Valeur nulle** : l'attribut est `null` (jamais un objet vide). Le client doit donc tester `if (createdAt !== null)` avant d'accéder à `iso`/`formatted`/`relative`.
  - **Champs concernés** : tous les `createdAt`, `updatedAt`, `confirmedAt`, `joinedAt`, `bannedUntil`, `profileCompletedAt`, `birthdate`, `shotAt`, `readAt`, `editedAt`, `deletedAt`, `linkedAt`, `earnedAt`, `expiresAt`, `lastUsedAt`, `cooldownUntil`, `followedAt`, `reactedAt`, etc.
  - **Hors-périmètre** : les **curseurs de pagination** restent des chaînes opaques (encodage interne) ; l'**API admin** (`/admin/*`) reste plate (string ISO simple, pour Talend/Postman).

---
