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.

Minting without a browser

For a headless or CI machine, or if the dashboard is unavailable, someone holding the deployment's Convex deploy key can mint a token from the command line:

npx convex run --prod ultrareview/operatorTools:mintApiTokenForOrg \
  '{"org":"org_...","label":"ci-agent","scopes":["settings:read"],"actor":"your name"}'

It is held to exactly the same rules as the dashboard (scope catalog, label, expiry bounds, per-org cap), and refuses any org without a live repository connection, so neither a typo nor a fully disconnected org can mint a working credential. The token is printed once by the command and is never written to the function log; treat that output as the secret.

The actor is required but asserted, not authenticated: a deploy key proves possession, never identity, so it is recorded as an operator-asserted claim rather than a verified one. Tokens minted this way appear in the dashboard list and are revoked there like any other.

Revoking without a browser

The same deploy key can kill a token, which is the half that matters when a credential leaks and the dashboard is unavailable:

# by id, when you have it
npx convex run --prod ultrareview/operatorTools:revokeApiTokenForOrg \
  '{"org":"org_...","tokenId":"...","actor":"your name"}'

# or by the token's first 12 characters, which the dashboard shows and which you
# can read off the front of a leaked value
npx convex run --prod ultrareview/operatorTools:revokeApiTokenForOrg \
  '{"org":"org_...","tokenPrefix":"urk_346b7a66","actor":"your name"}'

Never pass the full token. There is no argument that accepts one, on purpose: a command line ends up in your shell history and in the process list, which is a strange place to put a credential you are in the middle of containing. The 12-character prefix is not secret, and it identifies a token as long as it matches exactly one in the organization; if two ever share a prefix, the call refuses and you revoke by tokenId instead. Pasting a full token where a prefix belongs is refused with an instruction rather than a confusing "not found".

The call is org-checked and idempotent, it reports whether that call did the revoking so an already-dead token never reads as a fresh action, and it refuses an ambiguous prefix rather than guessing which token to kill.

When you are not sure which credential is loose, kill them all:

npx convex run --prod ultrareview/operatorTools:revokeAllApiTokensForOrg \
  '{"org":"org_...","actor":"your name"}'

Every revocation writes its own audit row, so the trail still names each token rather than collapsing them into one line.

Scopes

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

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

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.

Point an agent at the API

The API lives on your deployment's .convex.site host, and the base URL is shown on the API tokens tab. The base URL answers a GET with a 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.

curl -H "Authorization: Bearer urk_..." \
  https://your-deployment.convex.site/ultrareview/api/v1

The endpoints

All requests carry Authorization: Bearer urk_.... Bodies are JSON.

Method and pathScopeWhat it does
GET /noneThe self-describing catalog (plus your token's own context when authenticated)
GET /profilesprofiles:readProfiles (authored and built-in), the org default, and repo pins
POST /profilesprofiles:writeCreate a profile
PATCH /profiles/{name}profiles:writeUpdate fields of a profile (rename included)
DELETE /profiles/{name}profiles:writeDelete a profile (refused while a repo pin or lock references it)
POST /profiles/{name}/applyprofiles:applySet the org default, or pin the profile to one repo
GET /repossettings:readConnected repositories with policy summaries
GET /repos/{owner}/{repo}/settingssettings:readOne repo's settings, version, and effective values
PATCH /repos/{owner}/{repo}/settingssettings:writeUpdate repo settings (tri-state: omitted keeps, null clears, value sets)
GET /settingssettings:readOrg settings (everything except profiles)
PATCH /settingssettings:writeUpdate org settings slices

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

BASE=https://your-deployment.convex.site/ultrareview/api/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"

Dry runs

Add ?dryRun=1 to any write and the API runs the complete validation (the same referential-integrity checks the dashboard enforces) and commits nothing. The response carries dryRun: true. Agents should validate risky changes this way before committing them.

Errors

Every error is JSON with a machine code, a human message, and remediation fields, so an agent can self-correct without a human:

{ "error": { "code": "insufficient_scope", "message": "This endpoint requires the profiles:write scope.",
             "requiredScope": "profiles:write", "tokenScopes": ["profiles:read"] } }

Codes: missing_token, invalid_token, token_revoked, token_expired (401); insufficient_scope (403); not_found (404); method_not_allowed (405); conflict, version_conflict, builtin_profile, org_config_unreadable (409); validation_failed (422); rate_limited (429, with retryAfterMs and a Retry-After header); bad_request (400).

Concurrency

Repo settings carry a version. Read it with GET, send it back as expectedVersion in your PATCH, and the API refuses with version_conflict if someone changed the settings in between. Omitting expectedVersion writes against the current state atomically, which is fine for single-step changes.

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 is served: this API is for servers and agents, not browsers.