uVersion
Français
Télécharger →

Wiki

REST API

Référence des endpoints HTTP du serveur uVersion : authentification, dépôts, fichiers, verrous, commentaires, watchlist, board de production, builds, administration.

Le serveur uVersion expose une API HTTP JSON. Les appels sont authentifiés par un JWT (JSON Web Token), c'est-à-dire le jeton de session que le serveur vous remet au moment de la connexion et que vous renvoyez ensuite à chaque requête. Cette page documente les endpoints utilisés par le client desktop, les plugins éditeur et le CLI uversion. Vous pouvez les appeler directement pour intégrer uVersion dans votre outillage interne (tableau de bord maison, scripts d'audit, webhooks, etc.).

Elle ne couvre pas l'intégralité de l'API : plusieurs familles de routes existent et ne sont pas décrites ici. La liste se trouve dans la section Pans non couverts.

Conventions

Base URL

Tous les chemins documentés sont relatifs à l'URL de votre instance. Les exemples utilisent https://uversion.mygamestudio.com. Remplacez par la vôtre.

Headers requis

HeaderValeur
AuthorizationBearer <jwt> sur toutes les routes /api/* sauf /api/auth/* et /api/server-info (et /health)
Content-Typeapplication/json pour les POST/PUT avec body JSON. application/octet-stream pour les uploads binaires (build files).
Acceptapplication/json recommandé (le serveur retourne du JSON par défaut)

Deux formats de réponse, et il faut savoir lequel vous lisez

Le serveur n'a pas un format de réponse mais deux, et les confondre est l'erreur la plus coûteuse pour qui démarre une intégration.

1. Les routes authentifiées (tout /api/* sauf /api/auth/*) répondent avec une enveloppe :

{
  "success": true,
  "data": { /* payload */ }
}

En cas d'erreur, ces mêmes routes répondent :

{
  "success": false,
  "error": "No write permission on this repository"
}

Le champ success est toujours présent. data est présent en cas de succès, error en cas d'échec, jamais les deux ensemble.

2. Les routes d'authentification (/api/auth/login, /refresh, /validate, /logout, /register, /change-password) n'utilisent pas cette enveloppe. Elles renvoient l'objet nu, sans success ni data :

{
  "token": "eyJhbGciOiJIUzI1NiIs...",
  "user": { "id": 12, "username": "alice", ... },
  "must_change_password": false
}

Et leurs erreurs sont un objet à un seul champ, sans success :

{ "error": "Invalid credentials" }

Conséquence pratique : sur /api/auth/login, le jeton se lit en .token et non en .data.token. Un script qui interroge .data.token reçoit null sans aucune erreur visible.

Enfin, GET /api/files/{repo_id}/content ne renvoie ni l'un ni l'autre : c'est le contenu binaire du fichier, tel quel.

Pagination

Il n'y a pas de convention de pagination commune : chaque route a la sienne, ou n'en a pas. Ne présumez ni limit, ni total, ni has_more.

RouteParamètres acceptésForme de la réponse
GET /api/files/{repo_id}/history limit (défaut 50), offset (défaut 0), path Tableau plat de commits. Pas de total, pas de has_more : vous avez atteint la fin quand le tableau contient moins d'éléments que limit.
GET /api/repositories include_inactive uniquement (et il n'est honoré que pour un super administrateur) Tableau plat. Ni limit ni offset ne sont lus.
GET /api/locks/{repo_id}/status Aucun Tableau plat de tous les verrous du dépôt.
GET /api/files/{repo_id}/snapshot commit_hash, obligatoire Tableau plat de fichiers.
GET /api/admin/audit page et per_page, pas limit/offset, plus des filtres (user_id, action, entity_type, from, to) Réservée au super administrateur. Illustre bien l'absence de convention commune : c'est la seule route qui pagine par numéro de page.

Pour toute route non listée ici, considérez qu'elle renvoie l'intégralité de son résultat en un seul appel.

Limitation de débit

Il n'y a pas de limitation de débit générale : un projet Unreal compte des milliers de fichiers et les opérations en lot (réservation, libération de verrous) déclencheraient un throttle constamment. Vous pouvez donc appeler l'API en volume.

Une seule route est limitée, /api/auth/login : 5 tentatives échouées par nom d'utilisateur toutes les 15 minutes. Les connexions réussies ne sont pas comptées.

Révocation des jetons

Chaque jeton de session porte un champ tv (token version), qui reflète la colonne users.token_version en base. Le serveur compare les deux à chaque requête. Un appel à POST /api/auth/logout, une réinitialisation de mot de passe par un administrateur ou la désactivation d'un compte incrémentent cette valeur, ce qui invalide instantanément tous les jetons existants de cet utilisateur, sur toutes ses machines.

Curl

Pour appeler l'API depuis un terminal. Notez le .token : la réponse de /api/auth/login est l'objet nu, il n'y a pas de .data à traverser.

TOKEN=$(curl -s -X POST https://uversion.mygamestudio.com/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"alice","password":"secret"}' | jq -r '.token')

# Les routes authentifiées, elles, utilisent bien l'enveloppe :
curl -s -H "Authorization: Bearer $TOKEN" \
  https://uversion.mygamestudio.com/api/repositories | jq '.data'

Authentication

Rappel : aucune route de cette section n'utilise l'enveloppe {"success", "data"}. Elles renvoient l'objet nu, et leurs erreurs sont de la forme {"error": "..."}.

POST /api/auth/login

Authentifie un utilisateur et retourne un jeton de session valable 30 jours.

Body :

{
  "username": "alice",
  "password": "secret"
}

Response 200 :

{
  "token": "eyJhbGciOiJIUzI1NiIs...",
  "user": {
    "id": 12,
    "username": "alice",
    "email": "alice@mygamestudio.com",
    "role": "lead"
  },
  "must_change_password": false
}

Il n'y a pas de champ expires_at : la durée de validité se lit dans le jeton lui-même, ou se déduit de la configuration serveur (30 jours par défaut).

Ne pas ignorer must_change_password. Ce champ vaut true quand le compte tourne encore sur un mot de passe temporaire : celui que l'installateur a généré pour le compte administrateur initial, ou celui qu'un administrateur vient de poser lors d'une réinitialisation. Un client qui ne le regarde pas laisse l'utilisateur sur ce mot de passe provisoire indéfiniment. Le comportement attendu est de rediriger immédiatement vers POST /api/auth/change-password avant toute autre action.

Erreurs :

  • 401 : identifiants invalides ou compte désactivé
  • 429 : 5 tentatives échouées atteintes pour ce nom d'utilisateur dans la fenêtre de 15 minutes

Le serveur exécute toujours la vérification Argon2id du mot de passe, y compris contre une empreinte factice quand le compte n'existe pas. La réponse prend donc le même temps dans les deux cas, ce qui empêche de deviner l'existence d'un compte en chronométrant les requêtes.

POST /api/auth/refresh

Renouvelle le jeton de session sans repasser par le mot de passe.

Headers : Authorization: Bearer <jeton_actuel>. Pas de body.

Response 200 : exactement la même forme que /login (token, user, must_change_password), avec un jeton neuf.

Erreurs :

  • 401 : jeton invalide, expiré, compte désactivé, ou token_version obsolète

POST /api/auth/logout

Invalide tous les jetons de l'utilisateur courant, sur toutes ses machines, en incrémentant users.token_version. Il devra se reconnecter partout : client desktop, plugin éditeur et CLI compris.

Response 200 : { "logged_out": true }.

POST /api/auth/validate

Vérifie qu'un jeton est encore valide et renvoie l'utilisateur auquel il correspond. Le contrôle porte aussi sur is_active et sur token_version, donc un jeton révoqué est rejeté même s'il n'est pas encore arrivé à expiration.

Attention au body : ce n'est pas un objet, c'est une chaîne JSON nue, c'est-à-dire le jeton entouré de guillemets doubles.

curl -X POST https://uversion.mygamestudio.com/api/auth/validate \
  -H "Content-Type: application/json" \
  -d '"eyJhbGciOiJIUzI1NiIs..."'

Response 200 : un objet utilisateur nu.

{
  "id": 12,
  "username": "alice",
  "email": "alice@mygamestudio.com",
  "role": "lead"
}

Il n'y a ni valid, ni expires_at, ni refreshed_token : la validité se lit dans le code HTTP (200 ou 401), et le renouvellement passe par /api/auth/refresh, jamais par cette route.

POST /api/auth/register

Cette route refuse par défaut. L'inscription ouverte est désactivée sauf si l'exploitant l'a explicitement activée dans la configuration du serveur ; sinon la réponse est 403 avec {"error": "Open registration is disabled; contact your administrator"}.

Dans le fonctionnement normal, les comptes se créent par l'administration : POST /api/admin/users, ou l'onglet Utilisateurs du panneau d'administration. Ne construisez pas d'intégration qui dépende de /register.

POST /api/auth/change-password

Change le mot de passe de l'utilisateur courant. Incrémente token_version, ce qui invalide tous les jetons précédents, y compris celui qui vient de servir à faire l'appel.

Repositories

GET /api/repositories

Liste les dépôts accessibles par l'utilisateur courant, filtrés par la table de permissions. Un utilisateur qui n'a aucun motif de permission sur un dépôt ne le voit pas. Les rôles admin et lead voient tous les dépôts.

Query params : un seul, include_inactive (booléen, défaut false), et il n'est honoré que pour un super administrateur. Il n'y a ni limit ni offset : la route renvoie toute la liste.

Response 200 :

{
  "success": true,
  "data": [
    {
      "id": 1,
      "name": "hero-rpg",
      "description": "Main RPG project",
      "storage_path": "/var/lib/uversion/data/hero-rpg",
      "is_active": true,
      "created_at": "2026-01-15T10:00:00Z",
      "updated_at": "2026-05-15T08:30:00Z"
    },
    {
      "id": 2,
      "name": "shared-assets",
      "description": "Shared asset library",
      "storage_path": "/var/lib/uversion/data/shared-assets",
      "is_active": true,
      "created_at": "2026-02-01T09:00:00Z",
      "updated_at": "2026-05-02T11:12:00Z"
    }
  ]
}

Ce sont les seuls champs renvoyés. En particulier, il n'y a ni owner, ni current_revision, ni file_count, ni size_bytes, ni last_commit_at : un dépôt n'a pas de propriétaire au sens de l'API, et les volumétries s'obtiennent par les routes de statistiques d'administration.

GET /api/repositories/{repo_id}

Détail d'un dépôt. Même forme d'objet que dans la liste, avec les mêmes champs.

Erreurs :

  • 403 : pas d'accès à ce dépôt
  • 404 : dépôt inexistant

POST /api/repositories

Crée un dépôt. Réservé au super administrateur, c'est-à-dire au rôle admin et à lui seul. Ce n'est pas une capacité : le contrôle porte directement sur le rôle, donc un project_admin ou un lead reçoit un 403. Il n'existe aucune capacité create_repos dans le produit.

Body :

{
  "name": "new-project",
  "description": "Description facultative"
}

Validation :

  • name : 1 à 255 caractères. Aucune contrainte de jeu de caractères n'est appliquée par le serveur. Le chemin de stockage sur disque est dérivé du nom en le normalisant.
  • description : facultative.

L'unicité porte sur le nom seul, à l'échelle du serveur. Il n'y a pas de notion de propriétaire, donc pas d'unicité « par propriétaire ».

Erreurs :

  • 400 : nom vide ou au-delà de 255 caractères
  • 403 : l'appelant n'a pas le rôle admin
  • 409 : un dépôt porte déjà ce nom

Files

POST /api/files/{repo_id}/upload-chunks

Envoie un fichier entier, et non une liste de morceaux. Le nom de la route est trompeur : c'est le serveur qui découpe le fichier en blocs (les chunks), pas vous. Un appelant n'a jamais à faire ce découpage lui-même.

Les blocs identiques, reconnus par leur empreinte SHA-256, sont dédupliqués : un bloc déjà présent sur le serveur n'est pas stocké une seconde fois, quel que soit le fichier ou le dépôt d'où il vient. C'est ce qui fait qu'un gros fichier binaire modifié à la marge ne coûte presque rien en espace disque.

Body : un objet à un seul champ, contenant le fichier complet encodé en base64.

{
  "data": "<le fichier entier, encodé en base64>"
}

Toute autre forme, en particulier un tableau chunks, est rejetée par le serveur.

Response 200 :

{
  "success": true,
  "data": {
    "chunks": [
      { "hash": "abc123...", "offset": 0,       "size": 1048576, "compressed_size": 423152 },
      { "hash": "def456...", "offset": 1048576, "size": 2097152, "compressed_size": 891204 }
    ],
    "chunks_stored": 1,
    "chunks_deduplicated": 1
  }
}

Le tableau chunks est à reprendre tel quel, objets complets compris, dans le POST /commit qui suit. chunks_stored compte les blocs réellement écrits sur disque et chunks_deduplicated ceux qui existaient déjà.

Permission : l'écriture sur le dépôt est exigée, sinon 403.

Limites : corps de requête plafonné à 1 Go (réglable par security.max_body_size_files). Un fichier plus gros que cette limite ne peut pas passer par cette route.

POST /api/files/{repo_id}/commit

Crée un commit atomique à partir d'une liste de fichiers et de leurs blocs. Soit tous les fichiers passent, soit aucun.

Les trois seules valeurs acceptées pour action sont add, modify et delete

Pas added, pas modified, pas deleted.

Ce n'est pas un détail de forme. Le serveur teste littéralement action == "delete" et traite tout le reste comme un ajout ou une modification. Envoyer "deleted" ne supprime donc rien du tout : le fichier part dans la branche d'écriture, avec un tableau chunks vide, et le serveur enregistre une révision au contenu vide. Aucune erreur n'est levée. Le fichier reste présent, sa dernière version est écrasée par du vide, et la perte ne se voit qu'au prochain sync de quelqu'un d'autre.

Body :

{
  "message": "Updated main level + hero pose pass",
  "commit_hash": "optionnel : pour regrouper plusieurs lots sous un seul commit",
  "files": [
    {
      "path": "Content/Maps/MainLevel.umap",
      "action": "modify",
      "chunks": [
        { "hash": "abc123...", "offset": 0,       "size": 1048576, "compressed_size": 423152 },
        { "hash": "def456...", "offset": 1048576, "size": 2097152, "compressed_size": 891204 }
      ]
    },
    {
      "path": "Content/Characters/NewVillain.uasset",
      "action": "add",
      "chunks": [
        { "hash": "fed789...", "offset": 0, "size": 524288, "compressed_size": 201004 }
      ]
    },
    {
      "path": "Content/OldAsset.uasset",
      "action": "delete",
      "chunks": []
    }
  ]
}

chunks n'est pas facultatif, y compris sur un delete. Le champ doit être présent : l'omettre fait échouer la désérialisation de toute la requête. Pour une suppression, envoyez un tableau vide.

Le tableau contient des objets, pas des chaînes : reprenez sans les modifier les entrées {hash, offset, size, compressed_size} renvoyées par /upload-chunks. Chaque hash doit être une empreinte hexadécimale de 64 caractères, sans quoi le commit entier est refusé en 400.

Le champ commit_hash à la racine est facultatif. Il sert à faire porter plusieurs appels successifs par un seul et même commit, ce que fait le client desktop quand il découpe un envoi volumineux en lots. Omis, le serveur en calcule un.

Response 200 :

{
  "success": true,
  "data": {
    "commit_hash": "7f3a9b1c2d3e4f...",
    "files_committed": 3,
    "revisions": [
      { "path": "Content/Maps/MainLevel.umap",             "revision_number": 12 },
      { "path": "Content/Characters/NewVillain.uasset",    "revision_number": 1  },
      { "path": "Content/OldAsset.uasset",                 "revision_number": 8  }
    ]
  }
}

Ce sont les seuls champs renvoyés. Il n'y a ni files_changed, ni bytes_uploaded, ni bytes_deduped, ni revision. Notez que revision_number est un compteur par fichier, pas un numéro de version du dépôt : c'est commit_hash qui identifie le commit, et c'est lui qu'il faut conserver pour référencer un état.

Erreurs :

  • 400 : corps mal formé, empreinte de bloc invalide (attendu : 64 caractères hexadécimaux), champ chunks absent
  • 403 : pas de permission d'écriture sur un des chemins
  • 409 : verrou tenu par quelqu'un d'autre sur un fichier modifié, ou commit concurrent sur le même fichier

GET /api/files/{repo_id}/snapshot

Retourne l'état du dépôt tel qu'il était au moment d'un commit donné : la liste des fichiers présents, avec leur numéro de révision et leur taille. Utilisé par le client desktop pour le clone et pour la synchronisation forcée.

Query params :

  • commit_hash : obligatoire. C'est l'empreinte du commit qui sert de point de référence. Sans ce paramètre la requête est rejetée, et il n'existe pas de valeur par défaut « dernier état ».

Il n'y a pas de paramètre revision. Le snapshot se demande par empreinte de commit, jamais par numéro. Un commit inconnu répond 404.

Response 200 : un tableau plat, sans objet englobant.

{
  "success": true,
  "data": [
    {
      "path": "Content/Maps/MainLevel.umap",
      "revision_number": 12,
      "file_size": 84934656
    },
    {
      "path": "Content/Characters/Hero.uasset",
      "revision_number": 3,
      "file_size": 5242880
    }
  ]
}

La réponse ne contient pas les listes de blocs. Pour récupérer un contenu, passez par GET /api/files/{repo_id}/content, qui réassemble le fichier côté serveur.

GET /api/files/{repo_id}/content

Télécharge le contenu d'un fichier à une révision donnée (le serveur réassemble les chunks). Utilisé par le clone et le sync.

Query params :

  • path : chemin du fichier (relatif à la racine du repo)
  • revision (optionnel) : numéro de révision. Défaut : dernière.

Response 200 : le contenu binaire du fichier.

GET /api/files/{repo_id}/history

Historique des commits du dépôt, du plus récent au plus ancien.

Query params :

  • limit : nombre de commits, défaut 50
  • offset : défaut 0
  • path (facultatif) : ne garde que les commits ayant touché ce fichier

Ce sont les trois seuls paramètres lus. Il n'y a ni author ni since : un paramètre inconnu est ignoré en silence, ce qui donne une réponse plausible mais non filtrée. Filtrez par auteur ou par date côté appelant.

Response 200 : un tableau plat de commits.

{
  "success": true,
  "data": [
    {
      "commit_hash": "7f3a9b1c...",
      "message": "Fixed lighting in main level",
      "author": "alice",
      "created_at": "2026-05-15T08:30:00Z",
      "files": [
        {
          "path": "Content/Maps/MainLevel.umap",
          "revision_number": 12,
          "file_size": 84934656
        }
      ]
    }
  ]
}

Il n'y a ni total, ni has_more, ni rappel de limit et offset. Pour parcourir tout l'historique, incrémentez offset jusqu'à recevoir moins d'éléments que limit.

Chaque commit porte directement la liste des fichiers touchés. Une entrée dont la révision est une suppression est marquée comme telle et n'a pas de contenu à télécharger.

Sync incrémental

Endpoints utilisés par le client desktop pour synchroniser efficacement :

  • GET /api/files/{repo_id}/sync : fichiers changés depuis une révision, pour un sync delta.
  • GET /api/files/{repo_id}/deletions : fichiers supprimés côté serveur, pour propager les suppressions en local.
  • GET /api/files/{repo_id}/list : liste des fichiers du repo.
  • POST /api/files/{repo_id}/checkout : acquiert les locks et prépare l'édition.

Locks

Les verrous n'expirent jamais

Un verrou tient jusqu'à ce qu'il soit explicitement relâché : par un checkin, par un revert, ou par un déverrouillage forcé d'administrateur. Il n'y a aucune expiration automatique, ni au bout d'une heure, ni au bout d'un mois.

Le champ expires_at existe uniquement parce que la colonne correspondante en base n'accepte pas de valeur vide. Le serveur y écrit une valeur sentinelle à cent ans : un verrou pris aujourd'hui affiche une échéance située aux alentours de 2126. Ne construisez rien sur ce champ, et n'affichez pas cette date à un utilisateur.

Le heartbeat ne prolonge donc rien. C'est un signal de supervision, qui sert seulement à montrer aux administrateurs quels verrous sont encore activement utilisés.

POST /api/locks/{repo_id}/acquire

Acquiert des verrous sur une liste de chemins. Les acquisitions sont indépendantes : la liste acquired contient les succès, la liste failed les échecs avec leur motif.

Body :

{
  "paths": [
    "Content/Maps/MainLevel.umap",
    "Content/Characters/Hero.uasset"
  ]
}

Response 200 :

{
  "success": true,
  "data": {
    "acquired": [
      {
        "id": "550e8400-e29b-41d4-a716-446655440000",
        "file_id": 12345,
        "file_path": "Content/Maps/MainLevel.umap",
        "user_id": 12,
        "username": "alice",
        "acquired_at": "2026-05-15T14:30:00Z",
        "expires_at": "2126-05-15T14:30:00Z"
      }
    ],
    "failed": [
      {
        "path": "Content/Characters/Hero.uasset",
        "reason": "File is locked",
        "locked_by": "bob"
      }
    ]
  }
}

L'échéance à 2126 dans cet exemple n'est pas une coquille : c'est la valeur sentinelle décrite plus haut. Le verrou est permanent.

Motifs d'échec. Le champ reason est une phrase en anglais destinée à être affichée, pas un code stable. N'écrivez pas de logique qui compare cette chaîne, et ne « parsez » pas son préfixe : elle peut être reformulée d'une version à l'autre. Les valeurs actuellement produites sont :

reasonSignificationlocked_by
File is lockedQuelqu'un d'autre détient déjà le verrouLe nom de la personne
No write permissionPas de droit d'écriture sur ce cheminnull
Failed to create lockLa pose du verrou a échoué en basenull
Database errorErreur de base de données sur ce cheminnull

Un chemin invalide (absolu, contenant .., vide, au-delà de 4096 caractères ou porteur d'un octet nul) ne produit pas une entrée dans failed : il fait échouer toute la requête.

POST /api/locks/{repo_id}/release

Relâche des verrous que vous détenez. Body :

{
  "paths": ["Content/Maps/MainLevel.umap"]
}

Il n'y a pas de champ force sur cette route. En ajouter un est sans effet : le serveur ignore les champs qu'il ne connaît pas, la requête réussit, et le verrou d'autrui reste en place. Un intégrateur qui compte dessus croit avoir libéré le fichier alors que non.

Pour retirer le verrou de quelqu'un d'autre, la seule route est POST /api/admin/locks/{lock_id}/force-release. Elle est réservée à l'administration du dépôt concerné, et l'opération est inscrite au journal d'audit. Elle prend l'identifiant du verrou, que vous obtenez par GET /api/locks/{repo_id}/status.

POST /api/locks/{repo_id}/heartbeat

Met à jour l'horodatage d'activité de vos verrous. Cela ne prolonge rien, puisque rien n'expire : c'est un signal de supervision, qui permet à un administrateur de distinguer un verrou encore utilisé d'un verrou oublié.

Body : un tableau JSON de chemins, directement, sans objet englobant.

["Content/Maps/MainLevel.umap", "Content/Characters/Hero.uasset"]

Les chemins invalides sont ignorés individuellement plutôt que de faire échouer tout le lot, pour qu'un reliquat de suivi obsolète ne bloque pas les autres.

GET /api/locks/{repo_id}/status

Liste tous les verrous du dépôt, avec le nom du fichier et celui de la personne qui le détient.

Query params : aucun. Il n'y a ni filtre user, ni limit, ni offset. La route renvoie la totalité des verrous du dépôt, à filtrer côté appelant.

Response 200 : un tableau plat, sans objet englobant ni total.

{
  "success": true,
  "data": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "file_id": 12345,
      "file_path": "Content/Maps/MainLevel.umap",
      "user_id": 12,
      "username": "alice",
      "acquired_at": "2026-05-15T14:30:00Z",
      "expires_at": "2126-05-15T14:30:00Z"
    }
  ]
}

Le champ id est celui à passer à POST /api/admin/locks/{lock_id}/force-release.

Comments & Reviews

GET /api/comments/{repo_id}/comments?commit_hash=<hash>

Liste les commentaires d'un commit, threadés (parent → enfants).

Response 200 :

{
  "success": true,
  "data": [
    {
      "id": 42,
      "commit_hash": "7f3a9b1c...",
      "file_path": "Content/Maps/MainLevel.umap",
      "author_id": 12,
      "author_username": "alice",
      "body": "LGTM, ship it",
      "parent_id": null,
      "created_at": "2026-05-15T15:00:00Z",
      "replies": [
        {
          "id": 43,
          "author_username": "bob",
          "body": "Thanks!",
          "parent_id": 42,
          "created_at": "2026-05-15T15:05:00Z"
        }
      ]
    }
  ]
}

POST /api/comments/{repo_id}/comments

Crée un commentaire. file_path est optionnel (commentaire de commit vs. de fichier). parent_id pour répondre à un thread existant.

Body :

{
  "commit_hash": "7f3a9b1c...",
  "file_path": "Content/Maps/MainLevel.umap",
  "body": "LGTM, ship it",
  "parent_id": null
}

POST /api/comments/{repo_id}/reviews

Submit un review sur un commit. Capability approve_changes requise.

Body :

{
  "commit_hash": "7f3a9b1c...",
  "status": "approved",
  "comment": "Lighting looks great, approving"
}

Status accepté : approved, changes_requested, pending.

Watchlist

GET /api/watchlist/{repo_id}/watchlist

Liste les patterns de watch de l'utilisateur courant sur ce repository.

Response 200 :

{
  "success": true,
  "data": [
    {
      "id": 1,
      "user_id": 12,
      "pattern": "Content/Characters/Hero/**",
      "notify_on": ["commit", "lock"],
      "created_at": "2026-04-10T10:00:00Z"
    }
  ]
}

POST /api/watchlist/{repo_id}/watchlist

Ajoute un pattern de watch. Body :

{
  "pattern": "Content/Characters/Hero/**",
  "notify_on": ["commit", "lock"]
}

Les événements possibles : commit (un commit a modifié un fichier matchant), lock (un lock a été acquis), review (un review a été posté).

DELETE /api/watchlist/{repo_id}/watchlist/{watch_id}

Retire un pattern de watch. Retourne { "success": true }.

Production board (tasks)

L'ancienne API modification-requests a été retirée : le board de production l'absorbe (les demandes deviennent des cartes, origin='request'). Les endpoints sont nichés sous /api/tasks/{repo_id}.

RouteDescription
GET /api/tasks/{repo_id}/boardBoard complet : colonnes + cartes
POST /api/tasks/{repo_id}/tasksCrée une carte
PUT/DELETE /api/tasks/{repo_id}/tasks/{task_id}Met à jour / supprime une carte
PUT /api/tasks/{repo_id}/tasks/{task_id}/assigneesAssigne des utilisateurs à une carte
GET /api/tasks/{repo_id}/assignable-usersListe des utilisateurs assignables
POST /api/tasks/{repo_id}/columnsConfigure les colonnes du board

Sous-routes également disponibles : commentaires de carte, liens vers assets et commits, et pièces jointes (avec image de couverture). La configuration des colonnes requiert la capability manage_board (admin / lead).

Builds

POST /api/builds/{repo_id}/upload?hash=<sha256>

Upload un fichier de build vers <storage_path>/builds/<hash>. Le serveur vérifie le SHA-256 fourni en query param contre le contenu reçu. Si le hash existe déjà côté serveur, retourne 200 immédiatement sans recopier (dédup côté build storage).

Headers : Content-Type: application/octet-stream.

Body : binaire brut (pas de JSON wrapping).

Limites : 8 GB par fichier.

Erreurs :

  • 400 : hash query param manquant ou malformé, ou SHA-256 calculé != hash fourni
  • 413 : fichier > 8 GB

POST /api/builds/{repo_id}/publish

Enregistre le manifest d'un build après upload de tous ses fichiers. Capability publish_builds requise.

Body :

{
  "version": "0.1.5-nightly",
  "config": "Development",
  "platform": "Win64",
  "executable_path": "HeroRPG/Binaries/Win64/HeroRPG.exe",
  "release_notes": "Nightly build of main branch, 2026-05-15",
  "files": [
    { "path": "HeroRPG.exe",                "hash": "abc123...", "size_bytes": 12345678 },
    { "path": "HeroRPG/Content/Paks/pak0.pak", "hash": "def456...", "size_bytes": 2147483648 }
  ]
}

GET /api/builds/{repo_id}

Liste les builds publiés de ce projet.

L'accès aux builds se donne projet par projet. La capacité download_builds ne suffit pas : étant serveur-entière, elle dit seulement que le compte n'est pas un simple spectateur. Il faut en plus soit un accès au dépôt, soit une autorisation explicite sur les builds de ce projet, accordée par son administrateur via /api/admin/repositories/{repo_id}/build-access. Un administrateur de projet accède d'office à ceux qu'il administre, un super administrateur à tous.

Response 200 :

{
  "success": true,
  "data": [
    {
      "id": 5,
      "version": "0.1.5-nightly",
      "config": "Development",
      "platform": "Win64",
      "published_by": "ci-nightly",
      "published_at": "2026-05-15T03:00:00Z",
      "size_bytes": 2159829326,
      "file_count": 142,
      "release_notes": "Nightly build of main branch, 2026-05-15"
    }
  ]
}

GET /api/builds/{repo_id}/{build_id}/file

Télécharge un fichier d'un build. Capability download_builds requise. Le manifest d'un build est disponible via GET /api/builds/{repo_id}/{build_id}/manifest.

Admin

Ce qui garde réellement ces routes

Contrairement à ce que l'on pourrait attendre, les routes /api/admin/* ne sont pas gardées par une capacité chacune. Elles passent par l'un de quatre contrôles, qui portent presque tous sur le rôle. Une « capacité » (capability) est un droit nommé attaché à un rôle ; le point important est qu'elle est valable sur tout le serveur, car elle ne porte aucune référence à un dépôt. Elle ne peut donc jamais servir à cloisonner quelqu'un sur un projet.

ContrôlePasse pourÀ quoi il sert
Super administrateur Le rôle admin, et lui seul Tout ce qui vaut pour le serveur entier : comptes, groupes, journal d'audit, statistiques globales, licence, mise à jour du serveur.
Administrateur de ce dépôt admin, ou un project_admin qui administre ce dépôt précis Tout ce qui est propre à un projet : permissions, règles de validation, webhooks, accès aux builds, déverrouillage forcé, ramasse-miettes.
Capacité serveur-entière Tout rôle qui détient la capacité nommée Quelques routes transverses. Attention : la capacité vaut sur tous les dépôts, c'est sa nature. Un project_admin, qui n'en détient aucune, est admis à la place sur les seuls dépôts qu'il administre.
Admission seule admin ou project_admin Laisse entrer, mais n'autorise rien. La route qui l'emploie doit ensuite restreindre elle-même ses résultats aux dépôts administrés par l'appelant.

Autrement dit : les capacités manage_users et manage_permissions n'existent que sur le papier. Elles sont bien créées en base au moment de l'installation, mais aucune ligne de code ne les interroge. Les accorder à un rôle ne change rien du tout. N'écrivez pas d'intégration qui suppose qu'un compte non-admin pourra gérer des utilisateurs parce qu'on lui aura donné manage_users : il recevra un 403.

RouteContrôle réelDescription
GET /api/admin/usersAdmission seule, puis filtrageUn project_admin ne voit que les comptes de son périmètre
POST /api/admin/usersAdmission seuleCrée un compte
PUT/DELETE /api/admin/users/{id}Super administrateurMet à jour ou supprime un compte
POST /api/admin/users/{id}/reset-passwordSuper administrateurRéinitialise le mot de passe et révoque tous les jetons du compte
GET/POST /api/admin/groupsSuper administrateurLes groupes sont globaux, ils ne peuvent pas être délégués par projet
GET/POST /api/admin/permissions/{repo_id}Administrateur de ce dépôtRègles de permission par motif de chemin, accordées à un groupe ou à un utilisateur
GET/POST /api/admin/repositories/{repo_id}/rulesAdministrateur de ce dépôtRègles de validation appliquées avant l'envoi
GET/POST /api/admin/repositories/{repo_id}/webhooksAdministrateur de ce dépôtDiscord, Slack, Teams, ou webhook générique
GET /api/admin/locksAdmission seule, puis filtrageVerrous, restreints aux dépôts administrés
POST /api/admin/locks/{lock_id}/force-releaseAdministrateur de ce dépôtRetire le verrou de quelqu'un d'autre. Inscrit au journal d'audit
GET /api/admin/auditSuper administrateurJournal d'audit, filtrable et paginé
GET /api/admin/statsSuper administrateurVolumétrie de stockage, taux de déduplication, croissance
GET /api/admin/licenceSuper administrateurÉtat de la licence et sièges consommés
GET /api/admin/repositories/{repo_id}/gc/previewAdministrateur de ce dépôtSimule le ramasse-miettes sans rien supprimer
POST /api/admin/repositories/{repo_id}/gcAdministrateur de ce dépôtLance le ramasse-miettes

Le détail complet des rôles, de leurs rangs et de ce que chacun peut faire est dans la page consacrée aux rôles et permissions.

Pans d'API non couverts par cette page

Les familles de routes suivantes existent, sont montées et servies par le serveur, mais ne sont pas décrites ici. Si votre intégration en a besoin, le plus fiable aujourd'hui est d'observer les appels que fait le client desktop, ou de nous écrire.

PréfixeCe que ça couvre
/api/advisor/*Project Health : l'audit de projet Unreal, ses constats, le score par pilier et le tri des éléments à ignorer.
/api/distribution/*Distribution entre projets : liens entre un dépôt source et un dépôt cible, publication de fichiers d'un projet vers un autre, historique.
/api/binaries/*Binaires d'éditeur précompilés, associés à un commit, sur le modèle d'UnrealGameSync.
/api/watchlist/*Au-delà des motifs de surveillance décrits plus haut : les notifications et la boîte de réception.
/api/profile/*Profil de l'utilisateur courant.
/api/admin/repositories/{repo_id}/adminsQui administre un dépôt : désignation et retrait des administrateurs de projet.
/api/admin/repositories/{repo_id}/build-accessQui peut télécharger les builds de ce projet, accordé projet par projet.
/api/admin/licenceÉtat de la licence et sièges consommés.
/api/admin/server/*Mise à jour du serveur depuis le panneau d'administration.

Health

GET /health

Endpoint non authentifié retournant 200 OK si le serveur peut accéder à la base. Utilisé pour les health checks de load balancer ou de monitoring (Prometheus blackbox, Datadog synthetic, etc.).

Response 200 : OK (text/plain).

Response 503 : si la base est injoignable.

GET /api/server-info

Endpoint public non authentifié retournant les métadonnées du serveur (version, etc.). Comme /health, il n'est pas protégé par le middleware d'auth.

Error format

En cas d'erreur, une route authentifiée répond { "success": false, "error": "..." }, et une route /api/auth/* répond { "error": "..." }, dans les deux cas avec un code HTTP adapté.

Le champ error est un message destiné à un humain, pas un code. Il n'existe aucun catalogue de codes d'erreur stables, ni dans le corps, ni dans un en-tête. Ne construisez donc pas de logique sur son contenu, et n'en analysez pas le préfixe : ces phrases sont reformulées d'une version à l'autre, et un test de chaîne qui casse en silence est pire que pas de test du tout.

Le code HTTP est la seule chose sur laquelle brancher un comportement. Le tableau ci-dessous en donne la lecture.

HTTPSignificationQuand
400Bad requestParamètre invalide, JSON malformé, body trop gros (avant la limite hard 413)
401Not authenticatedToken absent, expiré, signature invalide, ou token_version mismatch
403Permission deniedCapability manquante, ou pas de permission sur le path / repo
404Resource not foundRepo / fichier / commit / user inexistant ou inaccessible
409ConflictLock déjà tenu, commit en concurrence, contrainte unique violée
413Payload too largeBody au-delà de la limite (1 GB sur /files, 8 GB sur /builds/upload)
429Too many requestsUniquement sur /auth/login (rate limiting per-user)
500Internal server errorDB error, IO error. Loggé côté serveur, joindre le timestamp si vous reportez le bug.
503Service unavailableBase injoignable (health check) ou migration en cours