Wiki
REST API
Referencia de los endpoints HTTP del servidor uVersion: autenticación, repositorios, archivos, bloqueos, comentarios, lista de seguimiento, tablero de producción, builds, administración.
El servidor uVersion expone una API HTTP JSON. Las llamadas se autentican con un
JWT (JSON Web Token), es decir, el token de sesión que el servidor te entrega
en el momento de iniciar sesión y que luego devuelves en cada solicitud. Esta página documenta los
endpoints usados por el cliente de escritorio, los plugins del editor y la CLI uversion.
Puedes llamarlos directamente para integrar uVersion en tu propio instrumental interno
(panel propio, scripts de auditoría, webhooks, etc.).
No cubre la totalidad de la API: existen varias familias de rutas que no se describen aquí. La lista está en la sección Áreas no cubiertas.
Convenciones
URL base
Toda ruta documentada es relativa a la URL de tu instancia. Los ejemplos usan
https://uversion.mygamestudio.com. Sustitúyela por la tuya.
Encabezados obligatorios
| Encabezado | Valor |
|---|---|
Authorization | Bearer <jwt> en toda ruta /api/* salvo /api/auth/* y /api/server-info (y /health) |
Content-Type | application/json para POST/PUT con cuerpo JSON. application/octet-stream para subidas binarias (archivos de build). |
Accept | application/json recomendado (el servidor devuelve JSON por defecto) |
Dos formatos de respuesta, y hay que saber cuál estás leyendo
El servidor no tiene un formato de respuesta sino dos, y confundirlos es el error más costoso para quien inicia una integración.
1. Las rutas autenticadas (todo /api/* salvo /api/auth/*)
responden con una envoltura:
{
"success": true,
"data": { /* payload */ }
}
En caso de error, esas mismas rutas responden:
{
"success": false,
"error": "No write permission on this repository"
}
El campo success siempre está presente. data está presente en caso de éxito,
error en caso de fallo, nunca ambos a la vez.
2. Las rutas de autenticación (/api/auth/login,
/refresh, /validate, /logout, /register,
/change-password) no usan esta envoltura. Devuelven
el objeto pelado, sin success ni data:
{
"token": "eyJhbGciOiJIUzI1NiIs...",
"user": { "id": 12, "username": "alice", ... },
"must_change_password": false
}
Y sus errores son un objeto de un solo campo, sin success:
{ "error": "Invalid credentials" }
Consecuencia práctica: en /api/auth/login, el token se lee en .token
y no en .data.token. Un script que consulta .data.token recibe
null sin ningún error visible.
Por último, GET /api/files/{repo_id}/content no devuelve ni lo uno ni lo otro: es el
contenido binario del archivo, tal cual.
Paginación
No hay una convención de paginación común: cada ruta tiene la suya, o
no tiene ninguna. No des por hecho ni limit, ni total, ni has_more.
| Ruta | Parámetros aceptados | Forma de la respuesta |
|---|---|---|
GET /api/files/{repo_id}/history |
limit (por defecto 50), offset (por defecto 0), path |
Arreglo plano de commits. Sin total, sin has_more: has llegado al final cuando el arreglo contiene menos elementos que limit. |
GET /api/repositories |
solo include_inactive (y solo se respeta para un superadministrador) |
Arreglo plano. Ni limit ni offset se leen. |
GET /api/locks/{repo_id}/status |
Ninguno | Arreglo plano de todos los bloqueos del repositorio. |
GET /api/files/{repo_id}/snapshot |
commit_hash, obligatorio |
Arreglo plano de archivos. |
GET /api/admin/audit |
page y per_page, no limit/offset, más filtros (user_id, action, entity_type, from, to) |
Reservada al superadministrador. Ilustra bien la ausencia de convención común: es la única ruta que pagina por número de página. |
Para cualquier ruta no listada aquí, considera que devuelve la totalidad de su resultado en una sola llamada.
Limitación de tasa
No hay una limitación de tasa general: un proyecto Unreal cuenta con miles de archivos y las operaciones por lotes (adquirir, liberar bloqueos) dispararían un estrangulamiento constantemente. Por lo tanto puedes llamar a la API en volumen.
Una sola ruta está limitada, /api/auth/login:
5 intentos fallidos por nombre de usuario cada 15 minutos. Los inicios de sesión
exitosos no se cuentan.
Revocación de tokens
Cada token de sesión lleva un campo tv (token version), que refleja la columna
users.token_version en la base de datos. El servidor compara ambos en cada solicitud.
Una llamada a POST /api/auth/logout, un restablecimiento de contraseña por un
administrador o la desactivación de una cuenta incrementan este valor, lo que invalida
instantáneamente todos los tokens existentes de ese usuario, en todas sus
máquinas.
Curl
Para llamar a la API desde una terminal. Fíjate en el .token: la respuesta de
/api/auth/login es el objeto pelado, no hay ningún .data que atravesar.
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')
# Las rutas autenticadas, en cambio, sí usan la envoltura:
curl -s -H "Authorization: Bearer $TOKEN" \
https://uversion.mygamestudio.com/api/repositories | jq '.data'
Autenticación
Recordatorio: ninguna ruta de esta sección usa la envoltura
{"success", "data"}. Devuelven el objeto pelado, y sus errores tienen la forma
{"error": "..."}.
POST /api/auth/login
Autentica a un usuario y devuelve un token de sesión válido durante 30 días.
Body:
{
"username": "alice",
"password": "secret"
}
Response 200:
{
"token": "eyJhbGciOiJIUzI1NiIs...",
"user": {
"id": 12,
"username": "alice",
"email": "alice@mygamestudio.com",
"role": "lead"
},
"must_change_password": false
}
No hay campo expires_at: la duración de validez se lee en
el propio token, o se deduce de la configuración del servidor (30 días por defecto).
No ignores must_change_password. Este campo vale true
cuando la cuenta aún funciona con una contraseña temporal: la que el instalador generó
para la cuenta de administrador inicial, o la que un administrador acaba de poner en un
restablecimiento. Un cliente que no lo mira deja al usuario en esa contraseña provisional
indefinidamente. El comportamiento esperado es redirigir de inmediato a
POST /api/auth/change-password antes de cualquier otra acción.
Errores:
401: credenciales inválidas o cuenta desactivada429: 5 intentos fallidos alcanzados para este nombre de usuario en la ventana de 15 minutos
El servidor ejecuta siempre la verificación Argon2id de la contraseña, incluso contra una huella ficticia cuando la cuenta no existe. La respuesta tarda por tanto lo mismo en ambos casos, lo que impide adivinar la existencia de una cuenta cronometrando las solicitudes.
POST /api/auth/refresh
Renueva el token de sesión sin volver a pasar por la contraseña.
Headers: Authorization: Bearer <token_actual>. Sin body.
Response 200: exactamente la misma forma que /login
(token, user, must_change_password), con un token nuevo.
Errores:
401: token inválido, expirado, cuenta desactivada otoken_versionobsoleto
POST /api/auth/logout
Invalida todos los tokens del usuario actual, en todas sus máquinas,
incrementando users.token_version. Deberá volver a iniciar sesión en todas partes: cliente de escritorio,
plugin del editor y CLI incluidos.
Response 200: { "logged_out": true }.
POST /api/auth/validate
Comprueba que un token sigue siendo válido y devuelve el usuario al que corresponde. La comprobación
abarca también is_active y token_version, así que un token revocado se
rechaza aunque aún no haya expirado.
Cuidado con el body: no es un objeto, es una cadena JSON pelada, es decir, el token rodeado de comillas dobles.
curl -X POST https://uversion.mygamestudio.com/api/auth/validate \
-H "Content-Type: application/json" \
-d '"eyJhbGciOiJIUzI1NiIs..."'
Response 200: un objeto de usuario pelado.
{
"id": 12,
"username": "alice",
"email": "alice@mygamestudio.com",
"role": "lead"
}
No hay valid, ni expires_at, ni refreshed_token: la
validez se lee en el código HTTP (200 o 401), y la renovación pasa por
/api/auth/refresh, nunca por esta ruta.
POST /api/auth/register
Esta ruta rechaza por defecto. El registro abierto está desactivado salvo que
el operador lo haya activado explícitamente en la configuración del servidor; de lo contrario la respuesta es
403 con {"error": "Open registration is disabled; contact your administrator"}.
En el funcionamiento normal, las cuentas se crean por la administración:
POST /api/admin/users, o la pestaña Usuarios del panel de administración. No
construyas una integración que dependa de /register.
POST /api/auth/change-password
Cambia la contraseña del usuario actual. Incrementa token_version, lo que
invalida todos los tokens anteriores, incluido el que acaba de servir para hacer la llamada.
Repositorios
GET /api/repositories
Lista los repositorios accesibles por el usuario actual, filtrados por la tabla de permisos. Un
usuario que no tiene ninguna regla de permiso sobre un repositorio no lo ve. Los roles
admin y lead ven todos los repositorios.
Query params: uno solo, include_inactive (booleano, por defecto
false), y solo se respeta para un superadministrador. No hay
ni limit ni offset: la ruta devuelve toda la lista.
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"
}
]
}
Estos son los únicos campos devueltos. En particular, no hay ni owner, ni
current_revision, ni file_count, ni size_bytes, ni
last_commit_at: un repositorio no tiene propietario en el sentido de la API, y las
volumetrías se obtienen por las rutas de estadísticas de administración.
GET /api/repositories/{repo_id}
Detalle de un repositorio. Misma forma de objeto que en la lista, con los mismos campos.
Errores:
403: sin acceso a este repositorio404: repositorio inexistente
POST /api/repositories
Crea un repositorio. Reservado al superadministrador, es decir, al rol
admin y solo a él. No es una capacidad: el control recae directamente sobre el
rol, así que un project_admin o un lead recibe un 403. No
existe ninguna capacidad create_repos en el producto.
Body:
{
"name": "new-project",
"description": "Descripción opcional"
}
Validación:
name: 1 a 255 caracteres. El servidor no aplica ninguna restricción de juego de caracteres. La ruta de almacenamiento en disco se deriva del nombre normalizándolo.description: opcional.
La unicidad recae sobre el nombre solo, a escala del servidor. No hay noción de propietario, así que no hay unicidad «por propietario».
Errores:
400: nombre vacío o más allá de 255 caracteres403: el llamante no tiene el roladmin409: un repositorio ya lleva este nombre
Archivos
POST /api/files/{repo_id}/upload-chunks
Envía un archivo entero, y no una lista de trozos. El nombre de la ruta es engañoso: es el servidor quien divide el archivo en bloques (los chunks), no tú. Un llamante nunca tiene que hacer esa división él mismo.
Los bloques idénticos, reconocidos por su huella SHA-256, se deduplican: un bloque ya presente en el servidor no se almacena una segunda vez, sea cual sea el archivo o el repositorio del que provenga. Es lo que hace que un archivo binario grande modificado al margen no cueste casi nada en espacio de disco.
Body: un objeto de un solo campo, que contiene el archivo completo codificado en base64.
{
"data": "<el archivo entero, codificado en base64>"
}
Cualquier otra forma, en particular un arreglo chunks, es rechazada por el servidor.
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
}
}
El arreglo chunks hay que retomarlo tal cual, objetos completos incluidos,
en el POST /commit que sigue. chunks_stored cuenta los bloques realmente
escritos en disco y chunks_deduplicated los que ya existían.
Permiso: se exige escritura sobre el repositorio, de lo contrario 403.
Límites: cuerpo de solicitud limitado a 1 GB (ajustable con
security.max_body_size_files). Un archivo más grande que este límite no puede
pasar por esta ruta.
POST /api/files/{repo_id}/commit
Crea un commit atómico a partir de una lista de archivos y sus bloques. O pasan todos los archivos, o ninguno.
action son add, modify y delete
No added, no modified, no deleted.
No es un detalle de forma. El servidor prueba literalmente
action == "delete" y trata todo lo demás como una adición o una
modificación. Enviar "deleted" no elimina, por tanto, nada en absoluto: el archivo se va a
la rama de escritura, con un arreglo chunks vacío, y el servidor registra una
revisión de contenido vacío. No se lanza ningún error. El archivo permanece presente, su última
versión queda sobrescrita por el vacío, y la pérdida solo se ve en el próximo sync de
otra persona.
Body:
{
"message": "Updated main level + hero pose pass",
"commit_hash": "opcional: para agrupar varios lotes bajo un solo 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 no es opcional, incluso en un delete.
El campo debe estar presente: omitirlo hace fallar la deserialización de toda la solicitud. Para
una eliminación, envía un arreglo vacío.
El arreglo contiene objetos, no cadenas: retoma sin modificar las
entradas {hash, offset, size, compressed_size} devueltas por
/upload-chunks. Cada hash debe ser una huella hexadecimal de 64
caracteres, de lo contrario el commit entero se rechaza con 400.
El campo commit_hash en la raíz es opcional. Sirve para que varias llamadas
sucesivas queden bajo un mismo commit, lo que hace el cliente de escritorio cuando divide un
envío voluminoso en lotes. Omitido, el servidor calcula uno.
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 }
]
}
}
Estos son los únicos campos devueltos. No hay ni files_changed, ni
bytes_uploaded, ni bytes_deduped, ni revision. Fíjate en que
revision_number es un contador por archivo, no un número de
versión del repositorio: es commit_hash quien identifica el commit, y es él lo que hay que
conservar para referenciar un estado.
Errores:
400: cuerpo mal formado, huella de bloque inválida (esperado: 64 caracteres hexadecimales), campochunksausente403: sin permiso de escritura sobre alguna de las rutas409: bloqueo en manos de otra persona sobre un archivo modificado, o commit concurrente sobre el mismo archivo
GET /api/files/{repo_id}/snapshot
Devuelve el estado del repositorio tal como estaba en el momento de un commit dado: la lista de archivos presentes, con su número de revisión y su tamaño. Usado por el cliente de escritorio para el clon y para la sincronización forzada.
Query params:
-
commit_hash: obligatorio. Es la huella del commit que sirve de punto de referencia. Sin este parámetro la solicitud se rechaza, y no existe un valor por defecto «último estado».
No hay parámetro revision. El snapshot se pide por
huella de commit, nunca por número. Un commit desconocido responde 404.
Response 200: un arreglo plano, sin objeto envolvente.
{
"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 respuesta no contiene las listas de bloques. Para recuperar un contenido, pasa
por GET /api/files/{repo_id}/content, que reensambla el archivo del lado del servidor.
GET /api/files/{repo_id}/content
Descarga el contenido de un archivo en una revisión dada (el servidor reensambla los chunks). Usado por el clon y el sync.
Query params:
path: ruta del archivo (relativa a la raíz del repo)revision(opcional): número de revisión. Por defecto: la última.
Response 200: el contenido binario del archivo.
GET /api/files/{repo_id}/history
Historial de commits del repositorio, del más reciente al más antiguo.
Query params:
limit: número de commits, por defecto 50offset: por defecto 0path(opcional): conserva solo los commits que tocaron este archivo
Estos son los tres únicos parámetros leídos. No hay ni author ni
since: un parámetro desconocido se ignora en silencio, lo que da una respuesta
plausible pero no filtrada. Filtra por autor o por fecha del lado del llamante.
Response 200: un arreglo plano 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
}
]
}
]
}
No hay ni total, ni has_more, ni recordatorio de limit y
offset. Para recorrer todo el historial, incrementa offset hasta
recibir menos elementos que limit.
Cada commit lleva directamente la lista de archivos tocados. Una entrada cuya revisión es una eliminación se marca como tal y no tiene contenido que descargar.
Sync incremental
Endpoints usados por el cliente de escritorio para sincronizar de forma eficiente:
GET /api/files/{repo_id}/sync: archivos cambiados desde una revisión, para un sync delta.GET /api/files/{repo_id}/deletions: archivos eliminados del lado del servidor, para propagar las eliminaciones en local.GET /api/files/{repo_id}/list: lista de los archivos del repo.POST /api/files/{repo_id}/checkout: adquiere los bloqueos y prepara la edición.
Bloqueos
Un bloqueo se mantiene hasta que se libera explícitamente: por un checkin, por un revert, o por un desbloqueo forzado de administrador. No hay ninguna expiración automática, ni al cabo de una hora, ni al cabo de un mes.
El campo expires_at existe únicamente porque la columna correspondiente en la base
no acepta un valor vacío. El servidor escribe en él un valor centinela a cien años: un bloqueo
tomado hoy muestra un vencimiento situado alrededor de 2126. No construyas nada sobre este
campo, y no muestres esta fecha a un usuario.
El heartbeat no prolonga, pues, nada. Es una señal de supervisión, cuyo único fin es mostrar a los administradores qué bloqueos siguen usándose activamente.
POST /api/locks/{repo_id}/acquire
Adquiere bloqueos sobre una lista de rutas. Las adquisiciones son independientes: la lista
acquired contiene los éxitos, la lista failed los fallos con su motivo.
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"
}
]
}
}
El vencimiento en 2126 de este ejemplo no es una errata: es el valor centinela descrito más arriba. El bloqueo es permanente.
Motivos de fallo. El campo reason es una frase en inglés
destinada a mostrarse, no un código estable. No escribas lógica que compare esta
cadena, y no «parsees» su prefijo: puede reformularse de una versión a otra.
Los valores producidos actualmente son:
reason | Significado | locked_by |
|---|---|---|
File is locked | Otra persona ya tiene el bloqueo | El nombre de la persona |
No write permission | Sin derecho de escritura sobre esta ruta | null |
Failed to create lock | La colocación del bloqueo falló en la base | null |
Database error | Error de base de datos sobre esta ruta | null |
Una ruta inválida (absoluta, que contiene .., vacía, más allá de 4096 caracteres o portadora
de un byte nulo) no produce una entrada en failed: hace fallar toda la
solicitud.
POST /api/locks/{repo_id}/release
Libera bloqueos que tienes. Body:
{
"paths": ["Content/Maps/MainLevel.umap"]
}
No hay campo force en esta ruta. Añadir uno no tiene ningún
efecto: el servidor ignora los campos que no conoce, la solicitud tiene éxito, y el bloqueo ajeno
permanece. Un integrador que cuenta con ello cree haber liberado el archivo cuando no.
Para retirar el bloqueo de otra persona, la única ruta es
POST /api/admin/locks/{lock_id}/force-release. Está reservada a la administración del
repositorio en cuestión, y la operación queda inscrita en el registro de auditoría. Toma el identificador del bloqueo,
que obtienes por GET /api/locks/{repo_id}/status.
POST /api/locks/{repo_id}/heartbeat
Actualiza la marca de tiempo de actividad de tus bloqueos. Esto no prolonga nada, puesto que nada expira: es una señal de supervisión, que permite a un administrador distinguir un bloqueo aún usado de un bloqueo olvidado.
Body: un arreglo JSON de rutas, directamente, sin objeto envolvente.
["Content/Maps/MainLevel.umap", "Content/Characters/Hero.uasset"]
Las rutas inválidas se ignoran individualmente en lugar de hacer fallar todo el lote, para que un vestigio de seguimiento obsoleto no bloquee a las demás.
GET /api/locks/{repo_id}/status
Lista todos los bloqueos del repositorio, con el nombre del archivo y el de la persona que lo tiene.
Query params: ninguno. No hay ni filtro user, ni limit,
ni offset. La ruta devuelve la totalidad de los bloqueos del repositorio, a filtrar del lado del llamante.
Response 200: un arreglo plano, sin objeto envolvente 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"
}
]
}
El campo id es el que hay que pasar a
POST /api/admin/locks/{lock_id}/force-release.
Comentarios & revisiones
GET /api/comments/{repo_id}/comments?commit_hash=<hash>
Lista los comentarios de un commit, en hilos (padre → hijos).
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
Crea un comentario. file_path es opcional (comentario de commit vs. de archivo). parent_id para responder a un hilo existente.
Body:
{
"commit_hash": "7f3a9b1c...",
"file_path": "Content/Maps/MainLevel.umap",
"body": "LGTM, ship it",
"parent_id": null
}
POST /api/comments/{repo_id}/reviews
Envía una revisión sobre un commit. Se requiere la capacidad approve_changes.
Body:
{
"commit_hash": "7f3a9b1c...",
"status": "approved",
"comment": "Lighting looks great, approving"
}
Estado aceptado: approved, changes_requested, pending.
Lista de seguimiento
GET /api/watchlist/{repo_id}/watchlist
Lista los patrones de seguimiento del usuario actual sobre este repositorio.
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
Añade un patrón de seguimiento. Body:
{
"pattern": "Content/Characters/Hero/**",
"notify_on": ["commit", "lock"]
}
Los eventos posibles: commit (un commit modificó un archivo coincidente),
lock (se adquirió un bloqueo), review (se publicó una revisión).
DELETE /api/watchlist/{repo_id}/watchlist/{watch_id}
Retira un patrón de seguimiento. Devuelve { "success": true }.
Tablero de producción (tasks)
La antigua API modification-requests fue retirada: el tablero de producción la absorbe
(las solicitudes se convierten en tarjetas, origin='request'). Los endpoints están anidados bajo
/api/tasks/{repo_id}.
| Ruta | Descripción |
|---|---|
GET /api/tasks/{repo_id}/board | Tablero completo: columnas + tarjetas |
POST /api/tasks/{repo_id}/tasks | Crea una tarjeta |
PUT/DELETE /api/tasks/{repo_id}/tasks/{task_id} | Actualiza / elimina una tarjeta |
PUT /api/tasks/{repo_id}/tasks/{task_id}/assignees | Asigna usuarios a una tarjeta |
GET /api/tasks/{repo_id}/assignable-users | Lista de usuarios asignables |
POST /api/tasks/{repo_id}/columns | Configura las columnas del tablero |
Subrutas también disponibles: comentarios de tarjeta, enlaces a assets y commits, y adjuntos
(con imagen de portada). La configuración de las columnas requiere la capacidad manage_board
(admin / lead).
Builds
POST /api/builds/{repo_id}/upload?hash=<sha256>
Sube un archivo de build a <storage_path>/builds/<hash>.
El servidor verifica el SHA-256 dado en el query param contra el contenido recibido. Si el hash ya existe
del lado del servidor, devuelve 200 de inmediato sin volver a copiar (dedup del lado del almacenamiento de builds).
Headers: Content-Type: application/octet-stream.
Body: binario crudo (sin envoltura JSON).
Límites: 8 GB por archivo.
Errores:
400: query param hash ausente o mal formado, o SHA-256 calculado != hash provisto413: archivo > 8 GB
POST /api/builds/{repo_id}/publish
Registra el manifest de un build tras subir todos sus archivos. Se requiere la capacidad publish_builds.
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}
Lista los builds publicados de este proyecto.
El acceso a los builds se da proyecto por proyecto. La capacidad
download_builds no basta: siendo de servidor entero, solo dice que la
cuenta no es un simple espectador. Hace falta además o bien un acceso al repositorio, o bien una
autorización explícita sobre los builds de este proyecto, concedida por su administrador vía
/api/admin/repositories/{repo_id}/build-access. Un administrador de proyecto accede
de oficio a los que administra, un superadministrador a todos.
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
Descarga un archivo de un build. Se requiere la capacidad download_builds. El manifest de un build está disponible vía GET /api/builds/{repo_id}/{build_id}/manifest.
Administración
Qué protege realmente estas rutas
Al contrario de lo que se podría esperar, las rutas /api/admin/* no están
protegidas por una capacidad cada una. Pasan por uno de cuatro controles, que casi todos
recaen sobre el rol. Una capability es un derecho con nombre asociado a un rol;
el punto importante es que es
válida en todo el servidor, pues no lleva ninguna referencia a un repositorio. Por tanto
nunca puede servir para acotar a alguien en un proyecto.
| Control | Pasa para | Para qué sirve |
|---|---|---|
| Superadministrador | El rol admin, y solo él |
Todo lo que vale para el servidor entero: cuentas, grupos, registro de auditoría, estadísticas globales, licencia, actualización del servidor. |
| Administrador de este repositorio | admin, o un project_admin que administra este repositorio preciso |
Todo lo que es propio de un proyecto: permisos, reglas de validación, webhooks, acceso a los builds, desbloqueo forzado, recolección de basura. |
| Capacidad de servidor entero | Todo rol que posee la capacidad nombrada | Algunas rutas transversales. Atención: la capacidad vale sobre todos los repositorios, es su naturaleza. Un project_admin, que no posee ninguna, es admitido en su lugar solo sobre los repositorios que administra. |
| Admisión sola | admin o project_admin |
Deja entrar, pero no autoriza nada. La ruta que lo emplea debe luego restringir ella misma sus resultados a los repositorios administrados por el llamante. |
Dicho de otro modo: las capacidades manage_users y manage_permissions
solo existen sobre el papel. Se crean bien en la base en el momento de la instalación,
pero ninguna línea de código las consulta. Concederlas a un rol no cambia absolutamente nada.
No escribas una integración que suponga que una cuenta no-admin podrá gestionar
usuarios porque se le haya dado manage_users: recibirá un 403.
| Ruta | Control real | Descripción |
|---|---|---|
GET /api/admin/users | Admisión sola, luego filtrado | Un project_admin solo ve las cuentas de su perímetro |
POST /api/admin/users | Admisión sola | Crea una cuenta |
PUT/DELETE /api/admin/users/{id} | Superadministrador | Actualiza o elimina una cuenta |
POST /api/admin/users/{id}/reset-password | Superadministrador | Restablece la contraseña y revoca todos los tokens de la cuenta |
GET/POST /api/admin/groups | Superadministrador | Los grupos son globales, no pueden delegarse por proyecto |
GET/POST /api/admin/permissions/{repo_id} | Administrador de este repositorio | Reglas de permiso por patrón de ruta, concedidas a un grupo o a un usuario |
GET/POST /api/admin/repositories/{repo_id}/rules | Administrador de este repositorio | Reglas de validación aplicadas antes del envío |
GET/POST /api/admin/repositories/{repo_id}/webhooks | Administrador de este repositorio | Discord, Slack, Teams, o webhook genérico |
GET /api/admin/locks | Admisión sola, luego filtrado | Bloqueos, restringidos a los repositorios administrados |
POST /api/admin/locks/{lock_id}/force-release | Administrador de este repositorio | Retira el bloqueo de otra persona. Inscrito en el registro de auditoría |
GET /api/admin/audit | Superadministrador | Registro de auditoría, filtrable y paginado |
GET /api/admin/stats | Superadministrador | Volumetría de almacenamiento, tasa de deduplicación, crecimiento |
GET /api/admin/licence | Superadministrador | Estado de la licencia y asientos consumidos |
GET /api/admin/repositories/{repo_id}/gc/preview | Administrador de este repositorio | Simula la recolección de basura sin eliminar nada |
POST /api/admin/repositories/{repo_id}/gc | Administrador de este repositorio | Lanza la recolección de basura |
El detalle completo de los roles, de sus rangos y de lo que cada uno puede hacer está en la página dedicada a los roles y permisos.
Áreas de la API no cubiertas por esta página
Las siguientes familias de rutas existen, están montadas y servidas por el servidor, pero no se describen aquí. Si tu integración las necesita, lo más fiable hoy es observar las llamadas que hace el cliente de escritorio, o escribirnos.
| Prefijo | Qué cubre |
|---|---|
/api/advisor/* | Project Health: la auditoría de proyecto Unreal, sus hallazgos, la puntuación por pilar y la clasificación de los elementos a ignorar. |
/api/distribution/* | Distribución entre proyectos: enlaces entre un repositorio origen y un repositorio destino, publicación de archivos de un proyecto a otro, historial. |
/api/binaries/* | Binarios de editor precompilados, asociados a un commit, según el modelo de UnrealGameSync. |
/api/watchlist/* | Más allá de los patrones de seguimiento descritos arriba: las notificaciones y la bandeja de entrada. |
/api/profile/* | Perfil del usuario actual. |
/api/admin/repositories/{repo_id}/admins | Quién administra un repositorio: designación y retiro de los administradores de proyecto. |
/api/admin/repositories/{repo_id}/build-access | Quién puede descargar los builds de este proyecto, concedido proyecto por proyecto. |
/api/admin/licence | Estado de la licencia y asientos consumidos. |
/api/admin/server/* | Actualización del servidor desde el panel de administración. |
Estado
GET /health
Endpoint no autenticado que devuelve 200 OK si el servidor puede acceder a la base.
Usado para los health checks de balanceador de carga o de monitoreo (Prometheus blackbox, Datadog synthetic, etc.).
Response 200: OK (text/plain).
Response 503: si la base es inalcanzable.
GET /api/server-info
Endpoint público no autenticado que devuelve los metadatos del servidor (versión, etc.).
Como /health, no está protegido por el middleware de auth.
Formato de error
En caso de error, una ruta autenticada responde { "success": false, "error": "..." },
y una ruta /api/auth/* responde { "error": "..." }, en ambos casos
con un código HTTP adecuado.
El campo error es un mensaje destinado a un humano, no un código.
No existe ningún catálogo de códigos de error estables, ni en el cuerpo, ni en un encabezado. Así que no
construyas lógica sobre su contenido, y no analices su prefijo: estas frases se
reformulan de una versión a otra, y una prueba de cadena que rompe en silencio es peor que ninguna
prueba.
El código HTTP es lo único sobre lo que ramificar un comportamiento. La tabla de abajo da su lectura.
| HTTP | Significado | Cuándo |
|---|---|---|
| 400 | Bad request | Parámetro inválido, JSON mal formado, cuerpo demasiado grande (antes del límite duro 413) |
| 401 | Not authenticated | Token ausente, expirado, firma inválida, o token_version no coincide |
| 403 | Permission denied | Capacidad ausente, o sin permiso sobre el path / repo |
| 404 | Resource not found | Repo / archivo / commit / usuario inexistente o inaccesible |
| 409 | Conflict | Bloqueo ya tomado, commit en concurrencia, restricción única violada |
| 413 | Payload too large | Cuerpo más allá del límite (1 GB en /files, 8 GB en /builds/upload) |
| 429 | Too many requests | Únicamente en /auth/login (limitación de tasa por usuario) |
| 500 | Internal server error | Error de DB, error de IO. Registrado del lado del servidor, adjunta el timestamp si reportas el bug. |
| 503 | Service unavailable | Base inalcanzable (health check) o migración en curso |