uVersion
Español
Descargar →

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

EncabezadoValor
AuthorizationBearer <jwt> en toda ruta /api/* salvo /api/auth/* y /api/server-info (y /health)
Content-Typeapplication/json para POST/PUT con cuerpo JSON. application/octet-stream para subidas binarias (archivos de build).
Acceptapplication/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.

RutaParámetros aceptadosForma 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 desactivada
  • 429: 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 o token_version obsoleto

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 repositorio
  • 404: 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 caracteres
  • 403: el llamante no tiene el rol admin
  • 409: 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.

Los tres únicos valores aceptados para 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), campo chunks ausente
  • 403: sin permiso de escritura sobre alguna de las rutas
  • 409: 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 50
  • offset: por defecto 0
  • path (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

Los bloqueos nunca expiran

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:

reasonSignificadolocked_by
File is lockedOtra persona ya tiene el bloqueoEl nombre de la persona
No write permissionSin derecho de escritura sobre esta rutanull
Failed to create lockLa colocación del bloqueo falló en la basenull
Database errorError de base de datos sobre esta rutanull

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}.

RutaDescripción
GET /api/tasks/{repo_id}/boardTablero completo: columnas + tarjetas
POST /api/tasks/{repo_id}/tasksCrea una tarjeta
PUT/DELETE /api/tasks/{repo_id}/tasks/{task_id}Actualiza / elimina una tarjeta
PUT /api/tasks/{repo_id}/tasks/{task_id}/assigneesAsigna usuarios a una tarjeta
GET /api/tasks/{repo_id}/assignable-usersLista de usuarios asignables
POST /api/tasks/{repo_id}/columnsConfigura 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 provisto
  • 413: 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.

ControlPasa paraPara 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.

RutaControl realDescripción
GET /api/admin/usersAdmisión sola, luego filtradoUn project_admin solo ve las cuentas de su perímetro
POST /api/admin/usersAdmisión solaCrea una cuenta
PUT/DELETE /api/admin/users/{id}SuperadministradorActualiza o elimina una cuenta
POST /api/admin/users/{id}/reset-passwordSuperadministradorRestablece la contraseña y revoca todos los tokens de la cuenta
GET/POST /api/admin/groupsSuperadministradorLos grupos son globales, no pueden delegarse por proyecto
GET/POST /api/admin/permissions/{repo_id}Administrador de este repositorioReglas de permiso por patrón de ruta, concedidas a un grupo o a un usuario
GET/POST /api/admin/repositories/{repo_id}/rulesAdministrador de este repositorioReglas de validación aplicadas antes del envío
GET/POST /api/admin/repositories/{repo_id}/webhooksAdministrador de este repositorioDiscord, Slack, Teams, o webhook genérico
GET /api/admin/locksAdmisión sola, luego filtradoBloqueos, restringidos a los repositorios administrados
POST /api/admin/locks/{lock_id}/force-releaseAdministrador de este repositorioRetira el bloqueo de otra persona. Inscrito en el registro de auditoría
GET /api/admin/auditSuperadministradorRegistro de auditoría, filtrable y paginado
GET /api/admin/statsSuperadministradorVolumetría de almacenamiento, tasa de deduplicación, crecimiento
GET /api/admin/licenceSuperadministradorEstado de la licencia y asientos consumidos
GET /api/admin/repositories/{repo_id}/gc/previewAdministrador de este repositorioSimula la recolección de basura sin eliminar nada
POST /api/admin/repositories/{repo_id}/gcAdministrador de este repositorioLanza 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.

PrefijoQué 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}/adminsQuién administra un repositorio: designación y retiro de los administradores de proyecto.
/api/admin/repositories/{repo_id}/build-accessQuién puede descargar los builds de este proyecto, concedido proyecto por proyecto.
/api/admin/licenceEstado 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.

HTTPSignificadoCuándo
400Bad requestParámetro inválido, JSON mal formado, cuerpo demasiado grande (antes del límite duro 413)
401Not authenticatedToken ausente, expirado, firma inválida, o token_version no coincide
403Permission deniedCapacidad ausente, o sin permiso sobre el path / repo
404Resource not foundRepo / archivo / commit / usuario inexistente o inaccesible
409ConflictBloqueo ya tomado, commit en concurrencia, restricción única violada
413Payload too largeCuerpo más allá del límite (1 GB en /files, 8 GB en /builds/upload)
429Too many requestsÚnicamente en /auth/login (limitación de tasa por usuario)
500Internal server errorError de DB, error de IO. Registrado del lado del servidor, adjunta el timestamp si reportas el bug.
503Service unavailableBase inalcanzable (health check) o migración en curso