Authentication and permissions
API keys and OAuth, scopes, organization binding, rate limits, and fixing 401, 403 and 429.
Every request to the REST API or the MCP server carries one token. This page covers which tokens work, what a token may do, and why a request is refused.
Credentials
Two kinds of token work:
- API keys start with
v12p_. You create them in Settings → Developer; see Create an API key. They suit scripts and CI. - OAuth access tokens start with
v12a_. MCP clients and thev12CLI get one when you sign in through the browser and approve the consent page.
Send the token in the Authorization header as Bearer <token>. The API looks nowhere else: a token in a cookie or in the query string is ignored.
Scopes
A token's scopes decide which operations it can call:
| Scope | Label | Allows |
|---|---|---|
user:read | View account and members | The member directory: usernames, emails and roles. Any key reads its own account, organization and credit balance without it. |
repos:read | View repositories and Steers | Repositories, branches, pull requests, tags and Steer instructions. |
repos:write | Add repositories | Add public repositories to the workspace; owners can also add private repositories granted by GitHub, create repositories V12 hosts and push code to them. |
runs:read | View runs | Runs, their progress and context documents. |
runs:write | Create runs | Estimate and start runs; add or archive context documents. |
runs:manage | Manage runs | Cancel runs. |
runs:share | Share runs | Create, change and revoke guest links that open a run to people outside the organization. |
findings:read | View findings | The inbox: findings, their status, reports and comments. |
findings:write | Edit findings | Change status, comment and update triage decisions. |
- The Create key dialog starts with no scope ticked, and a key needs at least one.
GET /api/v2/me(the MCP toolget_me) needs no scope: any valid token reads its own user, workspace and token.- Starting a run spends credits, so the consent page marks Create runs with Uses credits.
GET /runs/{run}/findings(the MCP toollist_run_findings) needs bothruns:readandfindings:read.- An MCP client sees only the tools its token's scopes allow.
v12 auth loginasks for every scope butruns:shareunless you pass--scopes.runs:shareis opt-in: a key or app holds it only when it asks for it by name. An omitted scope, the MCP sign-in challenge, the MCP server's discovery metadata and the Full access key preset never include it.- Creating or changing a guest link also needs
findings:read, and acommentortriagelink needsfindings:write, so a token never makes a link that does more than it can. Listing and revoking links need onlyruns:share. - A missing scope gets 403
insufficient_scope.details.requiredlists the scopes the operation needs, and so doesscopein theWWW-Authenticateheader.
The REST reference lists the scopes each endpoint needs.
Which organization a token reads
Each token reads exactly one workspace, and the API cannot switch it. Every REST path names that workspace by its slug, /api/v2/orgs/<org>/…; organization.slug in GET /api/v2/me gives it. See Addresses.
- An API key reads the workspace that was active when you created it.
- An OAuth token reads the workspace named on the consent page: the one active in the app when you signed in.
The consent page shows:
- the app: Authorize V12 CLI for V12's own CLI, or "app wants to access V12" with a Third-party app tag for anything else;
- your username and email, with Not you? to sign in as someone else;
- the workspace's name, marked "Personal workspace" or "Organization";
- the scopes the app asks for, grouped under Read and Act;
- Cancel and Authorize followed by the app's name.
The consent page has no workspace picker. To connect an app to another workspace:
If the app is already connected, revoke it under Settings → Developer → Authorized apps while its current workspace is active.
Switch to the other workspace: open the account menu at the bottom of the sidebar and pick it under Organizations.
Connect the app again. The consent page now names the new workspace.
A path that names another workspace's slug gets 404 not_found, even when you belong to both, and so does a finding, run or document of another workspace. A slug your workspace had before it was renamed still works. MCP takes no slug: it always acts on the token's workspace.
Organization permissions
A token can do only what its user's role allows in the workspace, and the API checks the role on every request, so a role change applies at once.
Admins and Members can both call almost every operation. Three exceptions get 403 forbidden: adding a private repository needs an Admin, archiving a context document that someone else created needs an Admin, and listing or changing a run's guest links needs an Admin or the run's owner. In GET /members, Admins have the role owner.
When access ends
The API answers 401 unauthenticated when:
- the API key was revoked;
- the OAuth token expired, or its app was revoked under Authorized apps, which "invalidates its tokens immediately";
- you changed or reset your password, which revokes every API key and OAuth token you hold, in every workspace;
- your membership in the workspace was removed;
- your account was suspended.
v12 auth logout revokes the CLI's OAuth tokens before it deletes them from your machine.
Rate limits
Budgets count per user, across all of your tokens and across REST, MCP and the CLI. The ip budget counts per IP address instead, before the token is checked.
| Bucket | Limit | Window | What counts |
|---|---|---|---|
ip | 3,000 | 1 minute | Every REST and MCP request |
mcp | 1,500 | 1 minute | Every authenticated MCP request |
reads | 1,200 | 1 minute | Operations that read |
findings:write | 120 | 1 minute | Finding changes and comments; a bulk change counts once per finding |
runs:estimate | 30 | 1 hour | Run estimates |
runs:write | 20 | 1 hour | Run starts |
runs:manage | 60 | 1 minute | Run cancellations and guest link changes |
repos:write | 60 | 10 minutes | Adding repositories, through the API or in the app |
documents | 60 | 10 minutes | Upload slots, new documents and archiving |
- Each response reports the bucket it charged in
X-RateLimit-Limit,X-RateLimit-RemainingandX-RateLimit-Reset, the seconds until the window resets. - Over a limit, the API answers 429
rate_limitedwith aRetry-Afterheader, anddetails.bucketnames the bucket. - Run estimates, run starts and document operations also share a limit of 60 a minute per user with the same actions in the app.
- If the rate limiter itself is down, reads carry on, but
repos:write,findings:write,runs:estimate,runs:write,runs:manageanddocumentsanswer 503rate_limit_unavailablewithRetry-After: 30.
Troubleshooting
Every error body carries a requestId. Keep it when you report a problem.
| Symptom | Cause | Fix |
|---|---|---|
401 unauthenticated, and WWW-Authenticate has no error | No Authorization header reached the API. | Send Authorization: Bearer <token>. |
400 invalid_request with details.field: "Authorization" | The header is not exactly Bearer and one token: another scheme, a comma, or extra text. | Send one token after Bearer . |
401 unauthenticated with error="invalid_token" | The token is not a V12 token, or it was revoked or expired; a password change or reset revokes all of yours. Or your membership was removed, or your account suspended. | Create a new key, or sign in again. |
403 insufficient_scope | The token lacks a scope that details.required names. | Create a key with those scopes, or connect the app again and approve them. |
403 insufficient_scope naming repos:write | Keys and app connections approved before V12 offered Add repositories do not have it, and a refreshed token keeps the scopes it was approved with. | Create a key with Add repositories, or sign in again (v12 auth login, or connect the app again) and approve it. |
403 forbidden | Your role does not allow the action. | Ask an Admin. |
404 not_found for something you can see in the app | The token reads another workspace, or the path names a slug other than the token's workspace. | Check organization.slug in GET /api/v2/me and use it in the path, then see Which organization a token reads. |
404 repository_not_added from an estimate or start | The run names a repository that is not in the workspace. | Add it with POST /repositories or the MCP tool add_repository, then estimate again. |
404 repository_not_visible | GitHub shows V12 no public repository by that name, and the workspace's GitHub connection does not include it. | Check the name. If it is private, follow details.recovery, or ask an Admin to give V12's GitHub App access to it. |
429 rate_limited | A budget is spent; details.bucket names it. With details.source: "github", GitHub is limiting V12's requests instead, and the answer has no Retry-After. | Wait Retry-After seconds, then retry; for GitHub, try again in a few minutes. |
503 public_api_disabled | The API is turned off for now; the app still works. | Retry later. |
503 platform_unavailable | A V12 service behind the API is unavailable, did not answer, or is rate limiting V12. | Wait Retry-After seconds (also in details.retryAfterSeconds) when present, else retry later. |
503 rate_limit_unavailable | The rate limiter is down, so writes and spending pause. | Retry after 30 seconds. |
500 internal_error | An unexpected failure inside V12. | Retry once; if it persists, report it with the requestId. |