docs
API

OAuth and identity

The userinfo endpoint and the OAuth 2.1 flow (discovery, registration, consent, token, refresh, revoke) that MCP clients and your own apps use.

Two different things live under /oauth. GET /oauth/userinfo is an ordinary API operation: it answers with the identity behind whatever credential you send, and every credential can call it. Everything else on this page is the authorization flow that issues an OAuth access token in the first place: discovery, client registration, consent, code exchange, refresh, revocation.

That flow is how nima's MCP consent works. Connecting Claude to https://mcp.getnima.io/mcp runs exactly these steps, which is why there is no client id or secret to paste. See Connect to nima's MCP server.

You can run the same flow from a client you write. It is the alternative to a personal API key for an interactive app: each person approves their own access in a browser, your app stores a token instead of their key, and the access ends when your app revokes the token (for example on sign-out), when the person leaves the workspace, or 30 days after the last refresh. nima has no screen where a person can list or revoke the apps they approved.

On for every workspace.

A client registered through this flow can only ever hold the 8 consent scopes, in this order: knowledge:read, signals:read, sources:read, documents:read, signals:write, subscriptions:read, subscriptions:write and inbox:read. Registration sets that list and ignores the scope you ask for. The other 9 grantable scopes (documents:write, versions:read, versions:write, prd:read, prd:write, operations:write, integrations:write, actions:approve, identities:write) can never be held by an OAuth client. The five other consumer scopes go on a personal API key. operations:write and integrations:write come only from a headless key minted in Slack. No one can mint actions:approve or identities:write today, so the tools behind them are out of reach for keys and OAuth clients, and approvals happen on the Slack card. See Authentication.

The flow runs in five steps:

  1. Read the discovery documents to learn the endpoints.
  2. Register a client and keep the client_id.
  3. Send the person to /oauth/authorize. They sign in and approve.
  4. Exchange the code they come back with for an access and refresh token.
  5. Refresh once an hour, for as long as the connection lives.

OAuth and identity operations

MethodPathScopeMCP tool
GET/oauth/userinfoAny one scopeNone
GET/.well-known/oauth-authorization-serverNoneNone
GET/.well-known/oauth-protected-resourceNoneNone
GET/.well-known/oauth-protected-resource/mcpNoneNone
POST/mcpA bearer token: an API key or an OAuth access token. JSON-RPC 2.0 over Streamable HTTP. Each tool call is checked against the scope that tool needs.All of them
GET, DELETE/mcpSame bearer token; answers 405 with Allow: POSTNone
POST/oauth/registerNoneNone
GET/oauth/authorizeNone on the call itselfNone
POST/oauth/token with grant_type=authorization_codeNoneNone
POST/oauth/token with grant_type=refresh_tokenNoneNone
POST/oauth/revokeNoneNone

/mcp is the resource an OAuth access token is issued for. The MCP server endpoint section documents its methods, protocol versions and message rules.

None of the paths on this page carry a /v1 segment. Most of the rest of the API does. Use each path exactly as printed.

/oauth/register, /oauth/token and /oauth/revoke allow 15 requests per 30 seconds (the hard tier, the strictest of the API's shared tiers). Everything else on this page allows 100 per minute. See Rate limits.

Read the identity behind a credential

GET/oauth/userinfo

Returns the person a credential acts as and the workspace it acts in. Any scope passes, so every API key and every access token can call it. That makes it the cheapest way to prove a credential works before you write anything against it.

It takes no parameters. The fields it answers with are in The userinfo identity object.

Errors

StatuserrorWhen
401TOKEN_INVALIDNo Authorization header, or a credential that is unknown, revoked or expired
403INSUFFICIENT_PERMISSIONSAn API key used from an address outside its allowlist
429RATE_LIMITEDOver the limit

Read the authorization server metadata

GET/.well-known/oauth-authorization-server

RFC 8414 authorization server metadata: the endpoints of the flow, the grants and response types nima supports, and the scopes a client can be registered for. It takes no credential, because a client reads it before it holds one. Send it without an Authorization header. A stale bearer answers 401, and a live key that holds none of the 8 consent scopes answers 403.

Both discovery documents are served on api.getnima.io and on mcp.getnima.io, and both name https://mcp.getnima.io whichever host you asked. Use the endpoints as the document prints them rather than the host you happened to fetch from.

There is no /.well-known/openid-configuration and no OpenID Connect layer. GET /oauth/userinfo is nima's own endpoint that happens to share the name: it takes any nima scope rather than openid, and its fields are nima's.

Read the protected resource metadata

GET/.well-known/oauth-protected-resource

RFC 9728 protected resource metadata. It names https://mcp.getnima.io/mcp as the resource, this same host as its authorization server, and header as the only way to present a bearer token. It takes no credential. Send it without an Authorization header. A stale bearer answers 401, and a live key that holds none of the 8 consent scopes answers 403.

The same document is also served at /.well-known/oauth-protected-resource/mcp, the path an MCP client derives from the resource URL.

Register an OAuth client

POST/oauth/register

RFC 7591 dynamic client registration. It takes no credential, because a client reaches it before anyone has approved anything. That is also why it sits at 15 requests per 30 seconds.

redirect_urisstring[]required

1 to 10 entries. Each must be https, or http on localhost, 127.0.0.1 or [::1]. A custom scheme such as myapp://callback is rejected.

client_namestring

Up to 255 characters. Defaults to MCP client.

grant_types, response_types, token_endpoint_auth_method and scope are accepted and ignored, as long as they have the RFC 7591 types (arrays of strings, or a string). The response says the same thing whatever you send. Any other member of the body is dropped.

The call answers 201, and the client_id is dcr_ followed by 32 hex characters. No client secret is issued and none is ever expected: token_endpoint_auth_method is none, and PKCE is what binds the exchange to the client that started it.

Registration is permanent. There is no endpoint to update or delete a client afterwards, and no way to add a redirect address to one later. Register again if the address changes.

Errors

StatuserrorWhen
400VALIDATION_ERRORA redirect_uris entry is not https and not http on loopback
400Bad RequestThe body failed field validation before the handler ran, such as a missing redirect_uris or eleven of them. See OAuth endpoint error format
401TOKEN_INVALIDYou sent an expired, revoked or unknown bearer in an Authorization header
403INSUFFICIENT_PERMISSIONSYou sent a live API key or access token in an Authorization header
429RATE_LIMITEDOver the limit
GET/oauth/authorize

The step the person sees. The response is a 302 to nima's consent screen at https://app.getnima.io/oauth/authorize, carrying the same parameters and nothing else. Anything extra you sent is dropped there. If the person is not signed in, they sign in first and are returned to the screen.

client_idstringrequired

From registration. An unknown id answers 404.

redirect_uristringrequired

One the client registered. Checked right after the client id, before any other parameter.

response_typeenumrequired

code. Anything else answers 400.

code_challengestringrequired

Base64url of the SHA-256 of your verifier.

code_challenge_methodenumrequired

S256. plain is rejected.

statestring

In practice required. Carried through to your callback. An empty one fails at the approval step, not here.

scopestring

Space separated. Omit it to ask for everything the client is registered for. Scopes outside the client's list are dropped silently, and if none survive the call answers 400.

A loopback redirect_uri is matched on its path only, so the ephemeral port a native app listens on does not have to be registered. Every other address must match exactly, character for character.

Where the browser lands after the person decides:

OutcomeWhere the browser lands
AllowYour redirect_uri with code and state
DenyYour redirect_uri with error=access_denied and state, and only if that address is registered. If it is not, nothing is sent and the screen says so.

The code is valid for 10 minutes and can be exchanged once. A second exchange answers 401 with TOKEN_REPLAY. The first exchange attempt consumes the code even if it fails, so after any 401 start again at /oauth/authorize. Do not retry with the same code.

Errors

StatuserrorWhen
400VALIDATION_ERRORresponse_type is not code, PKCE is missing or not S256, the redirect_uri is not registered for the client, or no requested scope survives
401TOKEN_INVALIDYou sent an expired, revoked or unknown bearer in an Authorization header
403INSUFFICIENT_PERMISSIONSYou sent a live API key or access token in an Authorization header
404RESOURCE_NOT_FOUNDNo client with that client_id
429RATE_LIMITEDOver the limit

This endpoint never reports its own errors by redirecting to your redirect_uri. A bad request answers 400 or 404 with a JSON body, so implement the failure path on the response to this request as well as on your callback. That is correct for an unregistered address, which must never be redirected to, and it applies to every other mistake too.

Send a non-empty state. It is forwarded to the consent screen and required there, so an empty one does not fail at /oauth/authorize. It fails at the moment the person clicks Allow, which is the worst place to find out.

The token can be narrower than the screen

The consent screen lists what the client asked for. The token carries that minus anything the approver's own role cannot back, with no error and no note on the screen. If nothing survives, for example a request for sources:read alone approved by a member, Allow fails on the screen and no code is issued. sources:read needs owner or admin, so when a member approves, the token comes back without it and get_signal_source answers 403 for the life of that token.

Read scope in the token response. It is the answer, not the request.

Exchange the code for tokens

POST/oauth/token

The body may be JSON or application/x-www-form-urlencoded. The response is the token object.

grant_typeenumrequired

authorization_code.

client_idstringrequired

The registered id.

codestringrequired

From your callback.

redirect_uristringrequired

Identical to the one on the authorize request.

code_verifierstringrequired

The verifier whose SHA-256 you sent as code_challenge.

Errors

StatuserrorWhen
400VALIDATION_ERRORThe grant is missing code, redirect_uri or code_verifier
400Bad Requestgrant_type is missing or not one of the two, or client_id is missing. See OAuth endpoint error format
401TOKEN_INVALIDThe code is unknown, client_id or redirect_uri does not match the code, or PKCE verification failed
401TOKEN_EXPIREDThe code is over 10 minutes old
401TOKEN_REPLAYThe code was already exchanged
401TOKEN_INVALIDYou sent an expired, revoked or unknown bearer in an Authorization header
403INSUFFICIENT_PERMISSIONSYou sent a live API key or access token in an Authorization header
429RATE_LIMITEDOver the limit

Refresh an access token

POST/oauth/token

The same endpoint with the other grant. The response is the same shape, with a new access token and a new refresh token.

grant_typeenumrequired

refresh_token.

client_idstringrequired

The registered id.

refresh_tokenstringrequired

The refresh token from the last exchange.

RuleBehaviour
RotationEvery refresh issues a new pair and revokes the one you used. Write the new refresh token down before you act on the response.
ReusePresenting a refresh token that was already rotated revokes the entire chain it belongs to, including the pair currently in use, and answers 401 with TOKEN_REPLAY. The person has to approve again. Reuse means a copy leaked, and that is the intended response to it.
LifetimeA refresh token lasts 30 days, and each refresh resets the clock. A client that refreshes at least monthly keeps working. One that goes quiet for 30 days has to start the flow over.
MembershipEvery refresh re-checks that the approver is still a member of the workspace and that it is the workspace they currently have selected in the admin panel. If either check fails, the token is revoked on the spot, the call answers 401 TOKEN_INVALID, and switching back does not restore it. The person has to approve again.

Errors

StatuserrorWhen
400VALIDATION_ERRORrefresh_token is missing
400Bad Requestgrant_type is missing or not one of the two, or client_id is missing. See OAuth endpoint error format
401TOKEN_INVALIDThe refresh token is unknown, the approver has left the workspace, or the approver has switched the admin panel to another workspace
401TOKEN_EXPIREDThe refresh token is over 30 days old
401TOKEN_REPLAYA refresh token that was already rotated or revoked was presented
401TOKEN_INVALIDYou sent an expired, revoked or unknown bearer in an Authorization header
403INSUFFICIENT_PERMISSIONSYou sent a live API key or access token in an Authorization header
429RATE_LIMITEDOver the limit

Revoke an OAuth token

POST/oauth/revoke

RFC 7009. Send either token of a pair as token.

tokenstringrequired

An access token or a refresh token.

The answer is 200 with an empty body, including for a token that was never issued, so it tells you nothing about whether the token existed. Both tokens of a pair are one record, so revoking either one ends the connection and the person has to approve again.

Errors

StatuserrorWhen
400Bad Requesttoken is missing or empty. See OAuth endpoint error format
401TOKEN_INVALIDYou sent an expired, revoked or unknown bearer in an Authorization header
403INSUFFICIENT_PERMISSIONSYou sent a live API key or access token in an Authorization header
429RATE_LIMITEDOver the limit

The userinfo identity object

What GET /oauth/userinfo answers with.

substring

Id of the person the credential acts as.

emailstring

That person's email address.

isOnboardedboolean

Whether they finished onboarding in the nima app.

organizationIdstring or null

The workspace the credential acts in.

orgRoleenum or null

owner, admin or member. Their role in that workspace right now. null means they are no longer a member of it, or they have switched the admin panel to a different workspace since. Either way the credential has no permissions, and every operation that needs one answers 403. See Authentication.

The token object

What both grants on POST /oauth/token answer with.

access_tokenstring

64 hex characters. Send it as a bearer token.

refresh_tokenstring

64 hex characters. Store it. It is replaced every time you use it.

token_typestring

Always Bearer.

expires_ininteger

Always 3600. It is a constant, not a measurement: the access token is good for one hour from issue.

scopestring

Space separated, the scopes actually granted. Can be narrower than the ones you asked for.

Scopes an OAuth client can hold

An OAuth client can hold exactly the 8 consent scopes. The table lists them in registration order (the order of scopes_supported), with the label the person sees and what each unlocks. The consent screen shows the read scopes first, then the two write scopes.

ScopeConsent screen labelWhat it unlocksWho can grant it
knowledge:readSearch what nima knowsask_knowledge_graph, search_code, blast_radius, get_data_modelAny member
signals:readRead signals and beaconssearch_signals, get_signal, list_beacons, get_beacon, get_historyAny member
sources:readRead the sources behind signalsget_signal_sourceOwners and admins
documents:readRead specs and documentslist_specs, get_document_markdown, get_spec_delivery, and the read tools for operationsAny member
signals:writeFile signals for youcreate_signalAny member
subscriptions:readSee what you are watchinglist_event_types, list_watches, get_scheduling_context, list_scheduled_messagesAny member
subscriptions:writeManage your subscriptionscreate_subscription, update_subscription, unwatch, schedule_message, update_scheduled_message, cancel_scheduled_messageAny member
inbox:readRead your inboxget_inboxAny member

subscriptions:write is broader than it sounds: saved watches and scheduled messages run AI tasks with the owner's workspace context.

Those names are the MCP tools; each one is an HTTP operation as well, and every operation in this reference names the scope it needs. See Knowledge tools, Pipeline tools and Watch tools for the tool reference.

GET /oauth/userinfo accepts a token holding any one of the 17 grantable scopes, which is why every API key and every access token can call it.

A token's scopes are checked on every call, and its permissions are narrowed again by the approver's role at that moment, so a token issued to an admin who is later demoted quietly loses reach without changing. Authentication has the full rule and the nine scopes an OAuth client cannot hold.

Use an OAuth access token on API calls

The same header as an API key, and the same treatment from there on.

curl -s "https://api.getnima.io/v1/signals?limit=20" \
  -H "Authorization: Bearer $NIMA_ACCESS_TOKEN"

Do not send an Authorization header to /oauth/token, /oauth/register, /oauth/authorize or /oauth/revoke. The header is checked before the handler runs. A live API key or access token is refused with 403 INSUFFICIENT_PERMISSIONS, and an expired, revoked or unknown one with 401 TOKEN_INVALID. So a client that attaches its expired access token while trying to refresh gets 401 instead of a new token.

OAuth endpoint error format

These endpoints answer with nima's error envelope, not the RFC 6749 {"error": "invalid_grant"} shape. An off-the-shelf OAuth client looking for that field will not find it. Read statusCode and error.

{
  "statusCode": 401,
  "error": "TOKEN_REPLAY",
  "message": "Authorization code has already been used",
  "timestamp": "2026-09-22T09:14:02.881Z",
  "path": "/oauth/token"
}

A 429 carries a Retry-After header with the whole seconds to wait: at most 30 for /oauth/register, /oauth/token and /oauth/revoke, and at most 60 for the other endpoints on this page. See Rate limits.

One 400 does not follow that pattern. A body that fails field validation before the handler runs, such as a missing grant_type or eleven redirect_uris, is answered by the framework rather than by nima:

{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Bad Request Exception",
  "timestamp": "2026-09-22T09:14:02.881Z",
  "path": "/oauth/register"
}

error is an English phrase there rather than a code, and the per-field messages are dropped on the way out, so it tells you the body was wrong and nothing else. Check the request against the parameters above. A 400 raised by the handler itself carries VALIDATION_ERROR and a message that names the problem.