TouchQuill REST API

Machine-readable: /api/v1/openapi.json (OpenAPI 3.0). Authorization: Authorization: Bearer <token>. Errors: {"error":{"code","message"}}. Stable operations change only by addition within /api/v1.

server

GET/internalLanding page (HTML) linking the admin panel, API docs and health
GET/admininternalAdmin panel (single HTML page; it calls the admin API with a token)
GET/healthstableLiveness with version and uptime (old address) deprecated since 2026-10-11, sunset Fri, 01 Oct 2027 00:00:00 GMT; use /api/v1/health
GET/healthzstableProcess is alive (always 200, no details)
GET/.well-known/touchquillinternalPorts of this instance for the requesting host (ADR-0039): grpc, api, server_version — clients find the server from the host name alone
GET/readyzstableReady for traffic: metadata store answers, data directory writable, no maintenance (503 with a reason otherwise)
GET/api/v1/healthstableLiveness with version and uptime
GET/api/v1/openapi.jsonstableThis specification (OpenAPI 3.0)
GET/api/v1/docsstableHuman-readable list of endpoints (HTML, no external assets)
GET/metricsexperimentalPrometheus metrics · needs admin.server

updates

GET/updates/v1/{path}internalOrganisation update mirror: signed manifest and packages (ADR-0018)

auth

POST/api/v1/loginstableLog in with a password (and 2FA code) - returns a session token tqs_*
GET/api/v1/mestableUser and role of the token · needs repo.read

admin

GET/api/v1/admin/usersinternalAccounts with roles and token expiry · needs admin.users
POST/api/v1/admin/usersinternalCreate an account (password by default; technical: token with ttl, scopes) with the repositories it may see {repos} - none by default · needs admin.users
DELETE/api/v1/admin/users/{name}internalRemove an account · needs admin.users
POST/api/v1/admin/users/{name}/passwdinternalSet a password · needs admin.users
POST/api/v1/admin/users/{name}/renameinternalRename the account {name}: the id stays, history shows the new name at once; locks, shelves, the inbox, notification settings, workspaces, reviews, conflicts, issues and stations move to the new name (`moved` in the answer); refused while it has a pending wipe (ADR-0034) · needs admin.users
GET/api/v1/admin/authors/remapinternalAuthor remaps: revisions without an author id whose author text is {from} show the account {to} (ADR-0034) · needs admin.server
POST/api/v1/admin/authors/remapinternalAdd or replace an author remap {from, to: existing account, repo: name or *}; nothing is rewritten, hashes stay · needs admin.server
DELETE/api/v1/admin/authors/remapinternalRemove an author remap · query: from, repo · needs admin.server
POST/api/v1/admin/users/{name}/tokeninternalRotate the token of a technical account (optional ttl) · needs admin.users
POST/api/v1/admin/users/{name}/roleinternalChange the role · needs admin.users
POST/api/v1/admin/users/{name}/logoutinternalEnd all sessions of the account · needs admin.users
POST/api/v1/admin/users/{name}/disableinternalDisable the account (tokens and sessions refused, workspaces locked: access none); {wipe: true} also removes the project files from all its workspaces (ADR-0035) · needs admin.users
POST/api/v1/admin/users/{name}/enableinternalEnable a disabled account (its wipe requests are cleared) · needs admin.users
POST/api/v1/admin/users/{name}/wipeinternalRequest removing the project files of {repo} (or *) from the account's workspaces while it has no access (ADR-0035) · needs admin.users
DELETE/api/v1/admin/users/{name}/wipeinternalCancel a wipe request (?repo=) · needs admin.users
GET/api/v1/admin/wipesinternalWipe requests with their state: pending, or done with host and time · needs admin.users
GET/api/v1/admin/rolesinternalRoles and their permissions · needs admin.roles
POST/api/v1/admin/rolesinternalCreate or change a custom role · needs admin.roles
DELETE/api/v1/admin/roles/{name}internalRemove a custom role · needs admin.roles
POST/api/v1/admin/users/{name}/custom-roleinternalAssign a custom role · needs admin.roles
POST/api/v1/admin/users/{name}/grantinternalGrant or revoke single permissions · needs admin.users
GET/api/v1/admin/users/{name}/reposinternalRepositories the account may see: its own list, the list of its custom role (ADR-0032) · needs admin.users
PUT/api/v1/admin/users/{name}/reposinternalReplace the account's repository list {repos: ["Name", "Name:role", "*"]}; {wipe_removed: true} also removes the project files of the revoked repositories from the account's workspaces · needs admin.users
GET/api/v1/admin/roles/{name}/reposinternalRepositories of a custom role · needs admin.roles
PUT/api/v1/admin/roles/{name}/reposinternalReplace the repositories of a custom role {repos: [...]} · needs admin.roles
GET/api/v1/admin/accessinternalAccess check: per repository - visible and why, the role there, the permissions · query: user, repo · needs admin.users
GET/api/v1/admin/protectionsinternalPath protections (read/write restrictions) · needs admin.roles
PUT/api/v1/admin/protectionsinternalReplace path protections · needs admin.roles
GET/api/v1/admin/patsinternalPersonal access tokens with scopes and expiry · needs admin.users
POST/api/v1/admin/patsinternalIssue a PAT {name, user, scopes, ttl?, repos?} - the token is shown once · needs admin.users
POST/api/v1/admin/scribe/rebuildinternalAsk the server to rebuild a repository's Scribe index (in the background) · query: repo · needs admin.server
GET/api/v1/admin/maintenanceinternalMaintenance mode state · needs admin.server
POST/api/v1/admin/maintenanceinternalTurn maintenance mode on {reason} · needs admin.server
DELETE/api/v1/admin/maintenanceinternalTurn maintenance mode off · needs admin.server
DELETE/api/v1/admin/pats/{name}internalRevoke a PAT · needs admin.users
GET/api/v1/admin/auditinternalAudit log entries · query: limit, event, user, denied, before, since_ms, until_ms, resource, channel, repo, outcome · needs admin.audit
GET/api/v1/admin/telemetryinternalClient timings and errors (ADR-0019) · query: days · needs admin.audit
GET/api/v1/admin/audit/verifyinternalVerify the audit hash chain · needs admin.audit
GET/api/v1/admin/audit/{id}internalOne audit entry (seq or au-*) with verified: hash, signature and link to the previous entry · needs admin.audit
GET/api/v1/admin/config/validateinternalCheck server.toml, ci.toml and users.toml: errors per file, warnings, settings that need a restart · needs admin.server
POST/api/v1/admin/config/reloadinternalCheck the configuration and apply what works without a restart (422 and nothing applied on errors) · needs admin.server
GET/api/v1/admin/protectioninternalConnection protection (ADR-0039): [protection] settings, rejection counters, banned addresses, login lockouts · needs admin.server
PATCH/api/v1/admin/protectioninternalChange [protection] settings at once (saved in server.toml; audited protection.settings) · needs admin.server
POST/api/v1/admin/protection/unbaninternalLift the ban of an address {ip} (audited protection.unbanned) · needs admin.server
GET/api/v1/admin/settingsinternalEvery server setting: effective value, source (flag, server.toml, env, default), when a change applies (restart, reload, panel) and whether the panel can change it · needs admin.server
PUT/api/v1/admin/configinternalChange a setting that config reload applies {key, value}: written to server.toml, checked, reloaded (422 and server.toml restored on errors; value null = default) · needs admin.server
GET/api/v1/admin/statusinternalServer status for the dashboard · needs admin.server
GET/api/v1/admin/licenseinternalServer license (ADR-0029): state and reason, licensee, dates, limits, instance fingerprint, last check-in, lease · needs admin.server
PUT/api/v1/admin/licenseinternalInstall a license key {key}: verified offline, saved to the license file, check-in in the background (422 for an invalid key) · needs admin.server
POST/api/v1/admin/license/checkininternalLicense check-in now; the state afterwards with checkin_ok and checkin_error · needs admin.server
POST/api/v1/admin/license/releaseinternalRelease this server instance at the license server (moving to another machine): the key file is moved to <file>.released and the server becomes unlicensed; installing a key registers it again (502 when the license server refuses or is unreachable, 409 for a key in server.toml) · needs admin.server
GET/api/v1/admin/stationsinternalClient stations seen in the last 30 days (ADR-0029): account, computer name, OS user, IP, OS, client version, product, first and last seen; the station limit from the license · needs admin.server
DELETE/api/v1/admin/stations/{key}internalFree a station's place (account@computer); it counts as new on its next use · needs admin.server
GET/api/v1/admin/repairsinternalRepair tasks (ADR-0035): newest first, filters ?account= and ?host=; state pending/delivered/done/rejected/expired/cancelled and the station's result · needs admin.server
POST/api/v1/admin/repairsinternalCreate a repair task for a station {account, hostname, repo, kind: tq|script, commands | script {name, body}, note}; signed with the instance key, runs on the station without asking the user · needs admin.server
GET/api/v1/admin/repairs/{id}internalOne repair task with its full content (script body) and signature · needs admin.server
DELETE/api/v1/admin/repairs/{id}internalCancel a repair task that has not reached the station yet (pending) · needs admin.server
GET/api/v1/admin/diagnosticsinternalDiagnostics bundles (ADR-0036): account, host, time, product, version, reason, size, repo; filters ?account=, ?host=, ?repo= · needs admin.server
POST/api/v1/admin/diagnostics/requestinternalAsk a station for a diagnostics bundle {account, hostname, repo?, note?}; delivered at its next contact, expires after 7 days · needs admin.server
DELETE/api/v1/admin/diagnostics/requestinternalCancel a pending diagnostics request (?account=&hostname=) · needs admin.server
GET/api/v1/admin/diagnostics/requestsinternalPending diagnostics requests · needs admin.server
GET/api/v1/admin/diagnostics/{id}internalOne diagnostics bundle: metadata, a readable summary and the JSON · needs admin.server
DELETE/api/v1/admin/diagnostics/{id}internalDelete a diagnostics bundle · needs admin.server
GET/api/v1/admin/diagnostics/{id}/downloadinternalDownload a diagnostics bundle as a JSON file · needs admin.server
PATCH/api/v1/admin/repos/{repo}/channels/{channel}internalSet a channel's parent and/or type (body {parent?, type?}; parent null = none, only for main and release; the parent must be an open, non-virtual channel not based on this one). For channels created before v0.4.0, which have no parent on the server; audited as channel.updated · needs admin.server
POST/api/v1/repos/{repo}/channels/{channel}/rewindinternalRemove the revisions after `to` from a channel's history (ADR-0031; global server-admin role, audited; body {to, reason, dry_run, expected_head, affected_channels, record_manual, from_hash, hashes, from_seq}) · needs channel.rewind
GET/api/v1/repos/{repo}/channels/{channel}/rewindsinternalRevisions removed from a channel by an administrator, newest first, with reason and author (ADR-0031), the copies that had them and the protection state (ADR-0037) · query: limit · needs channel.rewind
POST/api/v1/repos/{repo}/channels/{channel}/rewinds/{id}/allow-contentinternalAllow the content removed by a rewind back: pushes with it stop being refused; the revisions stay removed (ADR-0037) · needs channel.rewind
GET/api/v1/admin/workspacesinternalWorkspaces of every account in the repositories where the caller has workspace.list-all (role in that repository), elsewhere the caller's own; each entry carries can{list_all, edit_any, force_unlock}; 403 without workspace.list-all anywhere (admin panel) · needs repo.read

repos

GET/api/v1/reposstableRepositories visible to the caller · needs repo.read
GET/api/v1/repos/{repo}stableOne repository: channels, abandoned ones, default channel and its head · needs repo.read

channels

GET/api/v1/repos/{repo}/channelsstableChannels with type and parent, plus abandoned ones · needs repo.read
GET/api/v1/repos/{repo}/channels/{channel}stableOne channel (name percent-encoded, task%2Fx): type, parent, state, owner, ticket, head, locks, open conflicts, review gate · needs repo.read

revisions

GET/api/v1/repos/{repo}/changes/{change_id}stableA logical change (ch-*): current revision with files and earlier materializations · needs repo.read
GET/api/v1/repos/{repo}/revisionsstableRevisions of a channel, newest first, cursor pagination · query: limit, page_token, channel · needs repo.read
GET/api/v1/repos/{repo}/revisions/{rev}stableOne revision (number without rev:, or ch-*) with its files · needs repo.read
PATCH/api/v1/repos/{repo}/revisions/{rev}/messageexperimentalChange the description of a revision already on the server {message} (like p4 change -u): the hash, Change ID and number stay, no new revision. Your own revisions need repo.write, other people's revision.describe (lead, server-admin) or `describe` in the channel's [permissions]; the server policy message_require applies; audited as revision.message_edited. Returns the description state as in …/message/history; 409 on a replica or proxy · needs repo.read
GET/api/v1/repos/{repo}/revisions/{rev}/message/historyexperimentalDescription of a revision: current text, message_edited {by, by_id, at_ms, versions} (null = never changed), original_message and previous versions newest first (at most 20) · needs repo.read
GET/api/v1/repos/{repo}/revisions/{rev}/flagsexperimentalBuild flags of a revision and whether it is stable · needs repo.read

ugs

GET/ugs/api/latestexternalUnrealGameSync metadata protocol version ({version: 2})
GET/ugs/api/metadataexternalUGS/Horde build metadata of changes · query: stream, minchange, maxchange, change, project, sequence · needs repo.read
POST/ugs/api/metadataexternalUGS UpdateMetadataRequest: sync, vote, comment, badges (badges need build-flag.set) · needs repo.read

browse

GET/api/v1/repos/{repo}/tree/{rev}experimentalAll files at a revision (HEAD, number or commit hash) with size and blob hash · needs repo.read
GET/api/v1/repos/{repo}/file/{rev}experimentalFile content at a revision (text preview) · query: path · needs repo.read
GET/api/v1/repos/{repo}/filediff/{rev}experimentalDiff of one file against the parent commit or base · query: path, base · needs repo.read
GET/api/v1/repos/{repo}/diff/{rev}experimentalFiles changed by a revision against the parent commit or base · query: base · needs repo.read
GET/api/v1/repos/{repo}/raw/{rev}stableRaw bytes of a file (application/octet-stream), outside the request limit · query: path · needs repo.read

locks

GET/api/v1/repos/{repo}/locksstableLocks of a channel with queue, requests and disputes · query: channel · needs repo.read
POST/api/v1/repos/{repo}/locksstableAcquire a lock {path, channel?} · needs lock.acquire
POST/api/v1/repos/{repo}/locks/activityexperimentalReport local edits of files you hold locks on {paths, channel?} — the activity timeout of those locks starts over (TQ-44); returns {confirmed: [{path, timeout_ms}]} · needs lock.acquire
DELETE/api/v1/repos/{repo}/locks/{path}stableRelease a lock (the path is percent-encoded) · query: channel · needs lock.acquire

conflicts

GET/api/v1/repos/{repo}/conflictsstableConflict records · query: channel · needs repo.read
GET/api/v1/repos/{repo}/conflicts/{id}stableFull conflict record · needs repo.read
POST/api/v1/repos/{repo}/conflicts/{id}/assignexperimentalAssign a conflict record kept on the server to a person or role:<role> ({to, message?}) · needs conflict.assign

review

GET/api/v1/repos/{repo}/reviewexperimentalReview comments, verdicts, gate and the pending review request of a channel · query: channel · needs repo.read
POST/api/v1/repos/{repo}/reviewexperimentalAdd a review comment · needs repo.read
POST/api/v1/repos/{repo}/review/verdictexperimentalApprove or request changes (not on your own change) · needs review.verdict

shelves

GET/api/v1/repos/{repo}/shelvesstableShelves visible to the caller · query: channel · needs repo.read
GET/api/v1/repos/{repo}/shelves/{id}stableShelf record with files · needs repo.read
GET/api/v1/repos/{repo}/shelves/{id}/rawexperimentalBytes of a shelved file · query: path · needs repo.read

workspaces

GET/api/v1/repos/{repo}/workspacesexperimentalWorkspaces with machines (online, last seen, offline since, files being edited, locks held): yours; all=1 every account, owner=<account> one account (other accounts: workspace.list-all) · query: all, owner, unregistered · needs repo.read
PUT/api/v1/repos/{repo}/workspaces/{name}experimentalChange (or register) a workspace definition {channel?, profile?, sparse?}; someone else's (owner=) needs workspace.edit-any and is audited · query: owner · needs repo.read
DELETE/api/v1/repos/{repo}/workspaces/{name}experimentalRemove a workspace from the registry (files on the machines stay); someone else's (owner=) needs workspace.edit-any and is audited · query: owner · needs repo.read
POST/api/v1/repos/{repo}/workspaces/{name}/force-unlockexperimentalForce-unlock a machine of a workspace {machine}: its files being edited stop being read-only elsewhere (audited) · query: owner · needs lock.steal
GET/api/v1/workspacesexperimentalYour workspaces in every repository you see; each entry carries can{list_all, edit_any, force_unlock} for its repository, can.list_all on top = workspace.list-all in at least one repository · needs repo.read

webhooks

GET/api/v1/repos/{repo}/webhooksstableWebhook subscriptions · needs webhook.manage
POST/api/v1/repos/{repo}/webhooksstableSubscribe {url, events, channels?, secret?, kind?} - HMAC-SHA256 signed deliveries · needs webhook.manage
DELETE/api/v1/repos/{repo}/webhooks/{id}stableDelete a subscription · needs webhook.manage
GET/api/v1/repos/{repo}/webhooks/{id}/deliveriesstableDelivery attempts of a subscription · needs webhook.manage
POST/api/v1/repos/{repo}/webhooks/{id}/redeliver/{seq}stableDeliver an event again · needs webhook.manage

events

GET/api/v1/repos/{repo}/eventsstableServer-sent events with id: and Last-Event-ID resume · query: types, events, channels, last_event_id · needs repo.read

notifications

GET/api/v1/repos/{repo}/notify/prefsstableNotification preferences of the caller · needs repo.read
PUT/api/v1/repos/{repo}/notify/prefsstableSet notification preferences of the caller · needs repo.read
POST/api/v1/repos/{repo}/notify/testexperimentalTest notification for the caller {event?, email?}: the event template in the account language with example data goes to the inbox (and e-mail) · needs repo.read

scribe

GET/api/v1/repos/{repo}/scribe/symbolsstableSymbols of a file (also Blueprints) · query: path, rev · needs scribe.read
GET/api/v1/repos/{repo}/scribe/blamestableHistory of a symbol across renames and moves · query: symbol · needs scribe.read
GET/api/v1/repos/{repo}/scribe/traceexperimentalFull lineage of a symbol · query: symbol · needs scribe.read
POST/api/v1/repos/{repo}/scribe/linkexperimentalLink two symbols by hand {newer, older} (resolves Ambiguous) · needs repo.write
DELETE/api/v1/repos/{repo}/scribe/linkexperimentalRemove a manual link · query: symbol · needs repo.write
GET/api/v1/repos/{repo}/scribe/searchexperimentalFind symbols by name (exact, prefix, substring; case-insensitive) · query: q, kind, lang, path, rev · needs scribe.read
GET/api/v1/repos/{repo}/scribe/usagesexperimentalWhere a symbol or asset is used (code and Blueprint nodes) · query: symbol, rev · needs scribe.read
GET/api/v1/repos/{repo}/scribe/timelineexperimentalNames, files and authors of a symbol over time · query: symbol · needs scribe.read
GET/api/v1/repos/{repo}/scribe/diffexperimentalSemantic diff of two revisions: symbols added, removed, changed, renamed, moved · query: from, to, path · needs scribe.read
GET/api/v1/repos/{repo}/scribe/annotateexperimentalPer-symbol blame of a file · query: path, rev · needs scribe.read
GET/api/v1/repos/{repo}/scribe/relationsexperimentalTyped links of a symbol (must-sync, derived-from, replaces, depends-on) · query: symbol · needs scribe.read
GET/api/v1/repos/{repo}/scribe/notesexperimentalNotes attached to a symbol · query: symbol · needs scribe.read
POST/api/v1/repos/{repo}/scribe/notesexperimentalAttach a note to a symbol {symbol, tag, note} · needs repo.write
DELETE/api/v1/repos/{repo}/scribe/notesexperimentalRemove a note · query: symbol, tag · needs repo.write

commits

PUT/api/v1/repos/{repo}/blobsstableUpload file bytes (up to 1 GiB) for a server-side commit · query: path · needs repo.write
POST/api/v1/repos/{repo}/commitsstableServer-side commit from uploaded blobs {channel, message, changes, deletes} · needs repo.write