Scopes and permissions
All 17 nima scopes, what each unlocks, who can grant it, where it can be granted, and how requests are checked.
Every API key and every OAuth access token carries a list of scopes. A scope is written resource:verb, and it is the only thing that opens an operation to a token. An operation that declares no scope is closed to tokens entirely, however valid the token is. That is why signing in, managing members and minting keys can only be done from a signed-in session.
nima has 17 scopes. Nothing you send can widen a token beyond them.
Scopes are on for every workspace. What differs is where each one can be granted: 8 are offered to MCP clients on the OAuth consent screen, 13 can be put on a key in Settings → API keys, and 4 can only be minted from Slack or not at all. The table below says which.
The 17 nima scopes
The granter column is the workspace permission the person creating the key, or approving the consent screen, must already hold. Owners and admins hold every permission. Members hold a subset, so sources:read, documents:write, integrations:write and, in practice, operations:write are out of their reach.
| Scope | Unlocks | Granter must hold | Member can grant | MCP client consent | Settings and POST /v1/api-keys |
|---|---|---|---|---|---|
knowledge:read | Ask the knowledge graph, search code, blast radius, data model | READ_CONTENT | Yes | Yes | Yes |
signals:read | Read signals, beacons, facet counts and the event history | READ_CONTENT | Yes | Yes | Yes |
sources:read | The original document behind a signal, fetched live | READ_SOURCE_DOCUMENTS | No | Yes | Yes |
documents:read | Read specs, markdown, delivery state and agent operations | READ_CONTENT | Yes | Yes | Yes |
signals:write | File signals | CREATE_CONTENT | Yes | Yes | Yes |
subscriptions:read | Event-type catalog, your watches, scheduling context, your scheduled messages | READ_CONTENT | Yes | Yes | Yes |
subscriptions:write | Create, change and pause watches; schedule, edit and cancel messages | READ_CONTENT, UPDATE_CONTENT, USE_AI_GENERATION | Yes | Yes | Yes |
inbox:read | Your nima inbox | READ_CONTENT | Yes | Yes | Yes |
documents:write | Create, edit, promote and delete specs | CREATE_CONTENT, UPDATE_CONTENT, DELETE_CONTENT | No | No | Yes |
versions:read | Spec version history | READ_CONTENT | Yes | No | Yes |
versions:write | Snapshot a spec and restore a version | UPDATE_CONTENT | Yes | No | Yes |
prd:read | Read PRD sections, search and criteria | READ_CONTENT | Yes | No | Yes |
prd:write | Answer a PRD's open questions | UPDATE_CONTENT | Yes | No | Yes |
operations:write | Start, message, steer, park, resume, cancel agent operations; dispatch a spec | CREATE_CONTENT, UPDATE_CONTENT | No (the Slack command is owner and admin only) | No | No, Slack only |
integrations:write | Administration: hosted-compute settings, writer connections, action policies, project audience, identity linking; workspace-wide watch rules (together with subscriptions:write) | MANAGE_INTEGRATIONS | No | No | No, Slack only |
actions:approve | List, read, approve and discard drafted outbound actions | READ_CONTENT | Yes | No | No, not mintable |
identities:write | Link your own external account | READ_CONTENT, UPDATE_CONTENT | Yes | No | No, not mintable |
One built-in first-party client, nima-mcp, may also be granted documents:write, prd:read, prd:write, versions:read and versions:write on the consent screen; a token from it can call those REST routes. No OAuth client can ever be granted a headless scope.
Four scopes are headless scopes: operations:write, integrations:write, actions:approve and identities:write. The MCP transport accepts them, but nima never adds them to OAuth consent or to an existing key.
How to get a key with a headless scope
An owner or admin runs /nima mcp-key operations or /nima mcp-key integrations in Slack (also accepted as an @nima mention or DM), then confirms on the card or tells @nima confirm <code> in the channel within five minutes. Revocation, the key cap and other details are in Authentication.
| Slack command | Key carries | Lifetime |
|---|---|---|
/nima mcp-key operations | operations:write, documents:read, knowledge:read | 30 days |
/nima mcp-key integrations | integrations:write only | 30 days |
actions:approve and identities:write are minted by nothing: not the API key route, not the consent screen, not Slack. An API key or OAuth client cannot reach the tools behind them today. Approvals happen on the card in Slack. A signed-in browser session skips scope checks, so it can call those routes directly.
Which scopes the API key form accepts
POST /v1/api-keys validates the scopes array against 13 values: every scope in the table except the four headless ones. Sending operations:write there is a 400 with "error": "Bad Request", not a silent drop.
Routes and tools unlocked by each scope
Paths are relative to https://api.getnima.io. Signals, beacons, knowledge, code, operations and the newer resources sit under /v1. Specs (/documents) and PRDs (/prd) sit at the root with no version segment. There is no other version to ask for. The one exception is spec dispatch, POST /v1/documents/{id}/dispatch, which belongs to agent operations and carries /v1.
Each MCP tool is documented on exactly one MCP tool page; the last column only names it.
Scopes for reading and searching
| Scope | Operation | MCP tool |
|---|---|---|
knowledge:read | GET /v1/knowledge-graph/ask | ask_knowledge_graph |
knowledge:read | GET /v1/code-intel/search | search_code |
knowledge:read | GET /v1/code-intel/blast-radius | blast_radius |
knowledge:read | GET /v1/code-intel/data-model | get_data_model |
signals:read | GET /v1/signals | search_signals |
signals:read | GET /v1/signals/facets | none |
signals:read | GET /v1/signals/{id} | get_signal |
signals:read | GET /v1/beacons | list_beacons |
signals:read | GET /v1/beacons/facets | none |
signals:read | GET /v1/beacons/{id} | get_beacon |
signals:read | GET /v1/events | get_history |
sources:read | GET /v1/signals/{id}/source | get_signal_source |
documents:read | GET /documents | list_specs |
documents:read | GET /documents/{id} | none |
documents:read | GET /documents/{id}/markdown | get_document_markdown |
documents:read | GET /documents/{id}/delivery | get_spec_delivery |
versions:read | GET /documents/{id}/versions | none |
versions:read | GET /documents/{id}/versions/{versionId} | none |
prd:read | GET /prd/{documentId}/sections/{heading} | none |
prd:read | GET /prd/{documentId}/search | none |
prd:read | GET /prd/{documentId}/criteria | none |
Scopes for writing signals and specs
| Scope | Operation | MCP tool |
|---|---|---|
signals:write | POST /v1/signals | create_signal |
documents:write | POST /documents | none |
documents:write | PUT /documents/{id}/markdown | none |
documents:write | PATCH /documents/{id}/spec-metadata | none |
documents:write | PATCH /documents/{id}/epic-metadata | none |
documents:write | POST /documents/{id}/promote | none |
documents:write | DELETE /documents/{id} | none |
versions:write | POST /documents/{id}/snapshot | none |
versions:write | POST /documents/{id}/restore/{versionId} | none |
prd:write | POST /prd/{documentId}/questions | none |
Scopes for watches, inbox and scheduled messages
| Scope | Operation | MCP tool |
|---|---|---|
subscriptions:read | GET /v1/triggers/event-types | list_event_types |
subscriptions:read | GET /v1/watches | list_watches |
subscriptions:read | GET /v1/scheduled-messages/context | get_scheduling_context |
subscriptions:read | GET /v1/scheduled-messages | list_scheduled_messages |
subscriptions:write | POST /v1/watches | create_subscription |
subscriptions:write | PATCH /v1/watches/{id} | update_subscription |
subscriptions:write | DELETE /v1/watches/{id} | unwatch |
subscriptions:write | POST /v1/scheduled-messages | schedule_message |
subscriptions:write | PATCH /v1/scheduled-messages/{id} | update_scheduled_message |
subscriptions:write | DELETE /v1/scheduled-messages/{id} | cancel_scheduled_message |
inbox:read | GET /v1/inbox | get_inbox |
A workspace rule (scope: "org" on POST /v1/watches) needs integrations:write on the same key or token as well as subscriptions:write, plus the integrations-management permission. No key today carries both, so workspace rules can be created only from a signed-in owner or admin session. The workspace must also have watches and outbound dispatch turned on, or the call answers 409; see Watches.
Scopes for agent operations
Reads of operations use documents:read. Controls use operations:write.
| Scope | Operation | MCP tool |
|---|---|---|
documents:read | GET /v1/operations | list_operations |
documents:read | GET /v1/operations/{id} | get_operation |
documents:read | GET /v1/operations/{id}/result | get_operation_result |
documents:read | GET /v1/operations/{id}/brief | get_operation_brief |
documents:read | GET /v1/operations/{id}/events | none |
documents:read | POST /v1/operations/{id}/inspect | inspect_operation |
operations:write | POST /v1/operations | start_operation |
operations:write | POST /v1/operations/{id}/claim | claim_operation_handoff |
operations:write | POST /v1/operations/{id}/result | report_operation_result |
operations:write | POST /v1/operations/{id}/message | message_operation |
operations:write | POST /v1/operations/{id}/steer | none |
operations:write | POST /v1/operations/{id}/park | none |
operations:write | POST /v1/operations/{id}/resume | none |
operations:write | POST /v1/operations/{id}/cancel | cancel_operation |
operations:write | POST /v1/operations/{id}/raise-limit | raise_operation_limit |
operations:write | POST /v1/documents/{id}/dispatch | none |
Three more tools exist only for a hosted VM run and are never shown to a user token: get_run_brief (GET /v1/operations/{id}/run-brief, documents:read), report_or_ask_pm (POST /v1/agent-runs/{id}/report, signals:write) and wait_for_checks (POST /v1/agent-runs/{id}/wait-for-checks, signals:write).
Scopes for administration and outbound dispatch
| Scope | Operation | MCP tool |
|---|---|---|
integrations:write | GET /v1/agent-compute | get_agent_compute_settings |
integrations:write | PATCH /v1/agent-compute | update_agent_compute_settings |
integrations:write | GET /v1/github-writer/app/install | get_github_writer_install_url |
integrations:write | GET /v1/github-writer/app/status | get_github_publishing_status |
integrations:write | PATCH /v1/github-writer/publishing | set_github_publishing |
integrations:write | DELETE /v1/github-writer/app/installation | disconnect_github_writer |
integrations:write | POST /v1/integrations/jira/writer/mcp | connect_jira_writer |
integrations:write | POST /v1/integrations/jira/writer, GET /v1/integrations/jira/writer | none |
integrations:write | POST /v1/integrations/linear/writer | connect_linear_writer |
integrations:write | GET /v1/integrations/linear/writer | none |
integrations:write | PATCH /v1/dispatch/policies | set_action_policy |
integrations:write | GET /v1/dispatch/policies | none |
integrations:write | POST /v1/dispatch/project-audience | assert_project_audience |
integrations:write | GET /v1/identities/unlinked | list_unlinked_identities |
integrations:write or identities:write | POST /v1/identities/link | link_external_identity |
identities:write | POST /v1/identities/link-me | link_my_identity |
actions:approve | GET /v1/dispatch/actions (alias /v1/actions) | list_pending_actions |
actions:approve | GET /v1/dispatch/actions/{id} (alias /v1/actions/{id}) | get_action |
actions:approve | POST /v1/actions/{id}/approve | approve_action |
actions:approve | POST /v1/actions/{id}/discard | discard_action |
Operations that accept more than one scope
GET /oauth/userinfo accepts any of the 17 scopes, so it works with every token. It returns sub, email, isOnboarded, organizationId and orgRole, and no scope list. POST /v1/identities/link accepts integrations:write or identities:write. The MCP transport at POST /mcp accepts the 12 scopes described in Scopes on the MCP server. GET /mcp and DELETE /mcp accept the 8 consent scopes (they answer 405, since the server keeps no stream or session).
Some routes declare a plan feature as well, for example specs under documents, PRDs under prd_generation and code search under code_intelligence. A workspace whose plan lacks the feature can get 403 INSUFFICIENT_PERMISSIONS with details.feature naming it. Handle that response; do not assume a scope alone is enough.
How nima checks scopes
nima checks a token's scopes against the operation on every request, in four steps.
Closed by default. An operation that declares no scope rejects every key and every OAuth token with 403 and the message This endpoint does not accept OAuth access tokens, before the handler runs. A valid key is not enough.
One matching scope is enough. An operation passes a token that holds at least one of the scopes it lists, not all of them. Most operations list exactly one. POST /v1/identities/link (2), POST /mcp (12), GET/DELETE /mcp (8) and GET /oauth/userinfo (all 17) list more.
Scopes only narrow. On every request nima reads the token owner's current role in the workspace the token belongs to and intersects those permissions with the permissions the token's scopes imply. A token can never do more than its owner can do right now. Demote the owner to member and their sources:read key stops fetching sources on the next call. Remove them from the workspace and every token they minted resolves to no role: permission-checked routes answer 403, and GET /oauth/userinfo returns orgRole: null. Access stops without a revocation step, but the keys stay listed and keep authenticating until they are revoked or expire. Tokens never carry super-user rights.
Two different 403 messages. A token missing the scope is refused with OAuth token scopes do not cover this endpoint and details.requiredScopes. A token that holds the scope while its owner has lost the permission is refused with Missing required permissions (AND): … (or (OR) on a route that accepts any one permission), naming the permissions in details.requiredPermissions and the mode in details.mode.
Granting scopes: the silent drop
A scope is grantable only if the person granting it holds all the permissions in the granter column. Scopes they cannot back are dropped from the request and the key is created anyway.
Asking for a scope you cannot grant is not an error. A member who ticks sources:read in Settings → API keys gets a working key back with sources:read quietly missing, and the first 403 arrives later, in whatever script was going to use it. Read the scopes array on the created key, not the boxes you ticked.
If every requested scope is ungrantable, key creation fails instead with 400 and "None of the requested scopes can be granted by this user", carrying the scopes you asked for in details.requestedScopes.
OAuth consent works the same way and adds one filter in front. A client can only be granted the scopes it registered with. Scopes outside that set are dropped from the request, and only when none are left is the request refused with 400 and "None of the requested scopes are allowed for this client". If the approver's role can back none of what remains, approval fails with 400 and "User lacks permissions to grant the requested scopes". The authorization code is issued for whatever passes both filters.
No route reports a key's or token's scopes back when you present it. What a key carries is in the response you got when you created it and in the key list in Settings → API keys. What an OAuth token carries is in the scope field of the token response.
Scopes only owners and admins can grant
Four scopes are out of a member's reach, for different reasons.
documents:writeneeds create, update and delete on content. A member holds create and update but not delete.sources:readneedsREAD_SOURCE_DOCUMENTS. Fetching the document behind a signal replays the request against the source system with the workspace's own integration credential, which reaches further than any one member does.integrations:writeneedsMANAGE_INTEGRATIONS, and it is also minted only through the Slack command above.operations:writeis backed by permissions a member holds, but the only way to mint it is/nima mcp-key operationsin Slack, which accepts owners and admins only.
Scopes on the MCP server
The remote MCP server at https://mcp.getnima.io/mcp offers 8 scopes on its consent screen, and its transport accepts 12.
Consent screen scopes (8). In display order: knowledge:read, signals:read, sources:read, documents:read, signals:write, subscriptions:read, subscriptions:write, inbox:read. Every client that registers dynamically is registered with exactly this set, so a registered MCP client can be granted only these; the headless scopes are never grantable through consent. The screen groups them as read permissions ("Search what nima knows", "Read signals and beacons", "Read the sources behind signals", "Read specs and documents", "See what you are watching", "Read your inbox") and write permissions ("Manage your subscriptions", "File signals for you").
Transport scopes (12). POST /mcp accepts the 8 consent scopes plus the 4 headless scopes. A token holding any one of the 12 opens the endpoint, and each tool then re-checks its own scope.
Visibility rule for tools/list. A signed-in browser session sees every tool. A key or OAuth token sees every tool behind a consent scope, even if the token does not hold that scope, so a stale token learns the tool exists and the failed call triggers re-consent. Tools behind a headless scope appear only when the token holds that scope. VM-only tools never appear. A default OAuth connection therefore lists the 30 consent-scope tools, including the five that read agent operations, and none of the tools that start or control operations, administer the workspace, approve actions or link identities.
The WWW-Authenticate challenge. A 401 carries Bearer error="invalid_token" with the resource metadata URL and the 8 consent scopes. A 403 from a tool behind a consent scope becomes an outer 403 with error="insufficient_scope" and a scope list made of the tool's scope followed by the scopes the token already holds, so re-consent keeps what the client had. That is the client's cue to re-consent:
WWW-Authenticate: Bearer error="insufficient_scope", scope="sources:read knowledge:read signals:read"A 403 from a tool behind a headless scope is not an outer error. It comes back as a tool result with isError set and this text: "This tool requires an explicit headless grant. Ask a workspace admin to use /nima mcp-key operations or /nima mcp-key integrations in Slack, then connect using that key. Existing OAuth consent does not grant administration." An OAuth client can never administer integrations or compute.
Connect steps live on Connect to nima's MCP server; each tool is documented on its MCP tool page, starting from knowledge tools.
Scope errors
| Status | error | message | When |
|---|---|---|---|
401 | TOKEN_INVALID | Invalid or expired access token | The key or token is unknown, revoked or expired. |
403 | INSUFFICIENT_PERMISSIONS | This endpoint does not accept OAuth access tokens | The operation declares no scope. No key reaches it. |
403 | INSUFFICIENT_PERMISSIONS | OAuth token scopes do not cover this endpoint | The token holds none of the scopes the operation lists. details.requiredScopes says which. |
403 | INSUFFICIENT_PERMISSIONS | Missing required permissions (AND): … (or (OR) on a route that accepts any one permission) | The scope is there, the owner's current role is not. details.requiredPermissions names the permissions and details.mode gives the mode. |
403 | INSUFFICIENT_PERMISSIONS | API key not allowed from this address | The key is restricted to CIDR ranges and the call came from outside them. |
403 | INSUFFICIENT_PERMISSIONS | Feature "<feature>" is not available on your current plan | The route is declared under a plan feature the workspace plan lacks. details.feature and details.plan name them. |
400 | VALIDATION_ERROR | None of the requested scopes can be granted by this user | Every scope asked for at key creation was ungrantable. |
A scope string that is not one of the accepted values is rejected by generic input validation, which answers 400 with "error": "Bad Request" and "message": "Bad Request Exception", without saying which field was wrong. Compare your scope strings against the table above, not against that message.
To see a scope failure, call a route with a key that lacks the scope:
curl -i -H "Authorization: Bearer $NIMA_API_KEY" \
https://api.getnima.io/v1/signals/3f7a9c21-5b48-4d0e-9a11-6c2f8be41d73/sourceA key minted without sources:read gets:
{
"statusCode": 403,
"error": "INSUFFICIENT_PERMISSIONS",
"message": "OAuth token scopes do not cover this endpoint",
"details": { "requiredScopes": ["sources:read"] },
"timestamp": "2026-09-22T10:31:07.512Z",
"path": "/v1/signals/3f7a9c21-5b48-4d0e-9a11-6c2f8be41d73/source"
}To check that a token is alive without needing a particular scope, call GET /oauth/userinfo. A null orgRole means the owner is no longer a member.
Authentication
The credentials the nima API accepts (personal API keys, Slack-issued headless keys, OAuth access tokens), how each is created and expires, and the rules no credential breaks.
Errors
The single JSON error envelope of the nima API, every error code, the 4xx details you can branch on, the 5xx rules, and the edge cases where the API tells you less than it should.