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
| Header | Valeur |
|---|---|
Authorization | Bearer <jwt> sur toutes les routes /api/* sauf /api/auth/* et /api/server-info (et /health) |
Content-Type | application/json pour les POST/PUT avec body JSON. application/octet-stream pour les uploads binaires (build files). |
Accept | application/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.
| Route | Paramètres acceptés | Forme 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é, outoken_versionobsolè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ôt404: 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ères403: l'appelant n'a pas le rôleadmin409: 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.
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), champchunksabsent403: pas de permission d'écriture sur un des chemins409: 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 50offset: défaut 0path(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
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 :
reason | Signification | locked_by |
|---|---|---|
File is locked | Quelqu'un d'autre détient déjà le verrou | Le nom de la personne |
No write permission | Pas de droit d'écriture sur ce chemin | null |
Failed to create lock | La pose du verrou a échoué en base | null |
Database error | Erreur de base de données sur ce chemin | null |
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}.
| Route | Description |
|---|---|
GET /api/tasks/{repo_id}/board | Board complet : colonnes + cartes |
POST /api/tasks/{repo_id}/tasks | Cré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}/assignees | Assigne des utilisateurs à une carte |
GET /api/tasks/{repo_id}/assignable-users | Liste des utilisateurs assignables |
POST /api/tasks/{repo_id}/columns | Configure 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 fourni413: 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ôle | Passe 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.
| Route | Contrôle réel | Description |
|---|---|---|
GET /api/admin/users | Admission seule, puis filtrage | Un project_admin ne voit que les comptes de son périmètre |
POST /api/admin/users | Admission seule | Crée un compte |
PUT/DELETE /api/admin/users/{id} | Super administrateur | Met à jour ou supprime un compte |
POST /api/admin/users/{id}/reset-password | Super administrateur | Réinitialise le mot de passe et révoque tous les jetons du compte |
GET/POST /api/admin/groups | Super administrateur | Les 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ôt | Règles de permission par motif de chemin, accordées à un groupe ou à un utilisateur |
GET/POST /api/admin/repositories/{repo_id}/rules | Administrateur de ce dépôt | Règles de validation appliquées avant l'envoi |
GET/POST /api/admin/repositories/{repo_id}/webhooks | Administrateur de ce dépôt | Discord, Slack, Teams, ou webhook générique |
GET /api/admin/locks | Admission seule, puis filtrage | Verrous, restreints aux dépôts administrés |
POST /api/admin/locks/{lock_id}/force-release | Administrateur de ce dépôt | Retire le verrou de quelqu'un d'autre. Inscrit au journal d'audit |
GET /api/admin/audit | Super administrateur | Journal d'audit, filtrable et paginé |
GET /api/admin/stats | Super administrateur | Volumétrie de stockage, taux de déduplication, croissance |
GET /api/admin/licence | Super administrateur | État de la licence et sièges consommés |
GET /api/admin/repositories/{repo_id}/gc/preview | Administrateur de ce dépôt | Simule le ramasse-miettes sans rien supprimer |
POST /api/admin/repositories/{repo_id}/gc | Administrateur de ce dépôt | Lance 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éfixe | Ce 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}/admins | Qui administre un dépôt : désignation et retrait des administrateurs de projet. |
/api/admin/repositories/{repo_id}/build-access | Qui 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.
| HTTP | Signification | Quand |
|---|---|---|
| 400 | Bad request | Paramètre invalide, JSON malformé, body trop gros (avant la limite hard 413) |
| 401 | Not authenticated | Token absent, expiré, signature invalide, ou token_version mismatch |
| 403 | Permission denied | Capability manquante, ou pas de permission sur le path / repo |
| 404 | Resource not found | Repo / fichier / commit / user inexistant ou inaccessible |
| 409 | Conflict | Lock déjà tenu, commit en concurrence, contrainte unique violée |
| 413 | Payload too large | Body au-delà de la limite (1 GB sur /files, 8 GB sur /builds/upload) |
| 429 | Too many requests | Uniquement sur /auth/login (rate limiting per-user) |
| 500 | Internal server error | DB error, IO error. Loggé côté serveur, joindre le timestamp si vous reportez le bug. |
| 503 | Service unavailable | Base injoignable (health check) ou migration en cours |