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:
- Read the discovery documents to learn the endpoints.
- Register a client and keep the
client_id. - Send the person to
/oauth/authorize. They sign in and approve. - Exchange the code they come back with for an access and refresh token.
- Refresh once an hour, for as long as the connection lives.
OAuth and identity operations
| Method | Path | Scope | MCP tool |
|---|---|---|---|
GET | /oauth/userinfo | Any one scope | None |
GET | /.well-known/oauth-authorization-server | None | None |
GET | /.well-known/oauth-protected-resource | None | None |
GET | /.well-known/oauth-protected-resource/mcp | None | None |
POST | /mcp | A 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 | /mcp | Same bearer token; answers 405 with Allow: POST | None |
POST | /oauth/register | None | None |
GET | /oauth/authorize | None on the call itself | None |
POST | /oauth/token with grant_type=authorization_code | None | None |
POST | /oauth/token with grant_type=refresh_token | None | None |
POST | /oauth/revoke | None | None |
/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
/oauth/userinfoReturns 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
| Status | error | When |
|---|---|---|
401 | TOKEN_INVALID | No Authorization header, or a credential that is unknown, revoked or expired |
403 | INSUFFICIENT_PERMISSIONS | An API key used from an address outside its allowlist |
429 | RATE_LIMITED | Over the limit |
Read the authorization server metadata
/.well-known/oauth-authorization-serverRFC 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
/.well-known/oauth-protected-resourceRFC 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
/oauth/registerRFC 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[]required1 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_namestringUp 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
| Status | error | When |
|---|---|---|
400 | VALIDATION_ERROR | A redirect_uris entry is not https and not http on loopback |
400 | Bad Request | The body failed field validation before the handler ran, such as a missing redirect_uris or eleven of them. See OAuth endpoint error format |
401 | TOKEN_INVALID | You sent an expired, revoked or unknown bearer in an Authorization header |
403 | INSUFFICIENT_PERMISSIONS | You sent a live API key or access token in an Authorization header |
429 | RATE_LIMITED | Over the limit |
Send the person to the OAuth consent screen
/oauth/authorizeThe 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_idstringrequiredFrom registration. An unknown id answers 404.
redirect_uristringrequiredOne the client registered. Checked right after the client id, before any other parameter.
response_typeenumrequiredcode. Anything else answers 400.
code_challengestringrequiredBase64url of the SHA-256 of your verifier.
code_challenge_methodenumrequiredS256. plain is rejected.
statestringIn practice required. Carried through to your callback. An empty one fails at the approval step, not here.
scopestringSpace 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:
| Outcome | Where the browser lands |
|---|---|
| Allow | Your redirect_uri with code and state |
| Deny | Your 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
| Status | error | When |
|---|---|---|
400 | VALIDATION_ERROR | response_type is not code, PKCE is missing or not S256, the redirect_uri is not registered for the client, or no requested scope survives |
401 | TOKEN_INVALID | You sent an expired, revoked or unknown bearer in an Authorization header |
403 | INSUFFICIENT_PERMISSIONS | You sent a live API key or access token in an Authorization header |
404 | RESOURCE_NOT_FOUND | No client with that client_id |
429 | RATE_LIMITED | Over 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
/oauth/tokenThe body may be JSON or application/x-www-form-urlencoded. The response is
the token object.
grant_typeenumrequiredauthorization_code.
client_idstringrequiredThe registered id.
codestringrequiredFrom your callback.
redirect_uristringrequiredIdentical to the one on the authorize request.
code_verifierstringrequiredThe verifier whose SHA-256 you sent as code_challenge.
Errors
| Status | error | When |
|---|---|---|
400 | VALIDATION_ERROR | The grant is missing code, redirect_uri or code_verifier |
400 | Bad Request | grant_type is missing or not one of the two, or client_id is missing. See OAuth endpoint error format |
401 | TOKEN_INVALID | The code is unknown, client_id or redirect_uri does not match the code, or PKCE verification failed |
401 | TOKEN_EXPIRED | The code is over 10 minutes old |
401 | TOKEN_REPLAY | The code was already exchanged |
401 | TOKEN_INVALID | You sent an expired, revoked or unknown bearer in an Authorization header |
403 | INSUFFICIENT_PERMISSIONS | You sent a live API key or access token in an Authorization header |
429 | RATE_LIMITED | Over the limit |
Refresh an access token
/oauth/tokenThe same endpoint with the other grant. The response is the same shape, with a new access token and a new refresh token.
grant_typeenumrequiredrefresh_token.
client_idstringrequiredThe registered id.
refresh_tokenstringrequiredThe refresh token from the last exchange.
| Rule | Behaviour |
|---|---|
| Rotation | Every refresh issues a new pair and revokes the one you used. Write the new refresh token down before you act on the response. |
| Reuse | Presenting 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. |
| Lifetime | A 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. |
| Membership | Every 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
| Status | error | When |
|---|---|---|
400 | VALIDATION_ERROR | refresh_token is missing |
400 | Bad Request | grant_type is missing or not one of the two, or client_id is missing. See OAuth endpoint error format |
401 | TOKEN_INVALID | The refresh token is unknown, the approver has left the workspace, or the approver has switched the admin panel to another workspace |
401 | TOKEN_EXPIRED | The refresh token is over 30 days old |
401 | TOKEN_REPLAY | A refresh token that was already rotated or revoked was presented |
401 | TOKEN_INVALID | You sent an expired, revoked or unknown bearer in an Authorization header |
403 | INSUFFICIENT_PERMISSIONS | You sent a live API key or access token in an Authorization header |
429 | RATE_LIMITED | Over the limit |
Revoke an OAuth token
/oauth/revokeRFC 7009. Send either token of a pair as token.
tokenstringrequiredAn 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
| Status | error | When |
|---|---|---|
400 | Bad Request | token is missing or empty. See OAuth endpoint error format |
401 | TOKEN_INVALID | You sent an expired, revoked or unknown bearer in an Authorization header |
403 | INSUFFICIENT_PERMISSIONS | You sent a live API key or access token in an Authorization header |
429 | RATE_LIMITED | Over the limit |
The userinfo identity object
What GET /oauth/userinfo answers with.
substringId of the person the credential acts as.
emailstringThat person's email address.
isOnboardedbooleanWhether they finished onboarding in the nima app.
organizationIdstring or nullThe workspace the credential acts in.
orgRoleenum or nullowner, 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_tokenstring64 hex characters. Send it as a bearer token.
refresh_tokenstring64 hex characters. Store it. It is replaced every time you use it.
token_typestringAlways Bearer.
expires_inintegerAlways 3600. It is a constant, not a measurement: the access token is good
for one hour from issue.
scopestringSpace 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.
| Scope | Consent screen label | What it unlocks | Who can grant it |
|---|---|---|---|
knowledge:read | Search what nima knows | ask_knowledge_graph, search_code, blast_radius, get_data_model | Any member |
signals:read | Read signals and beacons | search_signals, get_signal, list_beacons, get_beacon, get_history | Any member |
sources:read | Read the sources behind signals | get_signal_source | Owners and admins |
documents:read | Read specs and documents | list_specs, get_document_markdown, get_spec_delivery, and the read tools for operations | Any member |
signals:write | File signals for you | create_signal | Any member |
subscriptions:read | See what you are watching | list_event_types, list_watches, get_scheduling_context, list_scheduled_messages | Any member |
subscriptions:write | Manage your subscriptions | create_subscription, update_subscription, unwatch, schedule_message, update_scheduled_message, cancel_scheduled_message | Any member |
inbox:read | Read your inbox | get_inbox | Any 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.
Workspace settings
Read and change workspace-level settings, including the switches for watches, outbound actions and chat fact capture.
Cookbooks
Which runnable recipe gives an agent built on LangChain, OpenAI Agents, Anthropic, Vercel AI SDK or Google ADK nima's MCP tools, with a plain HTTP fallback?