API tokens

Give an AI agent scoped access to ultrareview: mint a token, point the agent at the self-describing API, and let it manage review settings and profiles.

Ultrareview's configuration can be driven by machines, not just the dashboard. An org admin mints a scoped API token, hands it to an agent (an AI assistant, a CI job, a script), and that agent can read and update review settings, create review profiles, and apply them, over a plain JSON API. Everything the agent does runs through the exact same validation and audit trail a dashboard edit gets.

Create a token

  1. Open Configuration, then the API tokens tab in the ultrareview dashboard. Only org admins can manage tokens.
  2. Choose Create token, name it after the client that will hold it, pick its scopes, and pick an expiry (90 days by default).
  3. Copy the token from the one-time reveal. It starts with urk_ and is shown exactly once; only its hash is stored.

Revoke a token from the same list at any time. Revocation is immediate, and the row stays behind, marked Revoked and still showing when it was created and last used, as audit context.

When the dashboard is unavailable

Tokens can also be minted and revoked out of band, without a browser, by someone with operator access to your installation. That path is held to exactly the same rules as the dashboard (the same scope catalog, the same label and expiry bounds, the same per-organization cap) and it refuses any organization without a live repository connection, so neither a typo nor a disconnected organization can produce a working credential. Tokens created that way appear in this list and are revoked here like any other.

It is deliberately the narrower door: possession of an operator credential proves possession, never identity, so the person named on such a token is recorded as an asserted claim rather than a verified one.

If you need a token minted or, more urgently, a leaked one killed while the dashboard is down, contact support.

Scopes

A token holds only the scopes you grant it, and each endpoint demands exactly one scope. No scope implies another.

reviews:run is separate from reviews:read on purpose. Reading a review that already happened costs nothing; asking for a new one bills your provider key and can pin a required check on a pull request. A build token that reads a verdict has no business starting runs, so neither scope implies the other.

reviews:read deserves a word of its own. A review is a severity-ranked list of an organization's known unfixed defects, with file and line numbers, so it is the most sensitive read here and nothing else grants it. A token that manages settings cannot read reviews unless you say so. Note also that a token's scopes are fixed when it is minted: to give an existing token this scope, mint a new one and retire the old.

ScopeGrants
profiles:readRead profiles, built-ins, the org default, and per-repo pins
profiles:writeCreate, edit, and delete profiles
profiles:applyApply a profile (org default or a repo pin), and nothing else
settings:readRead org settings, connected repositories, and per-repo settings
settings:writeUpdate org settings and per-repo settings
reviews:readRead reviews, their findings, and the state of a review run
reviews:runStart a review of a pull request. This spends your model budget
context:readRead one repository's review context: resolved configuration, locks, notes, muted rules, statistics

context:read is its own grant for the same reason. The context bundle carries your configuration and your locks, which settings:read already covers, and it also carries your repository notes, the rules you muted and the accept-rate statistics behind them, which it does not. Serving those under a settings scope would have quietly widened every settings token you minted months ago, with nobody asked and nothing recorded, so it asks for a grant of its own. It reads only: it spends nothing and starts nothing.

One honest edge: a repo's pinned profile is itself a repo setting, so settings:write can also change pins through the settings endpoint. profiles:apply exists as the narrow grant for a token that should only switch profiles.

There is deliberately no scope for managing tokens. The API can never mint, list, or revoke credentials; that stays in the dashboard, behind your login.

Limit a token to repositories

A token acts for the whole organization unless you say otherwise. When a teammate owns one repository, give them a token limited to it: in the create dialog, choose Only these repositories and pick them. The token then:

  • can read and change settings, pins and routing for those repositories, and nothing else;
  • cannot create, edit or delete profiles, change organization settings, or set the organization default, even if it holds the scope for it, because those reach every repository;
  • still reads the profile list and organization settings, so it can see what it may route to;
  • lists only its repositories under GET /v1/repos, and names them when asked GET /v1/token.

Anything organization-wide is refused with repo_not_allowed (403) and a sentence saying so. A repository outside the limit answers exactly like one that does not exist, a 404 with the same sentence, so a limited token cannot learn which repositories exist by probing for a different answer.

Point an integration at the API

Your base URL is shown on the API tokens tab; copy it from there. On the hosted service it is https://api.mobsession.ai; a self-hosted installation shows its own API host instead. Either way, GET /v1 on that host returns the full machine-readable catalog: every endpoint, the scope it needs, example bodies, and the error code table. An agent holding only the base URL and a token can discover the rest by itself.

Three documents describe it, and none of them needs a token:

curl https://api.mobsession.ai/            # the root: what this is, and where the rest lives
curl https://api.mobsession.ai/v1          # the catalog: every endpoint, scope, and error code
curl https://api.mobsession.ai/v1/openapi.json   # the full OpenAPI 3.1 document

For a person rather than an agent, that last document is rendered as the browsable API reference: every endpoint with its parameters, schemas, examples and ready-to-paste code samples. It is generated from the specification above, so the two can never disagree.

To check what your own token may do:

curl -H "Authorization: Bearer urk_..." https://api.mobsession.ai/v1/token

The endpoints

Protected requests carry Authorization: Bearer urk_...; the three public documents and the health check take no token. Bodies are JSON, and a body sent without content-type: application/json is refused.

Method and pathScopeWhat it does
GET /noneThe root document: name, docs, versions
GET /healthnone200 while the API is serving
GET /v1noneThe machine catalog: endpoints, scopes, error codes, limits
GET /v1/openapi.jsonnoneThe OpenAPI 3.1 document
GET /v1/tokenany tokenWhat your token may do, and when it expires
GET /v1/profilesprofiles:readList profiles, authored and built-in
POST /v1/profilesprofiles:writeCreate a profile
GET /v1/profiles/{name}profiles:readRead one profile
PATCH /v1/profiles/{name}profiles:writeUpdate a profile (rename included)
DELETE /v1/profiles/{name}profiles:writeDelete a profile (refused while a pin or lock points at it)
POST /v1/profiles/{name}/applyprofiles:applyMake it the org default, or pin it to one repository
GET /v1/repossettings:readList connected repositories
GET /v1/repos/{owner}/{repo}settings:readOne repository's settings, version, and effective values
PATCH /v1/repos/{owner}/{repo}settings:writeUpdate repository settings
POST /v1/repos/{owner}/{repo}/routing/previewsettings:readWhere routing rules would send files, without writing them
POST /v1/repos/{owner}/{repo}/contextcontext:readThe review context for one repository, for a review running outside our service
GET /v1/runs/{runId}reviews:readWhat became of one review you asked for
GET /v1/runs/{runId}/findingsreviews:readWhat that review found, refusing a verdict about a different commit
GET /v1/runsreviews:readRecent reviews across the organization, newest first
GET /v1/repos/{owner}/{repo}/pulls/{pr}/runsreviews:readEvery review of one pull request, newest first
POST /v1/repos/{owner}/{repo}/reviewsreviews:runStart a review. Returns a run id immediately; the work happens on our side
POST /v1/local-reviewsreviews:runRecord that a review ran on somebody's own machine. A count and an outcome, never code, and only when your organization has opted in
GET /v1/settingssettings:readOrganization settings (everything except profiles)
PATCH /v1/settingssettings:writeUpdate organization settings

A repository is always addressed as owner/name, matched without regard to case, exactly as GitHub spells it. Each endpoint's parameters, request body and responses are in the API reference.

A worked example: create a strict security profile and apply it to a repository.

BASE=https://api.mobsession.ai/v1
AUTH="Authorization: Bearer urk_..."

curl -X POST -H "$AUTH" -H "content-type: application/json" \
  -d '{"name":"strict-security","dimensions":["security","correctness"],"verificationStrictness":2}' \
  "$BASE/profiles"

curl -X POST -H "$AUTH" -H "content-type: application/json" \
  -d '{"target":"repo","repo":"acme/storefront"}' \
  "$BASE/profiles/strict-security/apply"

Try routing before you write it

Routing rules are first-match-wins: the first rule that matches a file decides which profile reviews it, and later rules are never consulted for that file. So a broad rule listed ahead of a narrow one makes the narrow one dead, and nothing tells you. Put the specific path before the general one.

The preview endpoint says which rule wins for each path, and which of your rules never fire, before you write anything:

curl -X POST -H "$AUTH" -H "content-type: application/json" \
  -d '{"paths":["auth/login.ts","README.md"],
       "routing":[{"when":{"paths":["**/*.ts"]},"use":"default"},
                  {"when":{"paths":["auth/**"]},"use":"strict-security"}]}' \
  "$BASE/repos/acme/storefront/routing/preview"

The answer names the winning rule for each path and reports the second rule as unreachable, because the first one already claimed every TypeScript file. Swap the two and both fire.

Leave routing out to see what the repository does today, rules and organization locks included. The preview runs the same router a review runs and holds your rules to the same validation a write does, so a rule it accepts is a rule you can save, and a plan it shows is the plan a review would follow. It writes nothing.

Review context, for a review that runs somewhere else

A review run outside our service knows nothing about your organization unless you hand it something. Without that it re-raises every rule you muted, ignores the notes you wrote, and reads none of your locks, so it is louder than the review the same code gets on a pull request and disagrees with it. One call fixes that:

curl -X POST -H "$AUTH" "$BASE/repos/acme/storefront/context"

You get back the configuration a review here would run on with your overrides already composed in, the policy locks over it, the repository's notes, the rules you approved for muting, the accept-rate statistics behind them, the value statistics a review prints as a receipt, the five options a review is handed rather than reads for itself, and your provider policy. It writes nothing, spends nothing and starts nothing.

The repository comes from the path and from nowhere else. This endpoint takes no request body, and sending one is refused rather than ignored, so a repository named in a body can never be used instead of the one your token is allowed to read.

Three things it does not give you, on purpose. The last reviewed commit, the baseline pinned to it, and the previous review's findings are all keyed on a pull request number, and a review on somebody's laptop does not have one. So such a review is always a full look at the branch rather than an increment, and it starts with no memory of what a previous review said. The findings are the one worth spelling out: they are a ranked list of your known unfixed defects with file and line numbers, and there is no good reason for that list to be sitting on a laptop.

Two lists in the bundle stop at 500 rows: your approved mutes and your rule statistics. Neither one pages, because the whole point is that this is one call, so instead the response says truncated when it hit the bound and a client can tell you the review it is about to run will be noisier than the one on your pull request.

What comes back

A read of a resource and a write to it answer with the same object, so you never have to follow a write with a read. A write adds one key, dryRun.

Lists page the same way everywhere: ?limit= and ?cursor=, answering { "data": [...], "hasMore": false, "nextCursor": null }. Pass nextCursor back as cursor for the next page and treat it as opaque. A limit above 100 is refused rather than quietly clamped, because a caller that asked for 500 and silently got 100 believes it has the whole list.

Dry runs

Add ?dryRun=1 to any write. It runs the complete validation, including the referential-integrity checks the dashboard enforces, answers with the state it WOULD have produced, and commits nothing. The response carries dryRun: true.

A query parameter an endpoint does not accept is refused with invalid_query_parameter, and so is a dryRun value that is not 1, true, 0 or false. That rule exists for one reason: a mistyped ?dry_run=1 that silently committed the write is the accident a dry run is supposed to prevent.

Errors

Every error is JSON in one shape: a machine code, a human message, details carrying whatever helps you fix it, and a link to this page.

{ "error": { "code": "insufficient_scope",
             "message": "This endpoint requires the profiles:write scope.",
             "details": { "requiredScope": "profiles:write", "tokenScopes": ["profiles:read"] },
             "docs": "https://mobsession.ai/docs/api-tokens" } }

Codes: token_missing, token_invalid, token_revoked, token_expired (401); insufficient_scope (403); malformed_json, invalid_query_parameter (400); not_found (404); method_not_allowed (405); conflict, version_conflict, builtin_profile, org_config_unreadable (409); payload_too_large (413); unsupported_media_type (415); validation_failed (422); rate_limited (429, with details.retryAfterMs and a Retry-After header); internal_error (500, with a requestId to quote to support).

A 401 also names the reason in WWW-Authenticate, and a 403 names the scope it wanted, so a client that reads headers can decide whether to retry or stop without parsing the body.

Concurrency

Repository settings carry a version. Read it, send it back as expectedVersion in your PATCH, and the API refuses with version_conflict if anything changed in between. Omitting it writes against the current state atomically, which is fine for a single-step change; a multi-step agent should send it.

Security model

  • Tokens are hashed at rest (SHA-256) and shown once. A database leak exposes no usable credential.
  • Each token is bound to one organization and can never act outside it.
  • Requests are rate limited per token: 120 requests and 30 writes per minute.
  • Every write lands in the org audit trail naming the token, and settings history shows API writes distinctly from human edits.
  • Reviews' provider keys (your Anthropic key and friends) are not reachable through this API, read or write.
  • No CORS headers are sent on anything that takes a token, so a browser page can never call this API with your credential. The three public documents and the health check do send them, because a specification nobody can fetch is not much of a specification.

Related

  • API reference: every endpoint, generated from the specification the API serves.
  • Security model: how sessions, tokens and data are protected across the product.