Guide
API publique de la plateforme : lectures REST anonymes (GET /api/public/v1/*), une écriture REST authentifiée (POST /api/public/v1/matches/{id}/result) et un endpoint GraphQL (POST /api/graphql, décrit plus bas : il n'a pas d'entrée dans la liste des endpoints). Les surfaces bot, admin et cron sont internes et absentes d'ici. Les routes free-players, team-openings, newsletter et scrim-requests servent les formulaires du site (captcha) : elles ne sont pas destinées à une intégration.
Quel espace répond
La plateforme sert plusieurs espaces (organisations). Une réponse qui vient du mauvais espace n'échoue pas : elle est valide, et fausse.
- Lectures REST (
GET /api/public/v1/*) : ajouter ?tenant=<slug> pour viser un espace, par exemple /api/public/v1/tournaments?tenant=cup-estivale. SANS ce paramètre, sur https://owwomenscup.fr, c'est l'espace Women's Cup qui répond, jeton ou pas : ces lectures IGNORENT le jeton. Un slug inconnu ou un espace désactivé retombent aussi sur Women's Cup, sans erreur. (Un espace doté d'un domaine propre est aussi reconnu quand l'API est appelée sur ce domaine.) - Écriture REST et GraphQL avec jeton : l'espace est celui du jeton, fixé à son émission. Aucun paramètre ni en-tête ne peut le déplacer ;
?tenant= n'y a aucun effet. - GraphQL sans jeton, ou avec un jeton refusé : espace Women's Cup, sans erreur.
Vérification simple : comparer un nom d'équipe ou de tournoi renvoyé avec le back-office de l'espace visé.
Cache
Les lectures REST posent Cache-Control: public, s-maxage=N (N indiqué sur chaque endpoint : 30 à 3600 s). Le cache du CDN est tenu par URL complète, paramètres de query compris (?tenant=, filtres, pagination, format) : deux URL qui diffèrent par un paramètre ont chacune leur réponse. Une donnée modifiée peut donc mettre jusqu'à N secondes à apparaître sur une URL déjà servie.
POST /api/graphql et l'écriture REST ne posent aucun en-tête de cache public (no-store pour l'écriture).
Authentification et clés
- En-tête :
Authorization: Bearer pk_live_<64 hex>. Détail dans le schéma de sécurité PublicApiToken. - Une clé est émise par le staff et rattachée à UN espace. Depuis l'écran
/admin/api-tokens, c'est l'espace ACTIF du sélecteur du back-office qui la reçoit : le vérifier avant d'émettre. L'owner de la plateforme peut aussi émettre pour un espace nommé. - Affichée une seule fois. Révocable. Expiration optionnelle : une clé révoquée ou expirée répond 401
UNAUTHORIZED en REST, comme une clé inconnue — et passe en anonyme sur GraphQL. - Portées
resource:action, sans implication entre elles. Aujourd'hui, seule matches:write est exigée ; aucune route ne contrôle une portée :read. - Plan de l'espace : l'écriture exige la capacité
apiWrite (plans Circuit, Éditeur, Fondation) ; sinon 403 plan_required — valeur du champ error, pas de code. Une clé partenaire (comp) n'est jamais soumise au plan ni au quota. Les lectures REST et les requêtes GraphQL ne contrôlent ni plan, ni portée, ni quota.
Limites de débit
- Lectures REST : 120 requêtes par minute par IP, compteur distinct par endpoint. 429 avec
Retry-After: 60 ; selon le limiteur qui répond, le corps a ou non code: RATE_LIMITED. - Écriture REST : 30 par minute par IP, puis 15 par minute par jeton (
ACTOR_RATE_LIMIT), puis le quota du plan : Circuit 120 par minute et 500 000 par mois, Éditeur et Fondation illimités (RATE_LIMITED ou QUOTA_EXCEEDED, avec X-RateLimit-Scope et X-RateLimit-Limit). Le quota est aussi consommé par la mutation GraphQL. Clé partenaire : pas de quota. - GraphQL : 600 requêtes/min par IP (requêtes et mutation) ; au-delà, HTTP 429 avec
Retry-After et errors[].extensions.code = RATE_LIMITED.
Erreurs
REST : { "error": "…", "code": "…" }. Tester code, jamais error (texte libre). Liste complète et sens : schéma PublicApiErrorCode. Deux corps sans code : le 429 du limiteur par IP en mémoire et le 403 plan_required (schéma PublicPlanDenial). fields détaille les erreurs de validation (INVALID_BODY, INVALID_QUERY).
Idempotence
L'écriture REST honore Idempotency-Key (200 caractères au plus) : une réponse 2xx est rejouée pendant 5 minutes pour la même clé ET le même corps, avec Idempotency-Replay: true. La mutation GraphQL n'a pas d'idempotence.
GraphQL — POST /api/graphql
- Corps JSON
{ "query": "…", "variables": { … } }. Utiliser POST. - Espace : celui du jeton
Authorization: Bearer pk_live_…. Sans jeton, ou si le jeton est inconnu, révoqué, expiré ou mal formé : espace Women's Cup, SANS erreur. Vérifier le résultat : par exemple { tournaments { count items { slug name } } } doit lister les tournois de votre espace. - Requêtes :
tournaments(status, game, limit, offset) (liste paginée, limit ramené dans [1, 100], count = total), tournament(idOrSlug), match(id), team(idOrSlug). Champs en snake_case, mêmes projections et mêmes règles de visibilité que le REST (tournois published / running / completed ; matchs pending / ongoing / finished). Les requêtes ne contrôlent ni plan, ni portée, ni quota. - Mutation :
reportMatchResult(matchId, team1Score, team2Score), portée matches:write, mêmes effets que l'écriture REST. Pas de mode maintenance, pas d'idempotence. - Codes (
errors[].extensions.code) : UNAUTHENTICATED (pas de jeton valide), FORBIDDEN (plan insuffisant — reason: plan_required et requiredCapability — ou portée manquante), RATE_LIMITED et QUOTA_EXCEEDED (avec retryAfterSec et limit), BAD_USER_INPUT (score hors [0, 99], match bye ou sans ses deux équipes), NOT_FOUND, CONFLICT (match déjà clôturé), INTERNAL_SERVER_ERROR. Toute autre erreur serveur est masquée derrière un message générique. - Profondeur de requête : 8 au plus. Introspection et GraphiQL désactivés en production : le schéma est celui décrit ici.
Webhooks sortants
Un espace abonne une URL depuis /admin/webhooks et reçoit ses événements en POST signé. Catalogue, signature et règles de livraison : GET /api/public/webhook-events. match.forfeit figure au catalogue mais n'est émis par aucun code aujourd'hui.
Compatibilité et versionnement
- Aucune politique de versionnement n'est formalisée pour
/api/public/v1/*. Constaté jusqu'ici : des champs AJOUTÉS aux réponses, et un champ RETIRÉ dans /v1 même (tenant_id des ligues, 2026-09-15). - Les schémas de réponse publiés sont fermés (
additionalProperties: false) : ils décrivent la réponse du jour. Un client généré ne doit pas rejeter un champ inconnu, sans quoi le moindre ajout le casse. - Les changements de contrat sont datés dans
docs/PUBLIC_API_CONTRACT.md (section « Changements de contrat »).
Endpoints
get/api/public/free-playersSans auth
Liste anonymisee des joueuses cherchant une equipe.
Vitrine publique du marche des joueuses libres (lot 1 acquisition). Ne renvoie AUCUN moyen de contact — c'est une preuve qu'il y a du monde, pas un carnet d'adresses. Les annonces perimees (60 j) sont exclues.
Réponses
| Statut | Schéma | Description |
|---|
| 200 | object | |
| 429 | Error | Rate-limit dépassé (par-actor ou par-route). |
post/api/public/free-playersSans auth
Se signaler comme joueuse libre — SANS COMPTE. Anti-bot captcha + honeypot.
Cree (ou rafraichit) une fiche source='web' dans free_players, sans exiger de compte : c'est tout l'objet du lot 1 — une joueuse sans equipe n'avait aucun chemin sur le site. Une re-soumission avec le meme email met a jour la fiche et repousse sa peremption. Reponse toujours generique { success: true } (enumeration-safe). Un honeypot rempli est silencieusement absorbe (200). Emet l'event bot free_player.registered a la premiere inscription.
Réponses
| Statut | Schéma | Description |
|---|
| 200 | object | Toujours renvoye tant que la requete est bien formee (succes generique). |
| 400 | Error | Requête invalide (validation zod, JSON mal formé, etc.). |
| 429 | Error | Rate-limit dépassé (par-actor ou par-route). |
get/api/public/free-players/removeSans auth
Décrit la fiche visée par un lien de retrait (ne supprime rien).
Sert la page de confirmation /rejoindre/retrait : elle doit montrer CE qu'on s'apprête à retirer. La separation GET/POST n'est pas cosmetique — les clients mail et antivirus pre-visitent les liens d'un email, un GET destructeur ferait disparaitre des fiches sans clic humain. Token HMAC recu par email a l'inscription (aucun compte requis).
Paramètres
| Paramètre | Emplacement | Type | Description |
|---|
tokenrequis | requête | string | |
Réponses
| Statut | Schéma | Description |
|---|
| 200 | object | |
| 400 | Error | Requête invalide (validation zod, JSON mal formé, etc.). |
| 404 | Error | Ressource introuvable (UUID inconnu, route inexistante). |
| 429 | Error | Rate-limit dépassé (par-actor ou par-route). |
post/api/public/free-players/removeSans auth
Retire définitivement une fiche « joueuse libre ».
Porte de sortie autonome du parcours /rejoindre : l'inscription se fait SANS compte, donc sans cette route une joueuse devrait ecrire au staff pour disparaitre d'une liste publique qu'elle a elle-meme alimentee. Token invalide et fiche introuvable renvoient le MEME message : les distinguer transformerait la route en oracle d'existence.
Paramètres
| Paramètre | Emplacement | Type | Description |
|---|
token | requête | string | Accepte aussi dans le corps JSON : { "token": "..." }. |
Réponses
| Statut | Schéma | Description |
|---|
| 200 | object | |
| 400 | Error | Requête invalide (validation zod, JSON mal formé, etc.). |
| 404 | Error | Ressource introuvable (UUID inconnu, route inexistante). |
| 429 | Error | Rate-limit dépassé (par-actor ou par-route). |
get/api/public/newsletter/confirmSans auth
Confirme une inscription newsletter (double opt-in) via le lien email.
Cible du lien de confirmation. Passe l'abonne a confirmed puis redirige (302) vers /newsletter/merci. Token manquant / invalide / introuvable → redirige vers /newsletter/merci?status=invalid. Pas de captcha (clic sur un lien).
Paramètres
| Paramètre | Emplacement | Type | Description |
|---|
tokenrequis | requête | string | Le confirm_token recu par email. |
Réponses
| Statut | Schéma | Description |
|---|
| 302 | — | Redirection vers la page de remerciement (ou ?status=invalid). |
post/api/public/newsletter/subscribeSans auth
Inscription newsletter (double opt-in) — anti-bot captcha + honeypot.
Enregistre (ou re-arme) un abonne pending avec un confirm_token aleatoire et envoie l'email de confirmation. Reponse toujours generique { success: true } (enumeration-safe) : ne revele jamais si l'adresse existait deja. Un honeypot rempli est silencieusement absorbe (200).
Réponses
| Statut | Schéma | Description |
|---|
| 200 | object | Toujours renvoye tant que la requete est bien formee (succes generique). |
| 400 | Error | Requête invalide (validation zod, JSON mal formé, etc.). |
| 429 | Error | Rate-limit dépassé (par-actor ou par-route). |
get/api/public/openapiSans auth
Spécification OpenAPI publique (machine-readable).
Spec OpenAPI 3.1 filtrée à la surface publique (/api/public/*), dérivée de la spec complète (fragments docs/openapi/). À importer dans Postman ou un générateur de code. Version rendue lisible : /developpeurs/reference.
Anonyme, CORS *. Limite : 120 requêtes par minute par IP (le 429 n'a pas de code). Cache : public, max-age=300, s-maxage=3600.
Paramètres
| Paramètre | Emplacement | Type | Description |
|---|
format | requête | enum(json | yaml) | yaml pour la variante YAML (text/yaml), toute autre valeur donne du JSON.
|
Réponses
| Statut | Schéma | Description |
|---|
| 200 | object | Document OpenAPI (JSON par défaut, YAML si demandé). |
| 405 | PublicApiError | Méthode autre que GET / OPTIONS — METHOD_NOT_ALLOWED. En-têtes AllowMéthodes acceptées par la route.
Exemple{
"error": "Method not allowed",
"code": "METHOD_NOT_ALLOWED"
} |
| 429 | PublicApiError | Limite de débit par IP dépassée. Deux limiteurs se suivent, avec la même limite (120 requêtes par minute, compteur distinct par endpoint) : un limiteur en mémoire par instance, dont le corps n'a PAS de code, puis un compteur durable partagé (RATE_LIMITED, laisse passer si le compteur est indisponible). Respecter Retry-After. En-têtes Retry-AfterSecondes à attendre avant de réessayer.
Exemple — Limiteur en mémoire (sans code){
"error": "Trop de requêtes. Réessayez plus tard."
}Exemple — Compteur durable{
"error": "Trop de requêtes. Réessayez plus tard.",
"code": "RATE_LIMITED"
} |
| 500 | PublicApiError | Erreur serveur — INTERNAL. Réessayer plus tard. Exemple{
"error": "Internal server error",
"code": "INTERNAL"
} |
post/api/public/scrim-requestsSans auth
Propose un scrim public a une de nos equipes (anti-bot captcha + honeypot).
Réponses
| Statut | Schéma | Description |
|---|
| 201 | object | Demande de scrim enregistree. |
| 400 | Error | Requête invalide (validation zod, JSON mal formé, etc.). |
| 404 | Error | Equipe cible introuvable. |
| 429 | Error | Rate-limit dépassé (par-actor ou par-route). |
get/api/public/team-openingsSans auth
Liste anonymisee des equipes qui cherchent une joueuse.
Miroir de /api/public/free-players : la vitrine des equipes qui recrutent. Ne renvoie AUCUN moyen de contact — repondre a une annonce passe par /api/team-openings/contact, derriere un compte. Les annonces perimees (60 j) sont exclues. Cache CDN 60 s.
Paramètres
| Paramètre | Emplacement | Type | Description |
|---|
tenant | requête | string | Slug de l'espace à lire (ex. pogtv). Absent, inconnu ou espace désactivé : sur owwomenscup.fr, c'est l'espace Women's Cup qui répond, SANS erreur. Le jeton Authorization ne le remplace pas : ces lectures l'ignorent. Appelée sur le domaine propre d'un espace, l'API sert cet espace (le domaine prime sur ce paramètre). |
Réponses
| Statut | Schéma | Description |
|---|
| 200 | object | |
| 429 | Error | Rate-limit dépassé (par-actor ou par-route). |
post/api/public/team-openingsSans auth
Publier une annonce de recrutement — SANS COMPTE. Anti-bot captcha + honeypot.
Cree (ou rafraichit) une annonce source='web' dans team_openings, sans exiger de compte : une equipe qui se monte n'a souvent pas encore d'existence sur le site. Une re-soumission avec le meme email met a jour l'annonce et repousse sa peremption. Reponse toujours generique { success: true } (enumeration-safe). Un honeypot rempli est silencieusement absorbe (200). Emet l'event bot team_opening.published a la premiere publication.
Réponses
| Statut | Schéma | Description |
|---|
| 200 | object | Toujours renvoye tant que la requete est bien formee (succes generique). |
| 400 | Error | Requête invalide (validation zod, JSON mal formé, etc.). |
| 429 | Error | Rate-limit dépassé (par-actor ou par-route). |
get/api/public/team-openings/removeSans auth
Decrit l'annonce visee par un lien de retrait (ne supprime rien).
Sert la page de confirmation /recrutement/retrait : elle doit montrer CE qu'on s'apprete a retirer. La separation GET/POST n'est pas cosmetique — les clients mail et antivirus pre-visitent les liens d'un email, un GET destructeur ferait disparaitre des annonces sans clic humain. Token HMAC recu par email a la publication (aucun compte requis).
Paramètres
| Paramètre | Emplacement | Type | Description |
|---|
tokenrequis | requête | string | |
Réponses
| Statut | Schéma | Description |
|---|
| 200 | object | |
| 400 | Error | Requête invalide (validation zod, JSON mal formé, etc.). |
| 404 | Error | Ressource introuvable (UUID inconnu, route inexistante). |
| 429 | Error | Rate-limit dépassé (par-actor ou par-route). |
post/api/public/team-openings/removeSans auth
Retire definitivement une annonce de recrutement.
Porte de sortie autonome du parcours : la publication se fait SANS compte, donc sans cette route une equipe au complet devrait ecrire au staff pour disparaitre — et en attendant, chaque joueuse qui repond perd son temps. Token invalide et annonce introuvable renvoient le MEME message : les distinguer transformerait la route en oracle d'existence.
Paramètres
| Paramètre | Emplacement | Type | Description |
|---|
token | requête | string | Accepte aussi dans le corps JSON : { "token": "..." }. |
Réponses
| Statut | Schéma | Description |
|---|
| 200 | object | |
| 400 | Error | Requête invalide (validation zod, JSON mal formé, etc.). |
| 404 | Error | Ressource introuvable (UUID inconnu, route inexistante). |
| 429 | Error | Rate-limit dépassé (par-actor ou par-route). |
get/api/public/v1/leaderboardSans auth
Classement Glicko-2 public des joueuses.
Joueuses de l'espace servi ayant joué au moins un match classé, triées par rating décroissant (départage stable par identifiant). rank est la position absolue (offset + position dans la page). ATTENTION : pagination.count est le nombre d'éléments de CETTE page, pas le total.
Espace servi : voir « Quel espace répond » dans l'introduction. Le jeton Authorization est ignoré ici.
Anonyme, CORS *. Limite : 120 requêtes par minute par IP pour cet endpoint. Cache : 60 s.
Paramètres
| Paramètre | Emplacement | Type | Description |
|---|
tenant | requête | string | Slug de l'espace à lire (ex. pogtv). Absent, inconnu ou espace désactivé : sur owwomenscup.fr, c'est l'espace Women's Cup qui répond, SANS erreur. Le jeton Authorization ne le remplace pas : ces lectures l'ignorent. Appelée sur le domaine propre d'un espace, l'API sert cet espace (le domaine prime sur ce paramètre). |
limit | requête | integer | Ramené dans [1, 100] ; une valeur non numérique vaut 50. |
offset | requête | integer | |
Réponses
| Statut | Schéma | Description |
|---|
| 200 | object | Page du classement + pagination. En-têtes Cache-ControlPosé par la route sur toute réponse 200 : public, s-maxage=<N>, stale-while-revalidate=<N/2>. La valeur de N dépend de l'endpoint (indiquée dans sa description). Le CDN met en cache par URL complète, paramètres de query compris : une donnée modifiée peut mettre jusqu'à N secondes à apparaître sur une URL déjà servie.
Exemple{
"data": [
{
"userId": "8d7c6b5a-4938-4271-b6a5-948372615049",
"displayName": "Nova",
"battleTag": "Nova#21456",
"avatarUrl": null,
"teamName": "Les Aurores",
"teamSlug": "les-aurores",
"teamLogoUrl": "https://example.org/logos/aurores.png",
"rating": 1623.4,
"rd": 71.2,
"gamesPlayed": 12,
"wins": 8,
"losses": 4,
"rank": 1
}
],
"pagination": {
"limit": 50,
"offset": 0,
"count": 1
}
} |
| 405 | PublicApiError | Méthode autre que GET / OPTIONS — METHOD_NOT_ALLOWED. En-têtes AllowMéthodes acceptées par la route.
Exemple{
"error": "Method not allowed",
"code": "METHOD_NOT_ALLOWED"
} |
| 429 | PublicApiError | Limite de débit par IP dépassée. Deux limiteurs se suivent, avec la même limite (120 requêtes par minute, compteur distinct par endpoint) : un limiteur en mémoire par instance, dont le corps n'a PAS de code, puis un compteur durable partagé (RATE_LIMITED, laisse passer si le compteur est indisponible). Respecter Retry-After. En-têtes Retry-AfterSecondes à attendre avant de réessayer.
Exemple — Limiteur en mémoire (sans code){
"error": "Trop de requêtes. Réessayez plus tard."
}Exemple — Compteur durable{
"error": "Trop de requêtes. Réessayez plus tard.",
"code": "RATE_LIMITED"
} |
| 500 | PublicApiError | Erreur serveur — INTERNAL. Réessayer plus tard. Exemple{
"error": "Internal server error",
"code": "INTERNAL"
} |
get/api/public/v1/leagues/{slug}Sans auth
Détail public d'une league (standings + tournois liés).
Ligue publique de l'espace servi avec son classement, ses tournois et ses scrims comptés. 404 si la ligue est inconnue, privée ou en brouillon.
Espace servi : voir « Quel espace répond » dans l'introduction. Le jeton Authorization est ignoré ici.
Anonyme, CORS *. Limite : 120 requêtes par minute par IP pour cet endpoint. Cache : 120 s.
Paramètres
| Paramètre | Emplacement | Type | Description |
|---|
slugrequis | chemin | string | Slug de la ligue (pas d'identifiant UUID). |
tenant | requête | string | Slug de l'espace à lire (ex. pogtv). Absent, inconnu ou espace désactivé : sur owwomenscup.fr, c'est l'espace Women's Cup qui répond, SANS erreur. Le jeton Authorization ne le remplace pas : ces lectures l'ignorent. Appelée sur le domaine propre d'un espace, l'API sert cet espace (le domaine prime sur ce paramètre). |
Réponses
| Statut | Schéma | Description |
|---|
| 200 | object | En-têtes Cache-ControlPosé par la route sur toute réponse 200 : public, s-maxage=<N>, stale-while-revalidate=<N/2>. La valeur de N dépend de l'endpoint (indiquée dans sa description). Le CDN met en cache par URL complète, paramètres de query compris : une donnée modifiée peut mettre jusqu'à N secondes à apparaître sur une URL déjà servie.
Exemple{
"data": {
"league": {
"id": "4b3a2918-0f7e-4d6c-95b4-a39281706f5e",
"name": "Saison 2026",
"slug": "saison-2026",
"description": null,
"game": "overwatch",
"status": "active",
"start_date": "2026-01-10",
"end_date": "2026-12-20",
"points_table": {
"1": 100,
"2": 70,
"3": 50
},
"is_public": true,
"created_at": "2025-12-01T10:00:00+00:00",
"updated_at": "2026-09-01T08:30:00+00:00"
},
"standings": [
{
"teamId": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
"teamName": "Les Aurores",
"teamSlug": "les-aurores",
"logoUrl": "https://example.org/logos/aurores.png",
"points": 170,
"tournamentsCounted": 2,
"scrimsCounted": 1,
"bestRank": 1,
"rank": 1
}
],
"tournaments": [
{
"id": "3f2c8a1e-6b4d-4c7a-9e21-5d8f0b7a1c34",
"name": "Coupe d'automne 2026",
"slug": "coupe-automne-2026",
"weight": 1
}
],
"scrims": [
{
"id": "7f6e5d4c-3b2a-4190-8f7e-6d5c4b3a2190",
"name": "Aurores vs Nébuleuse",
"slug": null,
"weight": 0.5,
"team1Name": "Les Aurores",
"team2Name": "Nébuleuse",
"team1Score": 3,
"team2Score": 1,
"scheduledDate": "2026-09-12T19:00:00+00:00"
}
]
}
} |
| 404 | PublicApiError | Ressource inconnue dans l'espace servi, ou non publique — NOT_FOUND. Exemple{
"error": "Tournament not found",
"code": "NOT_FOUND"
} |
| 405 | PublicApiError | Méthode autre que GET / OPTIONS — METHOD_NOT_ALLOWED. En-têtes AllowMéthodes acceptées par la route.
Exemple{
"error": "Method not allowed",
"code": "METHOD_NOT_ALLOWED"
} |
| 429 | PublicApiError | Limite de débit par IP dépassée. Deux limiteurs se suivent, avec la même limite (120 requêtes par minute, compteur distinct par endpoint) : un limiteur en mémoire par instance, dont le corps n'a PAS de code, puis un compteur durable partagé (RATE_LIMITED, laisse passer si le compteur est indisponible). Respecter Retry-After. En-têtes Retry-AfterSecondes à attendre avant de réessayer.
Exemple — Limiteur en mémoire (sans code){
"error": "Trop de requêtes. Réessayez plus tard."
}Exemple — Compteur durable{
"error": "Trop de requêtes. Réessayez plus tard.",
"code": "RATE_LIMITED"
} |
| 500 | PublicApiError | Erreur serveur — INTERNAL. Réessayer plus tard. Exemple{
"error": "Internal server error",
"code": "INTERNAL"
} |
get/api/public/v1/leaguesSans auth
Liste des leagues publiques.
Ligues de l'espace servi marquées publiques et hors brouillon, triées par date de création décroissante. Pas de pagination.
Espace servi : voir « Quel espace répond » dans l'introduction. Le jeton Authorization est ignoré ici.
Anonyme, CORS *. Limite : 120 requêtes par minute par IP pour cet endpoint. Cache : 120 s.
Paramètres
| Paramètre | Emplacement | Type | Description |
|---|
tenant | requête | string | Slug de l'espace à lire (ex. pogtv). Absent, inconnu ou espace désactivé : sur owwomenscup.fr, c'est l'espace Women's Cup qui répond, SANS erreur. Le jeton Authorization ne le remplace pas : ces lectures l'ignorent. Appelée sur le domaine propre d'un espace, l'API sert cet espace (le domaine prime sur ce paramètre). |
Réponses
| Statut | Schéma | Description |
|---|
| 200 | object | En-têtes Cache-ControlPosé par la route sur toute réponse 200 : public, s-maxage=<N>, stale-while-revalidate=<N/2>. La valeur de N dépend de l'endpoint (indiquée dans sa description). Le CDN met en cache par URL complète, paramètres de query compris : une donnée modifiée peut mettre jusqu'à N secondes à apparaître sur une URL déjà servie.
Exemple{
"data": [
{
"id": "4b3a2918-0f7e-4d6c-95b4-a39281706f5e",
"name": "Saison 2026",
"slug": "saison-2026",
"description": null,
"game": "overwatch",
"status": "active",
"start_date": "2026-01-10",
"end_date": "2026-12-20",
"points_table": {
"1": 100,
"2": 70,
"3": 50
},
"is_public": true,
"created_at": "2025-12-01T10:00:00+00:00",
"updated_at": "2026-09-01T08:30:00+00:00"
}
]
} |
| 405 | PublicApiError | Méthode autre que GET / OPTIONS — METHOD_NOT_ALLOWED. En-têtes AllowMéthodes acceptées par la route.
Exemple{
"error": "Method not allowed",
"code": "METHOD_NOT_ALLOWED"
} |
| 429 | PublicApiError | Limite de débit par IP dépassée. Deux limiteurs se suivent, avec la même limite (120 requêtes par minute, compteur distinct par endpoint) : un limiteur en mémoire par instance, dont le corps n'a PAS de code, puis un compteur durable partagé (RATE_LIMITED, laisse passer si le compteur est indisponible). Respecter Retry-After. En-têtes Retry-AfterSecondes à attendre avant de réessayer.
Exemple — Limiteur en mémoire (sans code){
"error": "Trop de requêtes. Réessayez plus tard."
}Exemple — Compteur durable{
"error": "Trop de requêtes. Réessayez plus tard.",
"code": "RATE_LIMITED"
} |
| 500 | PublicApiError | Erreur serveur — INTERNAL. Réessayer plus tard. Exemple{
"error": "Internal server error",
"code": "INTERNAL"
} |
post/api/public/v1/matches/{id}/resultToken requis
Pose le score final d'un match (écriture publique authentifiée).
Écriture AUTORITAIRE : pas de consensus des capitaines. Le match passe en finished, le vainqueur est propagé dans le bracket et les notifications partent, comme une saisie du back-office.
ESPACE. Celui du jeton : un match d'un autre espace répond 404, comme un match inconnu. Aucun paramètre ni en-tête ne change l'espace.
CONTRÔLES, dans l'ordre (le premier qui échoue répond) : méthode (405) → limite par IP, 30/min (429 sans code) → base disponible (503) → jeton (401) → limite par jeton, 15/min (429 ACTOR_RATE_LIMIT) → plan de l'espace (403 plan_required, sauf clé partenaire) → portée matches:write (403 INSUFFICIENT_SCOPE) → quota du plan (429 RATE_LIMITED / QUOTA_EXCEEDED, sauf clé partenaire) → maintenance (503) → validation de id puis du corps (400) → rejeu d'idempotence → match (404, 400 bye ou équipes manquantes, 409 déjà clôturé) → finalisation (500 si elle échoue).
IDEMPOTENCE. Envoyer Idempotency-Key : une réponse 2xx est rejouée pendant 5 minutes pour la même clé ET le même corps (en-tête Idempotency-Replay: true). Un corps différent avec la même clé est traité comme une nouvelle requête ; les erreurs ne sont pas mises en cache.
PAS DE CORS : un appel depuis un navigateur tiers échoue au preflight. Appeler depuis un serveur. Réponses Cache-Control: no-store.
Paramètres
| Paramètre | Emplacement | Type | Description |
|---|
idrequis | chemin | string <uuid> | Id (UUID) du match. Non-UUID → 400 INVALID_QUERY. |
idempotency-key | en-tête | string | Clé d'idempotence client (≤ 200 caractères après trim ; une clé vide ou plus longue est IGNORÉE, sans erreur). Une réponse 2xx est mise en cache 5 minutes et rejouée (en-tête Idempotency-Replay: true) pour la même route, la même clé ET le même corps : un corps différent est traité comme une nouvelle requête. Les erreurs ne sont pas mises en cache. Portée : espace de la clé (bot, public) ou membre du staff et espace actif (admin). Voir BOT_API_CONTRACT.md §Idempotency. |
Corps de la requête
| Propriété | Type | Description |
|---|
team1Scorerequis | integer | |
team2Scorerequis | integer | |
Exemple
{
"team1Score": 2,
"team2Score": 1
}Réponses
| Statut | Schéma | Description |
|---|
| 200 | object | En-têtes Idempotency-Replaytrue quand la réponse est rejouée depuis le cache d'idempotence (même Idempotency-Key ET même corps dans les 5 minutes). Absent sinon.
X-RateLimit-LimitLimite de la fenêtre du plan (sur un 429 de quota : limite dépassée ; sur un succès : limite par minute). Absent pour une clé partenaire (comp) et pour les plans illimités. X-RateLimit-RemainingRequêtes restantes dans la minute pour le plan (succès uniquement). Absent pour une clé partenaire (comp) et pour les plans illimités.
Exemple{
"data": {
"matchId": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f",
"status": "finished",
"team1Score": 2,
"team2Score": 1,
"winnerTeamId": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b"
}
} |
| 400 | PublicApiError | Validation refusée (INVALID_QUERY, INVALID_BODY, avec fields) ou match inapplicable (BAD_REQUEST : bye, équipes non assignées). Exemple — Score hors bornes{
"error": "Too big: expected number to be <=99",
"code": "INVALID_BODY",
"fields": {
"team1Score": [
"Too big: expected number to be <=99"
]
}
}Exemple — Match sans ses deux équipes{
"error": "Match incomplet (équipes non assignées)",
"code": "BAD_REQUEST"
} |
| 401 | PublicApiError | Jeton absent, mal formé, inconnu, révoqué ou expiré — UNAUTHORIZED. Les cinq cas renvoient la même réponse. Exemple{
"error": "Invalid or missing API token.",
"code": "UNAUTHORIZED"
} |
| 403 | PublicPlanDenial | PublicApiError | Deux corps possibles, évalués dans cet ordre : plan insuffisant (PublicPlanDenial, sans code, jamais pour une clé partenaire), puis portée manquante (INSUFFICIENT_SCOPE). Exemple — Plan insuffisant{
"error": "plan_required",
"message": "Cette clé API en écriture nécessite le plan Circuit. Le plan Régie n'ouvre que la lecture. Mettez à niveau votre abonnement pour écrire via l'API.",
"requiredCapability": "apiWrite"
}Exemple — Portée manquante{
"error": "Token lacks required scope 'matches:write'.",
"code": "INSUFFICIENT_SCOPE"
} |
| 404 | PublicApiError | Ressource inconnue dans l'espace servi, ou non publique — NOT_FOUND. Exemple{
"error": "Tournament not found",
"code": "NOT_FOUND"
} |
| 405 | PublicApiError | Méthode autre que POST — METHOD_NOT_ALLOWED, en-tête Allow: POST. En-têtes AllowMéthodes acceptées par la route.
Exemple{
"error": "Method not allowed",
"code": "METHOD_NOT_ALLOWED"
} |
| 409 | PublicApiError | Match déjà clôturé (finished, walkover ou cancelled) — CONFLICT. Exemple{
"error": "Match déjà clôturé (status=finished). Contactez le staff pour modifier.",
"code": "CONFLICT"
} |
| 429 | PublicApiError | Trois limites successives : par IP (corps sans code), par jeton (ACTOR_RATE_LIMIT), puis quota du plan — débit par minute (RATE_LIMITED) ou quota mensuel (QUOTA_EXCEEDED), avec X-RateLimit-Scope et X-RateLimit-Limit. Le quota du plan laisse passer si son compteur est indisponible et ne s'applique pas à une clé partenaire (comp). En-têtes Retry-AfterSecondes à attendre avant de réessayer. X-RateLimit-ScopeFenêtre du quota de plan dépassée : minute (débit) ou month (quota mensuel). Absent des 429 du limiteur par IP ou par token. X-RateLimit-LimitLimite de la fenêtre du plan (sur un 429 de quota : limite dépassée ; sur un succès : limite par minute). Absent pour une clé partenaire (comp) et pour les plans illimités.
Exemple — Limite par IP (sans code){
"error": "Trop de requêtes. Réessayez plus tard."
}Exemple — Limite par jeton{
"error": "Trop de requêtes pour ton compte. Réessaye plus tard.",
"code": "ACTOR_RATE_LIMIT"
}Exemple — Débit du plan{
"error": "API rate limit exceeded.",
"code": "RATE_LIMITED"
}Exemple — Quota mensuel du plan{
"error": "Monthly API quota exceeded for this plan.",
"code": "QUOTA_EXCEEDED"
} |
| 500 | PublicApiError | Erreur serveur — INTERNAL, y compris l'échec de la finalisation du score (dont une finalisation concurrente du même match). Relire le match (GET /api/public/v1/matches/{id}) avant de réessayer. Exemple{
"error": "Échec de la finalisation : …",
"code": "INTERNAL"
} |
| 503 | PublicApiError | Écritures gelées pendant une maintenance (MAINTENANCE_MODE, Retry-After: 60), ou base indisponible (INTERNAL, sans Retry-After). En-têtes Retry-AfterSecondes à attendre avant de réessayer.
Exemple — Maintenance{
"error": "Site en maintenance, écritures temporairement désactivées.",
"code": "MAINTENANCE_MODE"
}Exemple — Base indisponible{
"error": "Service unavailable.",
"code": "INTERNAL"
} |
get/api/public/v1/matches/{id}Sans auth
Détail d'un match public + games (map par map).
Match de l'espace servi et ses parties, triées par map_order.
Seuls les statuts pending, ongoing et finished sont servis : un match walkover, disputed, postponed ou cancelled renvoie 404, comme un match inconnu.
Espace servi : voir « Quel espace répond » dans l'introduction. Le jeton Authorization est ignoré ici.
Anonyme, CORS *. Limite : 120 requêtes par minute par IP pour cet endpoint. Cache : 30 s.
Paramètres
| Paramètre | Emplacement | Type | Description |
|---|
idrequis | chemin | string <uuid> | Id (UUID) du match. Un slug n'est PAS accepté (400). |
tenant | requête | string | Slug de l'espace à lire (ex. pogtv). Absent, inconnu ou espace désactivé : sur owwomenscup.fr, c'est l'espace Women's Cup qui répond, SANS erreur. Le jeton Authorization ne le remplace pas : ces lectures l'ignorent. Appelée sur le domaine propre d'un espace, l'API sert cet espace (le domaine prime sur ce paramètre). |
Réponses
| Statut | Schéma | Description |
|---|
| 200 | object | En-têtes Cache-ControlPosé par la route sur toute réponse 200 : public, s-maxage=<N>, stale-while-revalidate=<N/2>. La valeur de N dépend de l'endpoint (indiquée dans sa description). Le CDN met en cache par URL complète, paramètres de query compris : une donnée modifiée peut mettre jusqu'à N secondes à apparaître sur une URL déjà servie.
Exemple{
"data": {
"id": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f",
"stage_id": "9b8c7d6e-5f4a-4b3c-a2d1-0e9f8a7b6c5d",
"round_number": 1,
"bracket_side": "wb",
"team1_id": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
"team1_name": "Les Aurores",
"team1_logo_url": "https://example.org/logos/aurores.png",
"team2_id": "2f3e4d5c-6b7a-4899-8a1b-2c3d4e5f6a7b",
"team2_name": "Nébuleuse",
"team2_logo_url": null,
"team1_score": 2,
"team2_score": 1,
"winner_team_id": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
"status": "finished",
"scheduled_at": "2026-10-03T18:00:00+00:00",
"games": [
{
"map_name": "Lijiang Tower",
"map_order": 1,
"team1_score": 2,
"team2_score": 0,
"winner_team_id": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b"
},
{
"map_name": "King's Row",
"map_order": 2,
"team1_score": 2,
"team2_score": 3,
"winner_team_id": "2f3e4d5c-6b7a-4899-8a1b-2c3d4e5f6a7b"
},
{
"map_name": "Circuit Royal",
"map_order": 3,
"team1_score": 1,
"team2_score": 0,
"winner_team_id": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b"
}
]
}
} |
| 400 | PublicApiError | Identifiant invalide (UUID exigé) — BAD_REQUEST. Exemple{
"error": "Invalid match id",
"code": "BAD_REQUEST"
} |
| 404 | PublicApiError | Ressource inconnue dans l'espace servi, ou non publique — NOT_FOUND. Exemple{
"error": "Tournament not found",
"code": "NOT_FOUND"
} |
| 405 | PublicApiError | Méthode autre que GET / OPTIONS — METHOD_NOT_ALLOWED. En-têtes AllowMéthodes acceptées par la route.
Exemple{
"error": "Method not allowed",
"code": "METHOD_NOT_ALLOWED"
} |
| 429 | PublicApiError | Limite de débit par IP dépassée. Deux limiteurs se suivent, avec la même limite (120 requêtes par minute, compteur distinct par endpoint) : un limiteur en mémoire par instance, dont le corps n'a PAS de code, puis un compteur durable partagé (RATE_LIMITED, laisse passer si le compteur est indisponible). Respecter Retry-After. En-têtes Retry-AfterSecondes à attendre avant de réessayer.
Exemple — Limiteur en mémoire (sans code){
"error": "Trop de requêtes. Réessayez plus tard."
}Exemple — Compteur durable{
"error": "Trop de requêtes. Réessayez plus tard.",
"code": "RATE_LIMITED"
} |
| 500 | PublicApiError | Erreur serveur — INTERNAL. Réessayer plus tard. Exemple{
"error": "Internal server error",
"code": "INTERNAL"
} |
get/api/public/v1/players/{userId}Sans auth
Profil public d'une joueuse (rating, historique, H2H, achievements).
Profil de la joueuse dans l'espace servi. Une joueuse qui n'a encore joué aucun match classé a quand même un profil si elle figure sur un roster : player.unrated vaut alors true, player.rank vaut null et les chiffres de rating valent 0 (ne pas les afficher). 404 si la joueuse n'est connue ni du classement ni d'un roster de l'espace.
Espace servi : voir « Quel espace répond » dans l'introduction. Le jeton Authorization est ignoré ici.
Anonyme, CORS *. Limite : 120 requêtes par minute par IP pour cet endpoint. Cache : 60 s.
Paramètres
| Paramètre | Emplacement | Type | Description |
|---|
userIdrequis | chemin | string <uuid> | Id (UUID) de la joueuse. Un slug ou un pseudo n'est PAS accepté (400). |
tenant | requête | string | Slug de l'espace à lire (ex. pogtv). Absent, inconnu ou espace désactivé : sur owwomenscup.fr, c'est l'espace Women's Cup qui répond, SANS erreur. Le jeton Authorization ne le remplace pas : ces lectures l'ignorent. Appelée sur le domaine propre d'un espace, l'API sert cet espace (le domaine prime sur ce paramètre). |
Réponses
| Statut | Schéma | Description |
|---|
| 200 | object | En-têtes Cache-ControlPosé par la route sur toute réponse 200 : public, s-maxage=<N>, stale-while-revalidate=<N/2>. La valeur de N dépend de l'endpoint (indiquée dans sa description). Le CDN met en cache par URL complète, paramètres de query compris : une donnée modifiée peut mettre jusqu'à N secondes à apparaître sur une URL déjà servie.
Exemple{
"data": {
"player": {
"userId": "8d7c6b5a-4938-4271-b6a5-948372615049",
"displayName": "Nova",
"battleTag": "Nova#21456",
"avatarUrl": null,
"twitch": "nova_ow",
"unrated": false,
"rating": 1623.4,
"rd": 71.2,
"volatility": 0.06,
"peakRating": 1650.1,
"gamesPlayed": 12,
"wins": 8,
"losses": 4,
"rank": 3
},
"history": [
{
"matchId": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f",
"tournamentId": "3f2c8a1e-6b4d-4c7a-9e21-5d8f0b7a1c34",
"occurredAt": "2026-10-03T19:12:00+00:00",
"ratingBefore": 1598.9,
"ratingAfter": 1623.4,
"result": "win",
"opponentAvgRating": 1571.3
}
],
"recentMatches": [
{
"matchId": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f",
"tournamentId": "3f2c8a1e-6b4d-4c7a-9e21-5d8f0b7a1c34",
"occurredAt": "2026-10-03T19:12:00+00:00",
"result": "win",
"opponentTeamId": "2f3e4d5c-6b7a-4899-8a1b-2c3d4e5f6a7b",
"opponentTeamName": "Nébuleuse"
}
],
"h2h": [
{
"opponentUserId": "0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
"opponentDisplayName": "Orbite",
"opponentBattleTag": "Orbite#1187",
"wins": 1,
"losses": 0,
"games": 1
}
],
"achievements": {
"badges": [],
"palmares": [
{
"tournamentId": "6e5d4c3b-2a19-4087-b6e5-d4c3b2a19087",
"tournamentName": "Coupe de printemps 2026",
"tournamentSlug": "coupe-printemps-2026",
"teamId": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
"teamName": "Les Aurores",
"rank": 2,
"date": "2026-04-19"
}
],
"seasons": []
}
}
} |
| 400 | PublicApiError | Identifiant invalide (UUID exigé) — BAD_REQUEST. Exemple{
"error": "Invalid match id",
"code": "BAD_REQUEST"
} |
| 404 | PublicApiError | Ressource inconnue dans l'espace servi, ou non publique — NOT_FOUND. Exemple{
"error": "Tournament not found",
"code": "NOT_FOUND"
} |
| 405 | PublicApiError | Méthode autre que GET / OPTIONS — METHOD_NOT_ALLOWED. En-têtes AllowMéthodes acceptées par la route.
Exemple{
"error": "Method not allowed",
"code": "METHOD_NOT_ALLOWED"
} |
| 429 | PublicApiError | Limite de débit par IP dépassée. Deux limiteurs se suivent, avec la même limite (120 requêtes par minute, compteur distinct par endpoint) : un limiteur en mémoire par instance, dont le corps n'a PAS de code, puis un compteur durable partagé (RATE_LIMITED, laisse passer si le compteur est indisponible). Respecter Retry-After. En-têtes Retry-AfterSecondes à attendre avant de réessayer.
Exemple — Limiteur en mémoire (sans code){
"error": "Trop de requêtes. Réessayez plus tard."
}Exemple — Compteur durable{
"error": "Trop de requêtes. Réessayez plus tard.",
"code": "RATE_LIMITED"
} |
| 500 | PublicApiError | Erreur serveur — INTERNAL. Réessayer plus tard. Exemple{
"error": "Internal server error",
"code": "INTERNAL"
} |
get/api/public/v1/teams/{id}Sans auth
Équipe publique + roster public (sans données privées).
Équipe de l'espace servi, trouvée par id puis par slug (404 si aucune). Le roster ne porte que le pseudo, le rôle et le statut de remplaçante : ni email, ni identifiant Discord. Ordre du roster non garanti.
Espace servi : voir « Quel espace répond » dans l'introduction. Le jeton Authorization est ignoré ici.
Anonyme, CORS *. Limite : 120 requêtes par minute par IP pour cet endpoint. Cache : 120 s.
Paramètres
| Paramètre | Emplacement | Type | Description |
|---|
idrequis | chemin | string | Id (UUID) ou slug de l'équipe. |
tenant | requête | string | Slug de l'espace à lire (ex. pogtv). Absent, inconnu ou espace désactivé : sur owwomenscup.fr, c'est l'espace Women's Cup qui répond, SANS erreur. Le jeton Authorization ne le remplace pas : ces lectures l'ignorent. Appelée sur le domaine propre d'un espace, l'API sert cet espace (le domaine prime sur ce paramètre). |
Réponses
| Statut | Schéma | Description |
|---|
| 200 | object | En-têtes Cache-ControlPosé par la route sur toute réponse 200 : public, s-maxage=<N>, stale-while-revalidate=<N/2>. La valeur de N dépend de l'endpoint (indiquée dans sa description). Le CDN met en cache par URL complète, paramètres de query compris : une donnée modifiée peut mettre jusqu'à N secondes à apparaître sur une URL déjà servie.
Exemple{
"data": {
"id": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
"name": "Les Aurores",
"short_name": "AUR",
"slug": "les-aurores",
"logo_url": "https://example.org/logos/aurores.png",
"roster": [
{
"display_name": "Nova",
"role": "player",
"is_substitute": false
},
{
"display_name": "Kestrel",
"role": "player",
"is_substitute": true
}
]
}
} |
| 404 | PublicApiError | Ressource inconnue dans l'espace servi, ou non publique — NOT_FOUND. Exemple{
"error": "Tournament not found",
"code": "NOT_FOUND"
} |
| 405 | PublicApiError | Méthode autre que GET / OPTIONS — METHOD_NOT_ALLOWED. En-têtes AllowMéthodes acceptées par la route.
Exemple{
"error": "Method not allowed",
"code": "METHOD_NOT_ALLOWED"
} |
| 429 | PublicApiError | Limite de débit par IP dépassée. Deux limiteurs se suivent, avec la même limite (120 requêtes par minute, compteur distinct par endpoint) : un limiteur en mémoire par instance, dont le corps n'a PAS de code, puis un compteur durable partagé (RATE_LIMITED, laisse passer si le compteur est indisponible). Respecter Retry-After. En-têtes Retry-AfterSecondes à attendre avant de réessayer.
Exemple — Limiteur en mémoire (sans code){
"error": "Trop de requêtes. Réessayez plus tard."
}Exemple — Compteur durable{
"error": "Trop de requêtes. Réessayez plus tard.",
"code": "RATE_LIMITED"
} |
| 500 | PublicApiError | Erreur serveur — INTERNAL. Réessayer plus tard. Exemple{
"error": "Internal server error",
"code": "INTERNAL"
} |
get/api/public/v1/tournaments/{id}/arbitrationSans auth
Métriques d'arbitrage agrégées (non-nominatives) du tournoi.
Agrégat de l'activité d'arbitrage (litiges) d'un tournoi public : volume, résolution, respect du SLA, répartition des litiges ouverts. AUCUN identifiant ni raison — que des nombres. 404 si le tournoi n'est pas public (draft / archived / inconnu).
Espace servi : voir « Quel espace répond » dans l'introduction. Le jeton Authorization est ignoré ici.
Anonyme, CORS *. Limite : 120 requêtes par minute par IP pour cet endpoint. Cache : 60 s.
Paramètres
| Paramètre | Emplacement | Type | Description |
|---|
idrequis | chemin | string | Id (UUID) ou slug du tournoi. |
tenant | requête | string | Slug de l'espace à lire (ex. pogtv). Absent, inconnu ou espace désactivé : sur owwomenscup.fr, c'est l'espace Women's Cup qui répond, SANS erreur. Le jeton Authorization ne le remplace pas : ces lectures l'ignorent. Appelée sur le domaine propre d'un espace, l'API sert cet espace (le domaine prime sur ce paramètre). |
Réponses
| Statut | Schéma | Description |
|---|
| 200 | object | Métriques d'arbitrage agrégées du tournoi. En-têtes Cache-ControlPosé par la route sur toute réponse 200 : public, s-maxage=<N>, stale-while-revalidate=<N/2>. La valeur de N dépend de l'endpoint (indiquée dans sa description). Le CDN met en cache par URL complète, paramètres de query compris : une donnée modifiée peut mettre jusqu'à N secondes à apparaître sur une URL déjà servie.
Exemple{
"data": {
"tournamentId": "3f2c8a1e-6b4d-4c7a-9e21-5d8f0b7a1c34",
"tournamentName": "Coupe d'automne 2026",
"tournamentSlug": "coupe-automne-2026",
"metrics": {
"totalDisputes": 4,
"open": 1,
"resolved": 3,
"avgResolutionMinutes": 43,
"medianResolutionMinutes": 38,
"withinSlaCount": 2,
"slaComplianceRate": 0.6667,
"openBreakdown": {
"breached": 0,
"approaching": 1,
"fresh": 0
},
"slaMinutes": 60
}
}
} |
| 404 | PublicApiError | Ressource inconnue dans l'espace servi, ou non publique — NOT_FOUND. Exemple{
"error": "Tournament not found",
"code": "NOT_FOUND"
} |
| 405 | PublicApiError | Méthode autre que GET / OPTIONS — METHOD_NOT_ALLOWED. En-têtes AllowMéthodes acceptées par la route.
Exemple{
"error": "Method not allowed",
"code": "METHOD_NOT_ALLOWED"
} |
| 429 | PublicApiError | Limite de débit par IP dépassée. Deux limiteurs se suivent, avec la même limite (120 requêtes par minute, compteur distinct par endpoint) : un limiteur en mémoire par instance, dont le corps n'a PAS de code, puis un compteur durable partagé (RATE_LIMITED, laisse passer si le compteur est indisponible). Respecter Retry-After. En-têtes Retry-AfterSecondes à attendre avant de réessayer.
Exemple — Limiteur en mémoire (sans code){
"error": "Trop de requêtes. Réessayez plus tard."
}Exemple — Compteur durable{
"error": "Trop de requêtes. Réessayez plus tard.",
"code": "RATE_LIMITED"
} |
| 500 | PublicApiError | Erreur serveur — INTERNAL. Réessayer plus tard. Exemple{
"error": "Internal server error",
"code": "INTERNAL"
} |
get/api/public/v1/tournaments/{id}Sans auth
Détail d'un tournoi public + résumé des stages.
Tournoi de l'espace servi, trouvé par id ou slug, s'il est published, running ou completed ; sinon (inconnu, draft, archived) 404. Les phases sont triées par ordre d'affichage ; leur status vaut active ou inactive.
Espace servi : voir « Quel espace répond » dans l'introduction. Le jeton Authorization est ignoré ici.
Anonyme, CORS *. Limite : 120 requêtes par minute par IP pour cet endpoint. Cache : 60 s.
Paramètres
| Paramètre | Emplacement | Type | Description |
|---|
idrequis | chemin | string | Id (UUID) ou slug du tournoi. |
tenant | requête | string | Slug de l'espace à lire (ex. pogtv). Absent, inconnu ou espace désactivé : sur owwomenscup.fr, c'est l'espace Women's Cup qui répond, SANS erreur. Le jeton Authorization ne le remplace pas : ces lectures l'ignorent. Appelée sur le domaine propre d'un espace, l'API sert cet espace (le domaine prime sur ce paramètre). |
Réponses
| Statut | Schéma | Description |
|---|
| 200 | object | En-têtes Cache-ControlPosé par la route sur toute réponse 200 : public, s-maxage=<N>, stale-while-revalidate=<N/2>. La valeur de N dépend de l'endpoint (indiquée dans sa description). Le CDN met en cache par URL complète, paramètres de query compris : une donnée modifiée peut mettre jusqu'à N secondes à apparaître sur une URL déjà servie.
Exemple{
"data": {
"id": "3f2c8a1e-6b4d-4c7a-9e21-5d8f0b7a1c34",
"name": "Coupe d'automne 2026",
"slug": "coupe-automne-2026",
"game": "overwatch",
"status": "running",
"start_date": "2026-10-03",
"end_date": "2026-10-25",
"format": null,
"stages": [
{
"id": "7a1d2e3f-4b5c-4d6e-8f70-1a2b3c4d5e6f",
"name": "Phase de groupes",
"stage_type": "group",
"status": "inactive"
},
{
"id": "9b8c7d6e-5f4a-4b3c-a2d1-0e9f8a7b6c5d",
"name": "Bracket final",
"stage_type": "bracket",
"status": "active"
}
]
}
} |
| 404 | PublicApiError | Ressource inconnue dans l'espace servi, ou non publique — NOT_FOUND. Exemple{
"error": "Tournament not found",
"code": "NOT_FOUND"
} |
| 405 | PublicApiError | Méthode autre que GET / OPTIONS — METHOD_NOT_ALLOWED. En-têtes AllowMéthodes acceptées par la route.
Exemple{
"error": "Method not allowed",
"code": "METHOD_NOT_ALLOWED"
} |
| 429 | PublicApiError | Limite de débit par IP dépassée. Deux limiteurs se suivent, avec la même limite (120 requêtes par minute, compteur distinct par endpoint) : un limiteur en mémoire par instance, dont le corps n'a PAS de code, puis un compteur durable partagé (RATE_LIMITED, laisse passer si le compteur est indisponible). Respecter Retry-After. En-têtes Retry-AfterSecondes à attendre avant de réessayer.
Exemple — Limiteur en mémoire (sans code){
"error": "Trop de requêtes. Réessayez plus tard."
}Exemple — Compteur durable{
"error": "Trop de requêtes. Réessayez plus tard.",
"code": "RATE_LIMITED"
} |
| 500 | PublicApiError | Erreur serveur — INTERNAL. Réessayer plus tard. Exemple{
"error": "Internal server error",
"code": "INTERNAL"
} |
get/api/public/v1/tournaments/{id}/matchesSans auth
Matches d'un tournoi (overlays de bracket).
Matchs d'un tournoi public (404 si le tournoi est inconnu ou non public), triés par scheduled_at croissante. Pas de pagination : tous les matchs retenus sont renvoyés.
Statuts renvoyés : pending, ongoing, finished UNIQUEMENT. Un match walkover (victoire par forfait, qui fait pourtant avancer le bracket), disputed, postponed ou cancelled n'apparaît PAS dans la liste.
Espace servi : voir « Quel espace répond » dans l'introduction. Le jeton Authorization est ignoré ici.
Anonyme, CORS *. Limite : 120 requêtes par minute par IP pour cet endpoint. Cache : 30 s.
Paramètres
| Paramètre | Emplacement | Type | Description |
|---|
idrequis | chemin | string | Id (UUID) ou slug du tournoi. |
tenant | requête | string | Slug de l'espace à lire (ex. pogtv). Absent, inconnu ou espace désactivé : sur owwomenscup.fr, c'est l'espace Women's Cup qui répond, SANS erreur. Le jeton Authorization ne le remplace pas : ces lectures l'ignorent. Appelée sur le domaine propre d'un espace, l'API sert cet espace (le domaine prime sur ce paramètre). |
stageId | requête | string | Égalité stricte sur la phase ; non validé (une valeur inconnue renvoie une liste vide). |
status | requête | enum(pending | ongoing | finished) | Une valeur hors de l'énumération est ignorée (aucun 400) : la route renvoie alors les trois statuts. |
Réponses
| Statut | Schéma | Description |
|---|
| 200 | object | Liste des matches publics du tournoi. En-têtes Cache-ControlPosé par la route sur toute réponse 200 : public, s-maxage=<N>, stale-while-revalidate=<N/2>. La valeur de N dépend de l'endpoint (indiquée dans sa description). Le CDN met en cache par URL complète, paramètres de query compris : une donnée modifiée peut mettre jusqu'à N secondes à apparaître sur une URL déjà servie.
Exemple{
"data": [
{
"id": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f",
"stage_id": "9b8c7d6e-5f4a-4b3c-a2d1-0e9f8a7b6c5d",
"round_number": 1,
"bracket_side": "wb",
"team1_id": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
"team1_name": "Les Aurores",
"team1_logo_url": "https://example.org/logos/aurores.png",
"team2_id": "2f3e4d5c-6b7a-4899-8a1b-2c3d4e5f6a7b",
"team2_name": "Nébuleuse",
"team2_logo_url": null,
"team1_score": 2,
"team2_score": 1,
"winner_team_id": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
"status": "finished",
"scheduled_at": "2026-10-03T18:00:00+00:00"
}
]
} |
| 404 | PublicApiError | Ressource inconnue dans l'espace servi, ou non publique — NOT_FOUND. Exemple{
"error": "Tournament not found",
"code": "NOT_FOUND"
} |
| 405 | PublicApiError | Méthode autre que GET / OPTIONS — METHOD_NOT_ALLOWED. En-têtes AllowMéthodes acceptées par la route.
Exemple{
"error": "Method not allowed",
"code": "METHOD_NOT_ALLOWED"
} |
| 429 | PublicApiError | Limite de débit par IP dépassée. Deux limiteurs se suivent, avec la même limite (120 requêtes par minute, compteur distinct par endpoint) : un limiteur en mémoire par instance, dont le corps n'a PAS de code, puis un compteur durable partagé (RATE_LIMITED, laisse passer si le compteur est indisponible). Respecter Retry-After. En-têtes Retry-AfterSecondes à attendre avant de réessayer.
Exemple — Limiteur en mémoire (sans code){
"error": "Trop de requêtes. Réessayez plus tard."
}Exemple — Compteur durable{
"error": "Trop de requêtes. Réessayez plus tard.",
"code": "RATE_LIMITED"
} |
| 500 | PublicApiError | Erreur serveur — INTERNAL. Réessayer plus tard. Exemple{
"error": "Internal server error",
"code": "INTERNAL"
} |
get/api/public/v1/tournaments/{id}/standingsSans auth
Classement final du tournoi (vide si non finalisé).
Classement final d'un tournoi public (404 si le tournoi est inconnu ou non public). Liste VIDE tant que le tournoi n'a pas été finalisé : ce n'est pas une erreur.
Espace servi : voir « Quel espace répond » dans l'introduction. Le jeton Authorization est ignoré ici.
Anonyme, CORS *. Limite : 120 requêtes par minute par IP pour cet endpoint. Cache : 60 s.
Paramètres
| Paramètre | Emplacement | Type | Description |
|---|
idrequis | chemin | string | Id (UUID) ou slug du tournoi. |
tenant | requête | string | Slug de l'espace à lire (ex. pogtv). Absent, inconnu ou espace désactivé : sur owwomenscup.fr, c'est l'espace Women's Cup qui répond, SANS erreur. Le jeton Authorization ne le remplace pas : ces lectures l'ignorent. Appelée sur le domaine propre d'un espace, l'API sert cet espace (le domaine prime sur ce paramètre). |
Réponses
| Statut | Schéma | Description |
|---|
| 200 | object | Classement final trié par rank. En-têtes Cache-ControlPosé par la route sur toute réponse 200 : public, s-maxage=<N>, stale-while-revalidate=<N/2>. La valeur de N dépend de l'endpoint (indiquée dans sa description). Le CDN met en cache par URL complète, paramètres de query compris : une donnée modifiée peut mettre jusqu'à N secondes à apparaître sur une URL déjà servie.
Exemple{
"data": [
{
"rank": 1,
"teamId": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
"teamName": "Les Aurores",
"teamSlug": "les-aurores",
"logoUrl": "https://example.org/logos/aurores.png",
"prize": null
}
]
} |
| 404 | PublicApiError | Ressource inconnue dans l'espace servi, ou non publique — NOT_FOUND. Exemple{
"error": "Tournament not found",
"code": "NOT_FOUND"
} |
| 405 | PublicApiError | Méthode autre que GET / OPTIONS — METHOD_NOT_ALLOWED. En-têtes AllowMéthodes acceptées par la route.
Exemple{
"error": "Method not allowed",
"code": "METHOD_NOT_ALLOWED"
} |
| 429 | PublicApiError | Limite de débit par IP dépassée. Deux limiteurs se suivent, avec la même limite (120 requêtes par minute, compteur distinct par endpoint) : un limiteur en mémoire par instance, dont le corps n'a PAS de code, puis un compteur durable partagé (RATE_LIMITED, laisse passer si le compteur est indisponible). Respecter Retry-After. En-têtes Retry-AfterSecondes à attendre avant de réessayer.
Exemple — Limiteur en mémoire (sans code){
"error": "Trop de requêtes. Réessayez plus tard."
}Exemple — Compteur durable{
"error": "Trop de requêtes. Réessayez plus tard.",
"code": "RATE_LIMITED"
} |
| 500 | PublicApiError | Erreur serveur — INTERNAL. Réessayer plus tard. Exemple{
"error": "Internal server error",
"code": "INTERNAL"
} |
get/api/public/v1/tournamentsSans auth
Liste des tournois publics (published/running/completed).
Tournois de l'espace servi dont le statut est published, running ou completed (jamais draft ni archived), triés par start_date décroissante puis created_at décroissante. pagination.count est le total avant pagination.
Espace servi : voir « Quel espace répond » dans l'introduction. Le jeton Authorization est ignoré ici.
Anonyme, CORS *. Limite : 120 requêtes par minute par IP pour cet endpoint. Cache : 60 s.
Paramètres
| Paramètre | Emplacement | Type | Description |
|---|
tenant | requête | string | Slug de l'espace à lire (ex. pogtv). Absent, inconnu ou espace désactivé : sur owwomenscup.fr, c'est l'espace Women's Cup qui répond, SANS erreur. Le jeton Authorization ne le remplace pas : ces lectures l'ignorent. Appelée sur le domaine propre d'un espace, l'API sert cet espace (le domaine prime sur ce paramètre). |
status | requête | enum(published | running | completed) | Filtre sur un statut public. Une valeur hors de l'énumération est ignorée (aucun 400) : la route renvoie alors les trois statuts. |
game | requête | string | Égalité stricte sur le jeu. |
limit | requête | integer | Ramené dans [1, 100] ; une valeur non numérique vaut 50. |
offset | requête | integer | |
Réponses
| Statut | Schéma | Description |
|---|
| 200 | object | Page de tournois publics + pagination. En-têtes Cache-ControlPosé par la route sur toute réponse 200 : public, s-maxage=<N>, stale-while-revalidate=<N/2>. La valeur de N dépend de l'endpoint (indiquée dans sa description). Le CDN met en cache par URL complète, paramètres de query compris : une donnée modifiée peut mettre jusqu'à N secondes à apparaître sur une URL déjà servie.
Exemple{
"data": [
{
"id": "3f2c8a1e-6b4d-4c7a-9e21-5d8f0b7a1c34",
"name": "Coupe d'automne 2026",
"slug": "coupe-automne-2026",
"game": "overwatch",
"status": "running",
"start_date": "2026-10-03",
"end_date": "2026-10-25",
"format": null
}
],
"pagination": {
"limit": 50,
"offset": 0,
"count": 1
}
} |
| 405 | PublicApiError | Méthode autre que GET / OPTIONS — METHOD_NOT_ALLOWED. En-têtes AllowMéthodes acceptées par la route.
Exemple{
"error": "Method not allowed",
"code": "METHOD_NOT_ALLOWED"
} |
| 429 | PublicApiError | Limite de débit par IP dépassée. Deux limiteurs se suivent, avec la même limite (120 requêtes par minute, compteur distinct par endpoint) : un limiteur en mémoire par instance, dont le corps n'a PAS de code, puis un compteur durable partagé (RATE_LIMITED, laisse passer si le compteur est indisponible). Respecter Retry-After. En-têtes Retry-AfterSecondes à attendre avant de réessayer.
Exemple — Limiteur en mémoire (sans code){
"error": "Trop de requêtes. Réessayez plus tard."
}Exemple — Compteur durable{
"error": "Trop de requêtes. Réessayez plus tard.",
"code": "RATE_LIMITED"
} |
| 500 | PublicApiError | Erreur serveur — INTERNAL. Réessayer plus tard. Exemple{
"error": "Internal server error",
"code": "INTERNAL"
} |
get/api/public/webhook-eventsSans auth
Catalogue public des types d'événements webhook souscriptibles.
Catalogue anonyme des types d'événements qu'un espace peut recevoir en webhook sortant, et convention de signature. Dérivé de la liste blanche WEBHOOK_EVENT_TYPES : les événements Discord internes n'y figurent jamais, même avec un abonnement *.
ATTENTION : match.forfeit figure au catalogue mais n'est émis par aucun code aujourd'hui — un abonnement à cet événement ne reçoit rien.
LIVRAISON (non exposée par cette route, décrite ici pour l'intégration) :
- un
POST par événement et par abonnement actif de l'espace ; corps = { id, event, tenantId, timestamp, data } ; la forme de data dépend de l'événement et n'est pas décrite dans cette spec ; - en-têtes :
Content-Type: application/json, User-Agent: conference-website-webhooks/1, X-Webhook-Event, X-Webhook-Id, X-Tenant-Id et X-Webhook-Signature: sha256=<hex> (HMAC-SHA256 du corps BRUT avec le secret de l'abonnement, affiché une seule fois à sa création) ; - une livraison réussit sur toute réponse 2xx reçue en moins de 8 s ;
- le répartiteur passe chaque minute : un échec est retenté au passage suivant, 5 tentatives au plus, et seuls les événements des dernières 24 h sont examinés ;
- 15 échecs consécutifs sur un abonnement le désactivent ; un succès remet le compteur à zéro.
Abonnements : back-office /admin/webhooks.
Anonyme, CORS *. Limite : 120 requêtes par minute par IP. Cache : 3600 s.
Réponses
| Statut | Schéma | Description |
|---|
| 200 | object | Catalogue d'événements + convention de signature. En-têtes Cache-ControlPosé par la route sur toute réponse 200 : public, s-maxage=<N>, stale-while-revalidate=<N/2>. La valeur de N dépend de l'endpoint (indiquée dans sa description). Le CDN met en cache par URL complète, paramètres de query compris : une donnée modifiée peut mettre jusqu'à N secondes à apparaître sur une URL déjà servie.
Exemple{
"data": {
"events": [
{
"type": "match.finished",
"description": "A match has finished and a result has been recorded."
}
],
"signature": {
"header": "X-Webhook-Signature",
"algo": "HMAC-SHA256",
"format": "sha256=<hex>"
}
}
} |
| 405 | PublicApiError | Méthode autre que GET / OPTIONS — METHOD_NOT_ALLOWED. En-têtes AllowMéthodes acceptées par la route.
Exemple{
"error": "Method not allowed",
"code": "METHOD_NOT_ALLOWED"
} |
| 429 | PublicApiError | Limite de débit par IP dépassée. Deux limiteurs se suivent, avec la même limite (120 requêtes par minute, compteur distinct par endpoint) : un limiteur en mémoire par instance, dont le corps n'a PAS de code, puis un compteur durable partagé (RATE_LIMITED, laisse passer si le compteur est indisponible). Respecter Retry-After. En-têtes Retry-AfterSecondes à attendre avant de réessayer.
Exemple — Limiteur en mémoire (sans code){
"error": "Trop de requêtes. Réessayez plus tard."
}Exemple — Compteur durable{
"error": "Trop de requêtes. Réessayez plus tard.",
"code": "RATE_LIMITED"
} |
| 500 | PublicApiError | Erreur serveur — INTERNAL. Réessayer plus tard. Exemple{
"error": "Internal server error",
"code": "INTERNAL"
} |
Cette page est générée depuis la spec OpenAPI du dépôt à chaque déploiement. Des vérifications automatiques confrontent au code les endpoints, les méthodes, les réponses de succès, les exemples et les codes d'erreur ; le reste est rédigé à la main.