Wiki
REST API
Referência dos endpoints HTTP do servidor uVersion: autenticação, repositórios, arquivos, bloqueios, comentários, lista de observação, quadro de produção, builds, administração.
O servidor uVersion expõe uma API HTTP JSON. As chamadas são autenticadas com um
JWT (JSON Web Token), ou seja, o token de sessão que o servidor lhe entrega
no momento do login e que você devolve depois em cada requisição. Esta página documenta os
endpoints usados pelo cliente desktop, pelos plugins do editor e pela CLI uversion.
Você pode chamá-los diretamente para integrar o uVersion ao seu ferramental interno
(painel próprio, scripts de auditoria, webhooks, etc.).
Ela não cobre a totalidade da API: existem várias famílias de rotas que não são descritas aqui. A lista está na seção Áreas não cobertas.
Convenções
URL base
Todo caminho documentado é relativo à URL da sua instância. Os exemplos usam
https://uversion.mygamestudio.com. Substitua pela sua.
Cabeçalhos obrigatórios
| Cabeçalho | Valor |
|---|---|
Authorization | Bearer <jwt> em toda rota /api/* exceto /api/auth/* e /api/server-info (e /health) |
Content-Type | application/json para POST/PUT com corpo JSON. application/octet-stream para uploads binários (arquivos de build). |
Accept | application/json recomendado (o servidor retorna JSON por padrão) |
Dois formatos de resposta, e é preciso saber qual você está lendo
O servidor não tem um formato de resposta, mas dois, e confundi-los é o erro mais custoso para quem começa uma integração.
1. As rotas autenticadas (todo /api/* exceto /api/auth/*)
respondem com um envelope:
{
"success": true,
"data": { /* payload */ }
}
Em caso de erro, essas mesmas rotas respondem:
{
"success": false,
"error": "No write permission on this repository"
}
O campo success está sempre presente. data está presente em caso de sucesso,
error em caso de falha, nunca os dois juntos.
2. As rotas de autenticação (/api/auth/login,
/refresh, /validate, /logout, /register,
/change-password) não usam esse envelope. Elas retornam
o objeto nu, sem success nem data:
{
"token": "eyJhbGciOiJIUzI1NiIs...",
"user": { "id": 12, "username": "alice", ... },
"must_change_password": false
}
E seus erros são um objeto de um único campo, sem success:
{ "error": "Invalid credentials" }
Consequência prática: em /api/auth/login, o token é lido em .token
e não em .data.token. Um script que consulta .data.token recebe
null sem nenhum erro visível.
Por fim, GET /api/files/{repo_id}/content não retorna nem um nem outro: é o
conteúdo binário do arquivo, tal como está.
Paginação
Não há convenção de paginação comum: cada rota tem a sua, ou
não tem nenhuma. Não presuma nem limit, nem total, nem has_more.
| Rota | Parâmetros aceitos | Forma da resposta |
|---|---|---|
GET /api/files/{repo_id}/history |
limit (padrão 50), offset (padrão 0), path |
Array plano de commits. Sem total, sem has_more: você chegou ao fim quando o array contém menos elementos que limit. |
GET /api/repositories |
apenas include_inactive (e ele só é respeitado para um superadministrador) |
Array plano. Nem limit nem offset são lidos. |
GET /api/locks/{repo_id}/status |
Nenhum | Array plano de todos os bloqueios do repositório. |
GET /api/files/{repo_id}/snapshot |
commit_hash, obrigatório |
Array plano de arquivos. |
GET /api/admin/audit |
page e per_page, não limit/offset, além de filtros (user_id, action, entity_type, from, to) |
Reservada ao superadministrador. Ilustra bem a ausência de convenção comum: é a única rota que pagina por número de página. |
Para qualquer rota não listada aqui, considere que ela retorna a totalidade do seu resultado em uma única chamada.
Limitação de taxa
Não há limitação de taxa geral: um projeto Unreal conta com milhares de arquivos e as operações em lote (adquirir, liberar bloqueios) disparariam um throttle constantemente. Você pode, portanto, chamar a API em volume.
Uma única rota é limitada, /api/auth/login:
5 tentativas falhas por nome de usuário a cada 15 minutos. Os logins
bem-sucedidos não são contados.
Revogação de tokens
Cada token de sessão carrega um campo tv (token version), que reflete a coluna
users.token_version no banco. O servidor compara os dois em cada requisição.
Uma chamada a POST /api/auth/logout, uma redefinição de senha por um
administrador ou a desativação de uma conta incrementam esse valor, o que invalida
instantaneamente todos os tokens existentes desse usuário, em todas as suas
máquinas.
Curl
Para chamar a API a partir de um terminal. Note o .token: a resposta de
/api/auth/login é o objeto nu, não há nenhum .data a atravessar.
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')
# As rotas autenticadas, por sua vez, usam sim o envelope:
curl -s -H "Authorization: Bearer $TOKEN" \
https://uversion.mygamestudio.com/api/repositories | jq '.data'
Autenticação
Lembrete: nenhuma rota desta seção usa o envelope
{"success", "data"}. Elas retornam o objeto nu, e seus erros têm a forma
{"error": "..."}.
POST /api/auth/login
Autentica um usuário e retorna um token de sessão válido por 30 dias.
Body:
{
"username": "alice",
"password": "secret"
}
Response 200:
{
"token": "eyJhbGciOiJIUzI1NiIs...",
"user": {
"id": 12,
"username": "alice",
"email": "alice@mygamestudio.com",
"role": "lead"
},
"must_change_password": false
}
Não há campo expires_at: a duração de validade é lida no
próprio token, ou deduzida da configuração do servidor (30 dias por padrão).
Não ignore must_change_password. Esse campo vale true
quando a conta ainda roda com uma senha temporária: a que o instalador gerou
para a conta de administrador inicial, ou a que um administrador acabou de definir numa
redefinição. Um cliente que não o observa deixa o usuário nessa senha provisória
indefinidamente. O comportamento esperado é redirecionar imediatamente para
POST /api/auth/change-password antes de qualquer outra ação.
Erros:
401: credenciais inválidas ou conta desativada429: 5 tentativas falhas atingidas para este nome de usuário na janela de 15 minutos
O servidor sempre executa a verificação Argon2id da senha, inclusive contra uma impressão fictícia quando a conta não existe. A resposta leva, portanto, o mesmo tempo nos dois casos, o que impede adivinhar a existência de uma conta cronometrando as requisições.
POST /api/auth/refresh
Renova o token de sessão sem passar novamente pela senha.
Headers: Authorization: Bearer <token_atual>. Sem body.
Response 200: exatamente a mesma forma que /login
(token, user, must_change_password), com um token novo.
Erros:
401: token inválido, expirado, conta desativada, outoken_versionobsoleto
POST /api/auth/logout
Invalida todos os tokens do usuário atual, em todas as suas máquinas,
incrementando users.token_version. Ele terá de fazer login novamente em todo lugar: cliente desktop,
plugin do editor e CLI inclusos.
Response 200: { "logged_out": true }.
POST /api/auth/validate
Verifica se um token ainda é válido e retorna o usuário ao qual corresponde. A verificação
abrange também is_active e token_version, então um token revogado é
rejeitado mesmo que ainda não tenha expirado.
Atenção ao body: não é um objeto, é uma string JSON nua, ou seja, o token cercado por aspas duplas.
curl -X POST https://uversion.mygamestudio.com/api/auth/validate \
-H "Content-Type: application/json" \
-d '"eyJhbGciOiJIUzI1NiIs..."'
Response 200: um objeto de usuário nu.
{
"id": 12,
"username": "alice",
"email": "alice@mygamestudio.com",
"role": "lead"
}
Não há valid, nem expires_at, nem refreshed_token: a
validade é lida no código HTTP (200 ou 401), e a renovação passa por
/api/auth/refresh, nunca por esta rota.
POST /api/auth/register
Esta rota recusa por padrão. O registro aberto é desativado a menos que
o operador o tenha ativado explicitamente na configuração do servidor; caso contrário a resposta é
403 com {"error": "Open registration is disabled; contact your administrator"}.
No funcionamento normal, as contas são criadas pela administração:
POST /api/admin/users, ou a aba Usuários do painel de administração. Não
construa uma integração que dependa de /register.
POST /api/auth/change-password
Altera a senha do usuário atual. Incrementa token_version, o que
invalida todos os tokens anteriores, inclusive o que acabou de servir para fazer a chamada.
Repositórios
GET /api/repositories
Lista os repositórios acessíveis pelo usuário atual, filtrados pela tabela de permissões. Um
usuário que não tem nenhuma regra de permissão sobre um repositório não o vê. Os papéis
admin e lead veem todos os repositórios.
Query params: um só, include_inactive (booleano, padrão
false), e ele só é respeitado para um superadministrador. Não há
nem limit nem offset: a rota retorna toda a 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"
}
]
}
Esses são os únicos campos retornados. Em particular, não há nem owner, nem
current_revision, nem file_count, nem size_bytes, nem
last_commit_at: um repositório não tem proprietário no sentido da API, e as
volumetrias se obtêm pelas rotas de estatísticas de administração.
GET /api/repositories/{repo_id}
Detalhe de um repositório. Mesma forma de objeto que na lista, com os mesmos campos.
Erros:
403: sem acesso a este repositório404: repositório inexistente
POST /api/repositories
Cria um repositório. Reservado ao superadministrador, ou seja, ao papel
admin e somente a ele. Não é uma capacidade: o controle recai diretamente sobre o
papel, então um project_admin ou um lead recebe um 403. Não
existe nenhuma capacidade create_repos no produto.
Body:
{
"name": "new-project",
"description": "Descrição opcional"
}
Validação:
name: 1 a 255 caracteres. O servidor não aplica nenhuma restrição de conjunto de caracteres. O caminho de armazenamento em disco é derivado do nome normalizando-o.description: opcional.
A unicidade recai sobre o nome sozinho, na escala do servidor. Não há noção de proprietário, portanto não há unicidade «por proprietário».
Erros:
400: nome vazio ou além de 255 caracteres403: o chamador não tem o papeladmin409: um repositório já leva este nome
Arquivos
POST /api/files/{repo_id}/upload-chunks
Envia um arquivo inteiro, e não uma lista de pedaços. O nome da rota é enganoso: é o servidor quem divide o arquivo em blocos (os chunks), não você. Um chamador nunca precisa fazer essa divisão ele mesmo.
Os blocos idênticos, reconhecidos pela sua impressão SHA-256, são deduplicados: um bloco já presente no servidor não é armazenado uma segunda vez, seja qual for o arquivo ou o repositório de onde vem. É isso que faz com que um arquivo binário grande modificado na margem não custe quase nada em espaço de disco.
Body: um objeto de um único campo, contendo o arquivo completo codificado em base64.
{
"data": "<o arquivo inteiro, codificado em base64>"
}
Qualquer outra forma, em particular um array chunks, é rejeitada pelo 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
}
}
O array chunks deve ser retomado tal como está, objetos completos inclusos,
no POST /commit que segue. chunks_stored conta os blocos realmente
escritos em disco e chunks_deduplicated os que já existiam.
Permissão: a escrita sobre o repositório é exigida, caso contrário 403.
Limites: corpo de requisição limitado a 1 GB (ajustável por
security.max_body_size_files). Um arquivo maior que esse limite não pode
passar por esta rota.
POST /api/files/{repo_id}/commit
Cria um commit atômico a partir de uma lista de arquivos e seus blocos. Ou todos os arquivos passam, ou nenhum.
action são add, modify e delete
Não added, não modified, não deleted.
Não é um detalhe de forma. O servidor testa literalmente
action == "delete" e trata todo o resto como uma adição ou uma
modificação. Enviar "deleted" não apaga, portanto, nada: o arquivo vai para
o ramo de escrita, com um array chunks vazio, e o servidor registra uma
revisão de conteúdo vazio. Nenhum erro é lançado. O arquivo permanece presente, sua última
versão é sobrescrita pelo vazio, e a perda só se vê no próximo sync de
outra pessoa.
Body:
{
"message": "Updated main level + hero pose pass",
"commit_hash": "opcional: para agrupar vários lotes sob um único 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ão é opcional, inclusive num delete.
O campo deve estar presente: omiti-lo faz falhar a desserialização de toda a requisição. Para
uma exclusão, envie um array vazio.
O array contém objetos, não strings: retome sem modificar as
entradas {hash, offset, size, compressed_size} retornadas por
/upload-chunks. Cada hash deve ser uma impressão hexadecimal de 64
caracteres, sem o que o commit inteiro é recusado com 400.
O campo commit_hash na raiz é opcional. Ele serve para fazer várias chamadas
sucessivas serem carregadas por um só e mesmo commit, o que o cliente desktop faz quando divide um
envio volumoso em lotes. Omitido, o servidor calcula um.
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 }
]
}
}
Esses são os únicos campos retornados. Não há nem files_changed, nem
bytes_uploaded, nem bytes_deduped, nem revision. Note que
revision_number é um contador por arquivo, não um número de
versão do repositório: é commit_hash quem identifica o commit, e é ele que se deve
conservar para referenciar um estado.
Erros:
400: corpo malformado, impressão de bloco inválida (esperado: 64 caracteres hexadecimais), campochunksausente403: sem permissão de escrita sobre um dos caminhos409: bloqueio mantido por outra pessoa sobre um arquivo modificado, ou commit concorrente sobre o mesmo arquivo
GET /api/files/{repo_id}/snapshot
Retorna o estado do repositório tal como estava no momento de um commit dado: a lista de arquivos presentes, com seu número de revisão e seu tamanho. Usado pelo cliente desktop para o clone e para a sincronização forçada.
Query params:
-
commit_hash: obrigatório. É a impressão do commit que serve de ponto de referência. Sem este parâmetro a requisição é rejeitada, e não existe um valor padrão «último estado».
Não há parâmetro revision. O snapshot é pedido por
impressão de commit, nunca por número. Um commit desconhecido responde 404.
Response 200: um array plano, sem 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
}
]
}
A resposta não contém as listas de blocos. Para recuperar um conteúdo, passe
por GET /api/files/{repo_id}/content, que remonta o arquivo do lado do servidor.
GET /api/files/{repo_id}/content
Baixa o conteúdo de um arquivo em uma revisão dada (o servidor remonta os chunks). Usado pelo clone e pelo sync.
Query params:
path: caminho do arquivo (relativo à raiz do repo)revision(opcional): número de revisão. Padrão: a última.
Response 200: o conteúdo binário do arquivo.
GET /api/files/{repo_id}/history
Histórico de commits do repositório, do mais recente ao mais antigo.
Query params:
limit: número de commits, padrão 50offset: padrão 0path(opcional): mantém apenas os commits que tocaram este arquivo
Esses são os três únicos parâmetros lidos. Não há nem author nem
since: um parâmetro desconhecido é ignorado em silêncio, o que dá uma resposta
plausível mas não filtrada. Filtre por autor ou por data do lado do chamador.
Response 200: um array 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
}
]
}
]
}
Não há nem total, nem has_more, nem lembrete de limit e
offset. Para percorrer todo o histórico, incremente offset até
receber menos elementos que limit.
Cada commit carrega diretamente a lista de arquivos tocados. Uma entrada cuja revisão é uma exclusão é marcada como tal e não tem conteúdo a baixar.
Sync incremental
Endpoints usados pelo cliente desktop para sincronizar de forma eficiente:
GET /api/files/{repo_id}/sync: arquivos alterados desde uma revisão, para um sync delta.GET /api/files/{repo_id}/deletions: arquivos excluídos do lado do servidor, para propagar as exclusões localmente.GET /api/files/{repo_id}/list: lista dos arquivos do repo.POST /api/files/{repo_id}/checkout: adquire os bloqueios e prepara a edição.
Bloqueios
Um bloqueio é mantido até ser explicitamente liberado: por um checkin, por um revert, ou por um desbloqueio forçado de administrador. Não há nenhuma expiração automática, nem ao fim de uma hora, nem ao fim de um mês.
O campo expires_at existe unicamente porque a coluna correspondente no banco
não aceita um valor vazio. O servidor escreve nele um valor sentinela de cem anos: um bloqueio
tomado hoje mostra um vencimento situado por volta de 2126. Não construa nada sobre este
campo, e não mostre esta data a um usuário.
O heartbeat não prolonga, portanto, nada. É um sinal de supervisão, cujo único fim é mostrar aos administradores quais bloqueios ainda são usados ativamente.
POST /api/locks/{repo_id}/acquire
Adquire bloqueios sobre uma lista de caminhos. As aquisições são independentes: a lista
acquired contém os sucessos, a lista failed as falhas com seu 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"
}
]
}
}
O vencimento em 2126 deste exemplo não é um erro de digitação: é o valor sentinela descrito acima. O bloqueio é permanente.
Motivos de falha. O campo reason é uma frase em inglês
destinada a ser exibida, não um código estável. Não escreva lógica que compare esta
string, e não «parseie» o seu prefixo: ela pode ser reformulada de uma versão a outra.
Os valores produzidos atualmente são:
reason | Significado | locked_by |
|---|---|---|
File is locked | Outra pessoa já detém o bloqueio | O nome da pessoa |
No write permission | Sem direito de escrita sobre este caminho | null |
Failed to create lock | A colocação do bloqueio falhou no banco | null |
Database error | Erro de banco de dados sobre este caminho | null |
Um caminho inválido (absoluto, contendo .., vazio, além de 4096 caracteres ou portador
de um byte nulo) não produz uma entrada em failed: ele faz falhar toda a
requisição.
POST /api/locks/{repo_id}/release
Libera bloqueios que você detém. Body:
{
"paths": ["Content/Maps/MainLevel.umap"]
}
Não há campo force nesta rota. Adicionar um não tem nenhum
efeito: o servidor ignora os campos que não conhece, a requisição tem sucesso, e o bloqueio alheio
permanece no lugar. Um integrador que conta com isso acredita ter liberado o arquivo quando não.
Para retirar o bloqueio de outra pessoa, a única rota é
POST /api/admin/locks/{lock_id}/force-release. Ela é reservada à administração do
repositório em questão, e a operação é inscrita no log de auditoria. Ela recebe o identificador do bloqueio,
que você obtém por GET /api/locks/{repo_id}/status.
POST /api/locks/{repo_id}/heartbeat
Atualiza o carimbo de tempo de atividade dos seus bloqueios. Isso não prolonga nada, já que nada expira: é um sinal de supervisão, que permite a um administrador distinguir um bloqueio ainda usado de um bloqueio esquecido.
Body: um array JSON de caminhos, diretamente, sem objeto envolvente.
["Content/Maps/MainLevel.umap", "Content/Characters/Hero.uasset"]
Os caminhos inválidos são ignorados individualmente em vez de fazer falhar todo o lote, para que um resíduo de rastreamento obsoleto não bloqueie os outros.
GET /api/locks/{repo_id}/status
Lista todos os bloqueios do repositório, com o nome do arquivo e o da pessoa que o detém.
Query params: nenhum. Não há nem filtro user, nem limit,
nem offset. A rota retorna a totalidade dos bloqueios do repositório, a filtrar do lado do chamador.
Response 200: um array plano, sem objeto envolvente nem 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"
}
]
}
O campo id é o que deve ser passado a
POST /api/admin/locks/{lock_id}/force-release.
Comentários & revisões
GET /api/comments/{repo_id}/comments?commit_hash=<hash>
Lista os comentários de um commit, em threads (pai → filhos).
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
Cria um comentário. file_path é opcional (comentário de commit vs. de arquivo). parent_id para responder a uma thread existente.
Body:
{
"commit_hash": "7f3a9b1c...",
"file_path": "Content/Maps/MainLevel.umap",
"body": "LGTM, ship it",
"parent_id": null
}
POST /api/comments/{repo_id}/reviews
Envia uma revisão sobre um commit. Capacidade approve_changes exigida.
Body:
{
"commit_hash": "7f3a9b1c...",
"status": "approved",
"comment": "Lighting looks great, approving"
}
Status aceito: approved, changes_requested, pending.
Lista de observação
GET /api/watchlist/{repo_id}/watchlist
Lista os padrões de observação do usuário atual sobre este repositório.
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
Adiciona um padrão de observação. Body:
{
"pattern": "Content/Characters/Hero/**",
"notify_on": ["commit", "lock"]
}
Os eventos possíveis: commit (um commit modificou um arquivo correspondente),
lock (um bloqueio foi adquirido), review (uma revisão foi postada).
DELETE /api/watchlist/{repo_id}/watchlist/{watch_id}
Retira um padrão de observação. Retorna { "success": true }.
Quadro de produção (tasks)
A antiga API modification-requests foi retirada: o quadro de produção a absorve
(as solicitações viram cartões, origin='request'). Os endpoints estão aninhados sob
/api/tasks/{repo_id}.
| Rota | Descrição |
|---|---|
GET /api/tasks/{repo_id}/board | Quadro completo: colunas + cartões |
POST /api/tasks/{repo_id}/tasks | Cria um cartão |
PUT/DELETE /api/tasks/{repo_id}/tasks/{task_id} | Atualiza / exclui um cartão |
PUT /api/tasks/{repo_id}/tasks/{task_id}/assignees | Atribui usuários a um cartão |
GET /api/tasks/{repo_id}/assignable-users | Lista dos usuários atribuíveis |
POST /api/tasks/{repo_id}/columns | Configura as colunas do quadro |
Sub-rotas também disponíveis: comentários de cartão, links para assets e commits, e anexos
(com imagem de capa). A configuração das colunas requer a capacidade manage_board
(admin / lead).
Builds
POST /api/builds/{repo_id}/upload?hash=<sha256>
Sobe um arquivo de build para <storage_path>/builds/<hash>.
O servidor verifica o SHA-256 fornecido no query param contra o conteúdo recebido. Se o hash já existe
do lado do servidor, retorna 200 imediatamente sem recopiar (dedup do lado do armazenamento de builds).
Headers: Content-Type: application/octet-stream.
Body: binário cru (sem envoltório JSON).
Limites: 8 GB por arquivo.
Erros:
400: query param hash ausente ou malformado, ou SHA-256 calculado != hash fornecido413: arquivo > 8 GB
POST /api/builds/{repo_id}/publish
Registra o manifest de um build após o upload de todos os seus arquivos. Capacidade publish_builds exigida.
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 os builds publicados deste projeto.
O acesso aos builds é dado projeto por projeto. A capacidade
download_builds não basta: sendo de servidor inteiro, ela só diz que a
conta não é um simples espectador. É preciso além disso ou um acesso ao repositório, ou uma
autorização explícita sobre os builds deste projeto, concedida pelo seu administrador via
/api/admin/repositories/{repo_id}/build-access. Um administrador de projeto acessa
de ofício os que administra, um superadministrador 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
Baixa um arquivo de um build. Capacidade download_builds exigida. O manifest de um build está disponível via GET /api/builds/{repo_id}/{build_id}/manifest.
Administração
O que realmente protege estas rotas
Ao contrário do que se poderia esperar, as rotas /api/admin/* não são
protegidas por uma capacidade cada uma. Elas passam por um de quatro controles, que quase todos
recaem sobre o papel. Uma capability é um direito com nome vinculado a um papel;
o ponto importante é que ela é
válida em todo o servidor, pois não carrega nenhuma referência a um repositório. Ela
não pode, portanto, jamais servir para confinar alguém a um projeto.
| Controle | Passa para | Para que serve |
|---|---|---|
| Superadministrador | O papel admin, e somente ele |
Tudo o que vale para o servidor inteiro: contas, grupos, log de auditoria, estatísticas globais, licença, atualização do servidor. |
| Administrador deste repositório | admin, ou um project_admin que administra este repositório preciso |
Tudo o que é próprio de um projeto: permissões, regras de validação, webhooks, acesso aos builds, desbloqueio forçado, coleta de lixo. |
| Capacidade de servidor inteiro | Todo papel que detém a capacidade nomeada | Algumas rotas transversais. Atenção: a capacidade vale sobre todos os repositórios, é a sua natureza. Um project_admin, que não detém nenhuma, é admitido em seu lugar apenas sobre os repositórios que administra. |
| Admissão sozinha | admin ou project_admin |
Deixa entrar, mas não autoriza nada. A rota que a emprega deve depois restringir ela mesma os seus resultados aos repositórios administrados pelo chamador. |
Dito de outro modo: as capacidades manage_users e manage_permissions
só existem no papel. Elas são de fato criadas no banco no momento da instalação,
mas nenhuma linha de código as consulta. Concedê-las a um papel não muda absolutamente nada.
Não escreva uma integração que suponha que uma conta não-admin poderá gerenciar
usuários porque lhe deram manage_users: ela receberá um 403.
| Rota | Controle real | Descrição |
|---|---|---|
GET /api/admin/users | Admissão sozinha, depois filtragem | Um project_admin só vê as contas do seu perímetro |
POST /api/admin/users | Admissão sozinha | Cria uma conta |
PUT/DELETE /api/admin/users/{id} | Superadministrador | Atualiza ou exclui uma conta |
POST /api/admin/users/{id}/reset-password | Superadministrador | Redefine a senha e revoga todos os tokens da conta |
GET/POST /api/admin/groups | Superadministrador | Os grupos são globais, não podem ser delegados por projeto |
GET/POST /api/admin/permissions/{repo_id} | Administrador deste repositório | Regras de permissão por padrão de caminho, concedidas a um grupo ou a um usuário |
GET/POST /api/admin/repositories/{repo_id}/rules | Administrador deste repositório | Regras de validação aplicadas antes do envio |
GET/POST /api/admin/repositories/{repo_id}/webhooks | Administrador deste repositório | Discord, Slack, Teams, ou webhook genérico |
GET /api/admin/locks | Admissão sozinha, depois filtragem | Bloqueios, restritos aos repositórios administrados |
POST /api/admin/locks/{lock_id}/force-release | Administrador deste repositório | Retira o bloqueio de outra pessoa. Inscrito no log de auditoria |
GET /api/admin/audit | Superadministrador | Log de auditoria, filtrável e paginado |
GET /api/admin/stats | Superadministrador | Volumetria de armazenamento, taxa de deduplicação, crescimento |
GET /api/admin/licence | Superadministrador | Estado da licença e assentos consumidos |
GET /api/admin/repositories/{repo_id}/gc/preview | Administrador deste repositório | Simula a coleta de lixo sem excluir nada |
POST /api/admin/repositories/{repo_id}/gc | Administrador deste repositório | Inicia a coleta de lixo |
O detalhe completo dos papéis, de seus ranques e do que cada um pode fazer está na página dedicada aos papéis e permissões.
Áreas da API não cobertas por esta página
As seguintes famílias de rotas existem, estão montadas e servidas pelo servidor, mas não são descritas aqui. Se a sua integração precisar delas, o mais confiável hoje é observar as chamadas que o cliente desktop faz, ou nos escrever.
| Prefixo | O que cobre |
|---|---|
/api/advisor/* | Project Health: a auditoria de projeto Unreal, seus achados, a pontuação por pilar e a triagem dos elementos a ignorar. |
/api/distribution/* | Distribuição entre projetos: links entre um repositório de origem e um repositório de destino, publicação de arquivos de um projeto para outro, histórico. |
/api/binaries/* | Binários de editor pré-compilados, associados a um commit, no modelo do UnrealGameSync. |
/api/watchlist/* | Além dos padrões de observação descritos acima: as notificações e a caixa de entrada. |
/api/profile/* | Perfil do usuário atual. |
/api/admin/repositories/{repo_id}/admins | Quem administra um repositório: designação e retirada dos administradores de projeto. |
/api/admin/repositories/{repo_id}/build-access | Quem pode baixar os builds deste projeto, concedido projeto por projeto. |
/api/admin/licence | Estado da licença e assentos consumidos. |
/api/admin/server/* | Atualização do servidor a partir do painel de administração. |
Saúde
GET /health
Endpoint não autenticado que retorna 200 OK se o servidor consegue acessar o banco.
Usado para os health checks de balanceador de carga ou de monitoramento (Prometheus blackbox, Datadog synthetic, etc.).
Response 200: OK (text/plain).
Response 503: se o banco está inacessível.
GET /api/server-info
Endpoint público não autenticado que retorna os metadados do servidor (versão, etc.).
Como /health, não é protegido pelo middleware de auth.
Formato de erro
Em caso de erro, uma rota autenticada responde { "success": false, "error": "..." },
e uma rota /api/auth/* responde { "error": "..." }, nos dois casos
com um código HTTP adequado.
O campo error é uma mensagem destinada a um humano, não um código.
Não existe nenhum catálogo de códigos de erro estáveis, nem no corpo, nem em um cabeçalho. Portanto não
construa lógica sobre o seu conteúdo, e não analise o seu prefixo: estas frases são
reformuladas de uma versão a outra, e um teste de string que quebra em silêncio é pior que nenhum
teste.
O código HTTP é a única coisa sobre a qual ramificar um comportamento. A tabela abaixo dá a sua leitura.
| HTTP | Significado | Quando |
|---|---|---|
| 400 | Bad request | Parâmetro inválido, JSON malformado, corpo grande demais (antes do limite duro 413) |
| 401 | Not authenticated | Token ausente, expirado, assinatura inválida, ou token_version não corresponde |
| 403 | Permission denied | Capacidade ausente, ou sem permissão sobre o path / repo |
| 404 | Resource not found | Repo / arquivo / commit / usuário inexistente ou inacessível |
| 409 | Conflict | Bloqueio já mantido, commit em concorrência, restrição única violada |
| 413 | Payload too large | Corpo além do limite (1 GB em /files, 8 GB em /builds/upload) |
| 429 | Too many requests | Unicamente em /auth/login (limitação de taxa por usuário) |
| 500 | Internal server error | Erro de DB, erro de IO. Logado do lado do servidor, anexe o timestamp se reportar o bug. |
| 503 | Service unavailable | Banco inacessível (health check) ou migração em curso |