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 | Your account, the member directory (usernames, emails and roles) and the credit balance. |
repos:read | View repositories and focuses | Repositories, branches, pull requests, tags and focus instructions. |
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. |
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.
- 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 unless you pass--scopes.- 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:
- 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 with Switch organization in the account menu.
Connect the app again. The consent page now names the new workspace.
An id from another workspace gets 404 not_found, even when you belong to both.
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 every operation. The one exception: archiving a context document that someone else created needs an Admin, and a Member gets 403 forbidden. 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 |
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
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 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. | Check organization in GET /me, then see Which organization a token reads. |
429 rate_limited | A budget is spent; details.bucket names it. | Wait Retry-After seconds, then retry. |
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. |