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.
Every error the nima API returns is one JSON object with the same shape, whatever failed: an auth check, a query parameter, a row that does not exist, a database, an upstream service. Two things in it are contracts you can build on: the HTTP status, and the error field.
On for every workspace. The envelope and codes on this page apply to every route under /v1 and to the failures the MCP server wraps for tool calls.
API error envelope
Every failed nima API request returns a JSON body with statusCode, error, message, timestamp and path, plus details on some 4xx responses and incidentId on 5xx responses.
curl -i https://api.getnima.io/v1/signals/8f2c41a6-0b7d-4e19-9a3c-1d6f5b8e2704 \
-H "Authorization: Bearer $NIMA_API_KEY"HTTP/2 404
x-request-id: 3f0b2c6e-9a1d-4f2b-8c77-5f6f0a2d1b44
content-type: application/json; charset=utf-8{
"statusCode": 404,
"error": "RESOURCE_NOT_FOUND",
"message": "Signal not found",
"timestamp": "2026-09-22T09:14:02.418Z",
"path": "/v1/signals/8f2c41a6-0b7d-4e19-9a3c-1d6f5b8e2704"
}| Field | Type | Always | What it is |
|---|---|---|---|
statusCode | number | yes | The HTTP status, repeated in the body. |
error | string | yes | The machine-readable code. Branch on this. |
message | string | yes | Written for a person reading a log. It can change in any release. |
details | object | no | Structured context about this specific failure. Sent on 4xx only, and only when the failure has something to add. |
incidentId | string | no | Sent on 5xx only. The request id. Quote it when you report the failure. |
timestamp | string | yes | ISO 8601 UTC, taken when the error was rendered. |
path | string | yes | The request path, including the query string. |
Branch on the error field, never on the message
The error field is the value to switch on in an API client. message is prose for whoever reads the logs: it is not versioned, and on most 5xx responses it is replaced with a fixed generic string before it reaches you. Keep a default branch, because the set of codes can grow and some routes return an English phrase instead of a code (see Routes where the error field is an English phrase).
API error codes
The nima API uses eight error codes on scoped routes.
error | Status | When you get it |
|---|---|---|
TOKEN_INVALID | 401 | No Authorization header, or a key or access token that is unknown, revoked or past its expiry. |
INSUFFICIENT_PERMISSIONS | 403 | Your token's scopes do not cover the route, your workspace role does not carry the permission, your workspace plan lacks the feature, your key was used from an address outside its allowlist, or the route accepts a browser session only. |
VALIDATION_ERROR | 400 (413 for an oversized body) | Input nima parses itself was rejected: a malformed filter, an unknown facet field, a search query under two characters. Also an unparseable request body (400) or one over the size limit (413). |
RESOURCE_NOT_FOUND | 404 | The id does not exist, or it belongs to another workspace. Also agent operations when the platform is off. |
CONFLICT | 409 | The resource is in a state that forbids the operation, for example a spec that is still being generated, or an action whose approval cannot proceed. |
RATE_LIMITED | 429 | You went past the request budget for your address, or hit a server-side cap (the re-index cap carries details.retryAfterSeconds). |
SERVICE_UNAVAILABLE | 503 | Something nima depends on failed, timed out or was cancelled. Retryable. |
INTERNAL_SERVER_ERROR | 500 | A bug on nima's side. Retrying the same call usually reproduces it. |
OAuth token error codes
TOKEN_REPLAY and TOKEN_EXPIRED are 401 codes an API client meets only on the OAuth token exchange, POST /oauth/token, never on a /v1 resource route.
error | Status | When you get it |
|---|---|---|
TOKEN_REPLAY | 401 | An authorization code that was already used, or a refresh token that was already rotated or revoked. Reusing a rotated refresh token also revokes the whole token family, so every descendant token stops working and the user must sign in again. |
TOKEN_EXPIRED | 401 | An authorization code past its expiry, or a refresh token past its expiry. Start the authorization flow again. |
SESSION_EXPIRED, ONBOARDING_REQUIRED, NOT_ORG_MEMBER and NOT_ORG_ADMIN belong to browser-session routes, and the four INTEGRATION_* codes to the connector endpoints the product uses on your behalf. A scoped API client does not need handlers for them. See OAuth for the token exchange.
Routes where the error field is an English phrase
On some routes error is not one of the codes above but a plain English phrase from the underlying web framework: "Bad Request", "Not Found", "Forbidden", "Conflict". The status code is still correct.
This is a real gap in the contract, not a documentation simplification. Match on both forms, or on the status code.
error | Status | Where it happens today |
|---|---|---|
"Bad Request" | 400 | Request bodies that fail validation, and a malformed id on routes that check it (Validation failed (uuid is expected)). |
"Not Found" | 404 | Document, version and PRD section lookups. Also any path that matches no route at all. |
"Forbidden" | 403 | A document you neither own nor reach through your workspace. |
"Conflict" | 409 | A document write that lost a concurrent update, or a spec already promoted to a ticket. |
A path that matches no route is the one error nima never produced itself:
{
"statusCode": 404,
"error": "Not Found",
"message": "Cannot GET /v1/signalz",
"timestamp": "2026-09-22T09:14:02.418Z",
"path": "/v1/signalz"
}Validation failures on request bodies carry no field detail
A request body that fails validation returns status 400 with error: "Bad Request" and no details, so the response does not name the field that failed.
curl -X POST https://api.getnima.io/v1/signals \
-H "Authorization: Bearer $NIMA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"sourceLabel":"support desk"}'{
"statusCode": 400,
"error": "Bad Request",
"message": "Bad Request Exception",
"timestamp": "2026-09-22T09:14:02.418Z",
"path": "/v1/signals"
}The phrase "Bad Request Exception" is a framework artifact, not a description of your request. Treat a 400 with error: "Bad Request" as "re-read the schema for this route". Two related behaviours:
- Unknown body fields are dropped silently. A body field the route does not declare is stripped before the route sees it. A typo in a field name is not an error, it is a value that never arrives.
- Query parameters nima parses itself fail as
VALIDATION_ERRORwithdetails. For a filter,details.issues[].pathpoints at the condition that failed (for exampleconditions.0), not always at the exact key:
{
"statusCode": 400,
"error": "VALIDATION_ERROR",
"message": "filter is not a valid filter expression",
"details": {
"issues": [{ "path": "conditions.0", "message": "Invalid input" }]
},
"timestamp": "2026-09-22T09:14:02.418Z",
"path": "/v1/signals?filter=%7B%22combinator%22%3A%22and%22%2C%22conditions%22%3A%5B%7B%22field%22%3A%22status%22%2C%22operator%22%3A%22nope%22%7D%5D%7D"
}A malformed id can return 500
A path id that is not a UUID can return a generic 500 instead of a 400, because not every route checks the id before it reaches the database.
{
"statusCode": 500,
"error": "INTERNAL_SERVER_ERROR",
"message": "Internal server error",
"incidentId": "3f0b2c6e-9a1d-4f2b-8c77-5f6f0a2d1b44",
"timestamp": "2026-09-22T09:14:02.418Z",
"path": "/v1/signals/not-a-uuid"
}Every id the API returns is a UUID. Validate ids on your side before sending them, and do not read a 500 on an id-bearing route as an outage until you have checked the id.
403 INSUFFICIENT_PERMISSIONS: which check rejected the request
A 403 from a scoped nima API route carries error: "INSUFFICIENT_PERMISSIONS" (document routes can answer "Forbidden" instead, see above), and details says which check rejected you. Some cases have no details.
| What rejected you | details |
|---|---|
| Token scopes | { "requiredScopes": ["signals:write"] } |
| Workspace role | { "requiredPermissions": ["CREATE_CONTENT"], "mode": "and" } (and means all listed permissions are needed, or means any one) |
| Plan feature | { "feature": "documents", "plan": "free" } |
| Approval refused for this action (not an eligible approver, or a question nima asked on someone's behalf) | { "id": "<action id>" } |
| Route accepts a browser session only, or your key was used from a disallowed address | none |
{
"statusCode": 403,
"error": "INSUFFICIENT_PERMISSIONS",
"message": "OAuth token scopes do not cover this endpoint",
"details": { "requiredScopes": ["signals:write"] },
"timestamp": "2026-09-22T09:14:02.418Z",
"path": "/v1/signals"
}Some routes are declared under a plan feature (documents, prd_generation, code_intelligence and others). A workspace whose plan lacks that feature can get a 403 with details.feature set to the feature name. Handle it as a normal 403. Hosted agent compute is enforced separately and needs a plan feature the nima team grants.
If details is empty, the candidates are a route that is not part of the public surface and an IP allowlist on the key. See Authentication and Scopes.
404 for another workspace's id and for agent operations
A key authenticates into exactly one workspace, and an id that exists in a different workspace answers 404 RESOURCE_NOT_FOUND, identical to an id that never existed. Whether an id exists elsewhere is not something the API confirms.
Document writes are the exception: some answer 403 with error: "Forbidden" and the message "Not your document". Treat both as "you cannot have this one".
Agent operations routes answer a different 404 when agent operations are switched off for the whole platform:
{
"statusCode": 404,
"error": "RESOURCE_NOT_FOUND",
"message": "Agent operations are not available",
"details": { "feature": "…" },
"timestamp": "2026-09-30T09:14:02.418Z",
"path": "/v1/operations"
}details.feature holds an internal switch name; do not branch on its value. Agent operations are on for the platform today, so you should not see this in normal use; treat it as "not available here". See Operations.
409 CONFLICT on outbound actions: expired, already decided, dispatch off
Approving an outbound action can fail with 409 CONFLICT, and details.reason says why: expired, already_decided or dispatch_off.
details.reason | Message | What to do |
|---|---|---|
expired | The approval window has closed | The approval expired (approval requests last 24 hours). Nothing to retry. |
already_decided | This action was already decided | Someone approved or discarded it first. Read the action to see the outcome. |
dispatch_off | Outbound messages are turned off for this workspace; the action is still waiting and can be approved once they are back on (switch org_off or flag_off), or The writer account nima posts with is not connected or is turned off; approve again once an admin reconnects it (switch binding_disabled) | The action keeps waiting. Approve again once outbound dispatch is back on. |
For dispatch_off, details.switch names the switch that is off: org_off means the workspace setting dispatchEnabled is off (a workspace owner or admin turns it on), binding_disabled means the writer account nima posts with is not connected or is turned off, and flag_off means nima has outbound dispatch switched off for the whole platform. Every one of these carries details.id, the action id. Discarding an action stays allowed while dispatch is off.
Approving or discarding needs the actions:approve scope, which no API key or OAuth client can hold today. Over a key the approve route answers 403 INSUFFICIENT_PERMISSIONS with details.requiredScopes: ["actions:approve"], and over MCP the tool returns the headless-grant text below. Approvals happen on the Slack approval card; the 409 reasons above are what a signed-in session gets. A question nima asked on someone's behalf can never be approved over a bearer token (403 This action cannot be approved over MCP or an API key; use the Slack approval card). See Actions and Outbound actions.
429 RATE_LIMITED and the re-index cooldown
A 429 from the nima API is RATE_LIMITED, and it comes from two places: the per-address request budget, and named server-side caps that carry details.retryAfterSeconds.
The re-index cap is the one server-side cap an API client meets today (hosted agent tasks also have a tool-call cap, reported with details.limit). POST /v1/code-intel/repositories/:id/reindex is a signed-in owner or admin route (an API key gets 403), allows one manual re-index per repository every 24 hours, and answers 429 with the seconds left:
{
"statusCode": 429,
"error": "RATE_LIMITED",
"message": "This repository was re-indexed less than 24 hours ago",
"details": {
"retryAfterSeconds": 41230,
"nextAllowedAt": "2026-09-30T20:41:32.000Z"
},
"timestamp": "2026-09-30T09:14:02.418Z",
"path": "/v1/code-intel/repositories/2b0c9d1e-6f43-4a58-9d0e-7a1c3f5e8b21/reindex"
}Read details.retryAfterSeconds (or nextAllowedAt) when it is present.
For the request budget, as of 2026-09-22 a blocked call carries a Retry-After header in seconds and the body has no equivalent field, and calls that are not blocked carry X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset. The budget is keyed on your IP address, not on your key, so several keys calling from one host share a bucket. See Rate limits.
5xx server errors from the nima API: what you get and what to do
On a 5xx the body carries the code, a message and incidentId, never details; the message is a fixed generic string unless nima raised the error deliberately. Nothing else about the underlying failure reaches you. The full failure stays in nima's logs under that same id.
| Status | error | message | What it means | What to do |
|---|---|---|---|---|
500 | INTERNAL_SERVER_ERROR | Internal server error | A bug on nima's side. Nothing you send will fix it. | Do not hammer it. Report it with the incidentId. |
503 | SERVICE_UNAVAILABLE | Service temporarily unavailable | Something nima depends on failed, timed out or was cancelled. | Retry with exponential backoff. |
A few 503s carry a specific message instead of the generic one, for example when a count exceeds its time budget; retry those. A 503 whose message says a feature is not enabled will not clear on retry. A failure in a third-party integration also answers 503.
incidentId is the request id. The same value is on the X-Request-Id response header of every response, success or failure, so log that one header per call.
Malformed or oversized request body errors
A request body that is not valid JSON or is too large fails before the route runs.
| Status | error | Cause |
|---|---|---|
400 | VALIDATION_ERROR | The body is not valid JSON, or the connection dropped mid-body. |
413 | VALIDATION_ERROR | The body is over the size limit, 100 KB by the server default. |
MCP tool errors: isError results and headless grants
A failed MCP tool call usually comes back as a normal JSON-RPC result with isError: true and a text message, not as a protocol error, so an agent can read it and correct itself. The nima MCP server runs each tool call as a request to the API with your own token, so the API's guards, scopes and rate limits apply.
| Situation | What the agent sees |
|---|---|
| Arguments fail the tool's schema, or include an unknown key | isError text Invalid arguments: followed by the per-field problems. Unlike REST bodies, these name the field. |
| The API answers a 4xx | isError text with the status and the first 500 characters of the response body, for example 404 {...} or 429 {...}. |
| The API answers a 5xx, or does not answer | isError text <status> (see Cloud Logging), or upstream error (see Cloud Logging) when no response came back. Retry, and do not expect detail. |
| The tool does not exist, or only runs inside a hosted agent VM | A JSON-RPC error -32602 with Unknown tool: <name>. |
| A consent-scope tool and your token lacks the scope | An outer HTTP 403 with insufficient_scope, which makes a client such as Claude ask for consent again. |
| A headless-scope tool and your token lacks the scope | isError 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. |
The headless grant message means the tool needs a scope that OAuth consent never grants. For operations:write and integrations:write, a workspace owner or admin mints a key with /nima mcp-key operations or /nima mcp-key integrations in Slack. The approval and identity tools (actions:approve, identities:write) cannot be granted to any key today; approvals happen on the Slack card. See Scopes.
Handle nima API errors in a client
A client that retries only 503 and 429, keeps incidentId on every 5xx and never branches on message handles every case above.
resp = session.post(url, json=payload, headers=headers)
if resp.ok:
return resp.json()
body = resp.json()
code = body["error"]
details = body.get("details", {})
if resp.status_code >= 500:
log.error("nima %s %s incident=%s", resp.status_code, code, body.get("incidentId"))
if resp.status_code == 503:
raise Retryable(body["message"])
raise Fatal(body["message"])
if resp.status_code == 429:
wait = details.get("retryAfterSeconds") or int(resp.headers.get("Retry-After", "60"))
sleep(wait)
raise Retryable("rate limited")
if resp.status_code == 409 and details.get("reason") == "dispatch_off":
raise Waiting("outbound dispatch is off; the action is still pending")
if code in ("VALIDATION_ERROR", "Bad Request"):
raise Fatal(f"{body['message']} {details}")
raise Fatal(f"{code}: {body['message']}")