HTTP API
The server has two kinds of routes. Format routes are what Maven, Gradle, apt and curl use. The JSON API under /api/ is what the web UI uses. You can call it from scripts too.
Conventions
Section titled “Conventions”- Errors are
{"error": "..."}. Settings validation addsfields, a map from key to message. - Request bodies are capped at
max_json_bytes(1 MiB by default). - Lists return
{items, next}. Pass?after=<next>&limit=<n>to get the next page. The defaultlimitis 50 and the maximum is 200.nextis empty on the last page. Lists use keyset paging, so there is no offset. - CSRF. The JSON API refuses cross-origin browser writes.
- Rate limit. Failed attempts are limited per IP and per user name. The burst is 10, then 1 every 6 seconds. Over the limit you get 429 with
Retry-After.
Credentials
Section titled “Credentials”| Credential | Looks like | Accepted on |
|---|---|---|
| Password | your password | HTTP Basic on format routes and /api/. Reads only. SSO users have no password. |
| Read token | trp_<id>_<secret> |
HTTP Basic password (any user name) and /api/. Reads only. |
| UI session | cookie tr_session |
/api/ only. |
| Publish session token | trs_<id>_<secret> |
HTTP Basic password (any user name) on the format routes of its one repo, and /api/publish/.... This is the only credential that can write. |
A publish session token gets 403 on every other /api/ route, even for an admin user. See Publishing overview and Read tokens.
In the tables below, “read” and “write” mean the rules in Repo access. “Session” means a UI session or HTTP Basic with a password. “Admin” means an admin user.
Format routes
Section titled “Format routes”| Route | Auth | Notes |
|---|---|---|
GET /healthz |
none | Returns ok when the database answers. |
GET, HEAD, PUT /maven/{repo}/{path...} |
read, write | PUT returns 201 for a new file, 200 for an overwrite or an identical retry, 409 for a changed release file, 413 over max_upload_bytes. Mutable paths are maven-metadata.xml* and anything under a -SNAPSHOT directory. |
PUT, POST /debian/{repo}/upload/{dist}/{component} |
write | The body is a raw .deb. Returns 201 {package, version, architecture, filename, sha256}, 200 for an identical package, 409 for a conflict. |
GET, HEAD /debian/{repo}/{path...} |
read | Serves dists/... and pool/.... |
GET, HEAD, PUT /raw/{repo}/{path...} |
read, write | PUT returns 201 for a new path. It returns 200 for identical bytes, or for a replace when the repo overwrite is allow. It returns 409 for different bytes when overwrite is deny (the default), and 413 over max_upload_bytes. |
GET /keys/debian.asc |
none | The public signing key. |
GET / |
none | The embedded web UI. |
On S3 storage with s3.redirect on, a GET download answers 302 with a presigned URL. HEAD is never redirected.
Repo access
Section titled “Repo access”- A repo with
anonymous_readon allows reads without credentials. - Otherwise, no credentials or bad credentials get 401 with a Basic challenge. Valid credentials without permission get 403.
- An unknown repo gets 404. For anonymous callers it gets 401, so repo names do not leak.
- A write to a repo that mirrors a Nexus source gets 409 until cut-over.
- A write without an open publish session for that repo gets 401 with
{"error":"publishing needs a publish session: <base_url>/#/publishing"}. - A device session or manual session writes only while its user still has
writepermission on the repo. Otherwise it gets 403.
curl -fL -u anything:trp_abc123def456_SECRET \ -O https://repo.example.com/raw/files/tools/app.tar.gzcurl -f -u publish:trs_abc123def456_SECRET \ -T app.tar.gz https://repo.example.com/raw/files/tools/app.tar.gzcurl -f -u publish:trs_abc123def456_SECRET \ -T mytool_1.0_amd64.deb \ https://repo.example.com/debian/apt/upload/stable/mainSee Maven and Gradle, Debian and Raw files.
Sign-in and account
Section titled “Sign-in and account”| Method and path | Auth | Notes |
|---|---|---|
POST /api/auth/login |
none | Body {username, password}. A user with a second factor gets {second_factor: true, methods: [...]} and the cookie tr_login. A user without one gets the user object with second_factor_required: true and a setup-only session. 401 for SSO users. 403 for a local admin when admin_password_login is off. |
POST /api/auth/logout |
session | Ends the local session only. |
POST /api/auth/login/passkey |
tr_login cookie |
Returns {publicKey: {...}}, the WebAuthn options for the pending sign-in. |
POST /api/auth/login/second |
tr_login cookie |
Body {totp_code}, {passkey: <assertion>} or {recovery_code}. 200 sets tr_session. 401 for a wrong proof (the sign-in stays pending) or an expired sign-in. 429 after too many failures. |
GET /api/auth/config |
none | {password_login, oidc, admin_password_login}. The UI uses it to draw the login page. |
GET /api/auth/oidc/login |
none | Starts SSO. 404 when SSO is off, 503 when discovery fails, otherwise 302 to the identity provider. |
GET /api/auth/oidc/callback |
none | The SSO return URL. On success it sets the session and redirects to /. On failure it redirects to /#/login?error=<reason>. |
GET /api/me |
any | The current user, with kind (local or oidc), email and second_factor_required. |
GET, POST /api/tokens |
any | Lists or creates read tokens. The token is shown once. A token cannot create tokens. 403 for SSO users. |
DELETE /api/tokens/{id} |
any | Deletes a read token. |
GET, POST /api/users |
admin | Lists (paginated) or creates users. |
PATCH /api/users/{username} |
admin; a user may change their own password | Updates a user. |
The reason values after a failed SSO sign-in are not_authorized, username_conflict, invalid_username, account_disabled and sso_failed. See Single sign-on.
Second factors
Section titled “Second factors”All routes here need a UI session. Basic auth and tokens get 403. See Accounts and 2FA.
| Method and path | Notes |
|---|---|
GET /api/security |
Returns {totp: [...], passkeys: [...], recovery_codes: <unused count>, second_factor: bool}. |
POST /api/security/totp/begin |
Body {name}. Returns {setup_id, secret, otpauth_url}. The secret is shown once and the setup lasts 10 minutes. |
POST /api/security/totp/confirm |
Body {setup_id, code}. Returns 201 {id, name, created_at}. |
DELETE /api/security/totp/{id} |
Returns 204. |
POST /api/security/passkeys/begin |
Returns {publicKey} creation options. |
POST /api/security/passkeys/finish |
Body {name, credential}. Returns 201. |
POST /api/security/passkeys/assert |
Returns options for a fresh check before an approval or a manual session. |
DELETE /api/security/passkeys/{id} |
Returns 204. |
POST /api/security/recovery-codes |
Returns 201 {codes: [...]} with 10 one-time codes. It replaces the old set. The codes are shown once. |
Passkey routes return 409 when base_url is empty.
Repos, search and usage
Section titled “Repos, search and usage”| Method and path | Auth | Notes |
|---|---|---|
GET /api/formats |
none | [{name, title, default_config, settings_group?}] for every registered format. |
GET /api/repos |
any or anonymous | Lists only repos the caller can see. |
POST /api/repos |
admin | Creates a repo. format must be a registered format name. config must be a JSON object that the format accepts (400 otherwise). An empty config becomes the format default. |
GET /api/repos/{name} |
any or anonymous | One repo. The JSON has locked_by, which is "" when the repo is not locked. |
PATCH /api/repos/{name} |
admin | Changes a repo. A PATCH with config lets the format react to the change. |
GET /api/repos/{name}/permissions |
admin | Lists permissions. |
PUT, DELETE /api/repos/{name}/permissions/{username} |
admin | PUT takes {level: "read" | "write"}. write includes read. |
GET /api/repos/{name}/assets?prefix=&after=&limit= |
read | Files in path order. The prefix is a range scan. |
GET /api/search?q=&repo=&after=&limit= |
any or anonymous | Substring search on paths. q needs at least 3 characters. Only readable repos are searched. |
GET /api/repos/{name}/status |
read | The format status, or {}. For Debian it is {pending}, the number of index units still to rebuild. |
GET /api/repos/{name}/usage |
read | {base_url, snippets: [{title, lang, code}]}. The web UI shows these client snippets. |
See Browsing and search.
Publishing
Section titled “Publishing”All publishing routes are described in Publishing overview. Lists use ?after=&limit=.
Devices
Section titled “Devices”| Method and path | Auth | Notes |
|---|---|---|
POST /api/devices/enroll |
none | Body {name, kind: "gradle" | "maven", public_key}. public_key is the raw 32-byte Ed25519 key in base64url without padding. Returns {device_code, user_code, verification_url, expires_in: 600, interval: 5}. Each IP gets a burst of 10, then 1 per 30 seconds. |
POST /api/devices/enroll/poll |
none | Body {device_code}. 200 {device_id} once approved (once only). Otherwise 400 with authorization_pending, slow_down or expired_token. |
GET /api/devices/enroll?user_code= |
session | Shows the pending enrollment, for the approval page. 404 for an unknown or expired code. |
POST /api/devices/approve |
session | Body {user_code, totp_code | passkey}. Returns 201 with the device. 401 for a wrong second factor, 404 for an unknown code, 409 for a used code. |
GET /api/devices |
session | Your devices, newest first. An admin sees all devices. |
DELETE /api/devices/{id} |
session | Returns 204. It revokes the device and ends its open sessions. An admin may revoke any device. |
Approve refuses read tokens and publish session tokens.
Publish sessions
Section titled “Publish sessions”| Method and path | Auth | Notes |
|---|---|---|
POST /api/publish/challenge |
none | Body {device_id}. Returns {nonce}. The nonce is single use and lives 60 seconds. |
POST /api/publish/sessions |
none | Body {device_id, repo, nonce, signature}. Returns 201 {id, token, repo, expires_at}. The signature is Ed25519 over "telesphorepo publish v1\n" + nonce + "\n" + device_id + "\n" + repo, in base64url without padding. |
POST /api/publish/sessions/oidc |
none | Body {token, repo}. token is the CI identity token. Same answer as above. |
POST /api/publish/sessions/manual |
session | Body {repo, totp_code | passkey}. Same answer. It needs a fresh check. Recovery codes do not work here. 401 for a wrong proof. 429 with Retry-After when limited. |
GET /api/publish/sessions |
session | Your own sessions. |
DELETE /api/publish/sessions/{id} |
the session’s own token | Returns 204. Send the token as the Basic password or as a Bearer token. |
GET /api/publish/log |
admin | Every session, newest first. |
Trust policies
Section titled “Trust policies”| Method and path | Auth | Notes |
|---|---|---|
GET, POST /api/publish/trusts |
admin | The list is in name order. |
GET, PATCH, DELETE /api/publish/trusts/{id} |
admin | PATCH changes only the fields you send. A change of issuer, audience, claims, jwks or repos, a disable and a delete end the open sessions of the policy. |
A trust input is {name, issuer, audience, claims: {claim: [value...]}, jwks, repos: [name...], disabled}. See Trust policies.
curl -fsS -X POST https://repo.example.com/api/publish/sessions/oidc \ -H 'Content-Type: application/json' \ -d '{"token":"'"$CI_ID_TOKEN"'","repo":"releases"}'curl -fsS -X DELETE -u publish:trs_abc123def456_SECRET \ https://repo.example.com/api/publish/sessions/42Settings and signing
Section titled “Settings and signing”All routes are admin only. See Settings.
| Method and path | Notes |
|---|---|
GET /api/settings |
Returns {groups: [{name, label, settings: [Item]}]}. An item has key, env, group, label, help, type, options, secret, value, default, source (db, env or default), locked and read_only. Secrets show only {"set": bool}. |
PATCH /api/settings |
Body {key: value}. null resets a key. "" for a secret means unchanged. Returns 400 {error, fields} for invalid values, and 409 for an env-locked key. A change of backend runs the storage probe first. A change that starts a storage move saves the other keys and returns 202 {move}. It returns 409 while a move runs. |
POST /api/settings/storage/test |
Same body as the PATCH, merged over the current values and not saved. Returns {ok: true}, or 400 {ok: false, error}. |
POST /api/settings/oidc/test |
The same, for SSO discovery. |
GET /api/settings/signing-key |
Returns {public_key, fingerprint}. |
POST /api/settings/signing-key |
Body {armored?}. An empty value generates a new key. 409 when the key comes from gpg.key_file. |
curl -fsS -X PATCH -u admin:PASSWORD https://repo.example.com/api/settings \ -H 'Content-Type: application/json' \ -d '{"log.level":"debug","trusted_proxies":["10.0.0.0/8"]}'curl -fsS -X POST -u admin:PASSWORD https://repo.example.com/api/settings/storage/test \ -H 'Content-Type: application/json' \ -d '{"storage":"s3","s3.bucket":"artifacts"}'Storage moves
Section titled “Storage moves”All routes are admin only. See Moving storage.
| Method and path | Notes |
|---|---|
GET /api/storage/moves/current |
The newest move, running or ended. 404 when there has been none. |
GET /api/storage/moves/{id} |
A move: {id, state, total, copied, skipped, failed, error, started_at, finished_at, target}. state is copying, done, failed or cancelled. It never holds secrets. |
GET /api/storage/moves/{id}/failures?after=&limit= |
{items: [{asset_id, key, reason}], next}. |
DELETE /api/storage/moves/{id} |
Cancels the move and returns it. 409 when it has ended. |
Nexus sources
Section titled “Nexus sources”All routes are admin only. See Nexus migration. Errors are 400 for invalid input or refused credentials, 404, 409 (exists, running, not running, cut over, still mapped) and 502 when Nexus is unreachable.
| Method and path | Notes |
|---|---|
POST /api/nexus/sources |
Body {name, base_url, username, password}. Checks the connection. Returns 201 with the source. |
GET /api/nexus/sources?after=&limit= |
Sources in name order. |
GET /api/nexus/sources/{id} |
The source with its Nexus repos. After cut-over, or when Nexus is unreachable, it lists only the mapped repos and nexus_error says why. |
PATCH /api/nexus/sources/{id} |
Body {base_url?, username?, password?}. Checks the connection. 409 after cut-over. |
PUT /api/nexus/sources/{id}/repos |
Body [{source_repo, target, create}]. Replaces the whole mapping. |
POST /api/nexus/sources/{id}/sync |
Starts a sync. Returns 202. 400 without mapped repos. 409 while a sync runs or after cut-over. |
POST /api/nexus/sources/{id}/cutover |
Runs a final sync and cuts over. Returns 202. |
GET /api/nexus/sources/{id}/syncs?after=&limit= |
Sync history, newest first. |
GET /api/nexus/syncs/{id} |
One sync with per-repo counters. |
GET /api/nexus/syncs/{id}/failures?after=&limit= |
Files that a sync did not import. |
DELETE /api/nexus/syncs/{id} |
Cancels a running sync. 409 when it has ended. |
DELETE /api/nexus/sources/{id} |
Returns 204. 409 while a sync runs or while the source still mirrors repos. |
The server never returns the Nexus password.
Gotchas
Section titled “Gotchas”- Write with the right credential. A password or read token cannot publish. Use a publish session token (
trs_...) as the Basic password. The user name does not matter. - Tokens are shown once. Read tokens, publish session tokens, TOTP secrets and recovery codes cannot be fetched again.
- Pass
nextback unchanged. Treatnextas an opaque cursor. For sync history it is a sync ID, and for Nexus sources it is a source name, but do not build your own. - Use
limitup to 200. A larger value is capped.