Rate limits
The three throttle tiers, which routes sit on each, how buckets are keyed, and how to back off, including through MCP.
Availability
On for every workspace. Rate limits apply to every caller of api.getnima.io
and mcp.getnima.io and cannot be raised per workspace.
Every request to api.getnima.io passes one limiter before the route's own
checks and before the handler runs. A personal API key or OAuth token is checked
first: a request whose key or token is invalid, or lacks the scope for the
route, is refused before the limiter sees it. There are three tiers. Which tier
a route sits on is fixed: it does not vary by workspace, by plan, or by how you
authenticated. Counters are shared across all of nima's servers, so opening more
connections buys you nothing.
The three rate-limit tiers
| Tier | Limit | Window | What sits there |
|---|---|---|---|
| Default | 100 requests | 60 seconds | everything not listed below |
| Soft | 300 requests | 10 seconds | signal ingest and the expensive reads |
| Hard | 15 requests | 30 seconds | the OAuth endpoints that mint or destroy a credential, and a few session-only routes |
A route sits on exactly one tier. A route on the soft tier is not also held to 100 per minute: the tier replaces the default rather than stacking on it.
Routes on the soft tier (300 per 10 seconds)
These are the soft-tier routes an API key or OAuth token can reach.
| Method | Path | Also reachable as MCP tool |
|---|---|---|
POST | /v1/signals | create_signal |
GET | /v1/signals/{id}/source | get_signal_source |
GET | /v1/code-intel/search | search_code |
GET | /v1/code-intel/blast-radius | blast_radius |
GET | /v1/code-intel/data-model | get_data_model |
GET | /v1/knowledge-graph/ask | ask_knowledge_graph |
POST | /v1/webhooks/signals/{id} | none |
The last one is the webhook ingest endpoint, which
authenticates with an endpoint secret rather than a token. It has per-endpoint
budgets of its own on top of this tier, and those answer 202 with
"reason": "rate_limited" rather than 429.
Routes on the hard tier (15 per 30 seconds)
| Method | Path |
|---|---|
POST | /oauth/token |
POST | /oauth/revoke |
POST | /oauth/register |
POST /v1/api-keys and POST /v1/api-keys/{id}/rotate are also on the hard
tier, but only a signed-in session can call them.
Mint once, hold the token
If you fetch a fresh access token for every call, POST /oauth/token is the
limit you will hit first, at 15 in 30 seconds, long before any read limit is
in sight. An access token is good for one hour. A personal API key needs no
token call at all.
Routes on the default tier (100 per 60 seconds)
Everything else you can reach with a key or an OAuth token is on the default
tier: the signal and beacon reads, specs and their versions and sections,
watches, the inbox, scheduled messages, operations, action policies,
GET /oauth/userinfo, and POST /mcp itself. The browser consent page
GET /oauth/authorize is also on the default tier.
Routes that only a signed-in session can reach, the admin panel's own, carry tiers of their own. Because a bucket is per endpoint, they never spend an API caller's budget.
How a rate-limit bucket is keyed
A rate-limit bucket is one endpoint for one caller address. Two things decide which counter a request lands in.
The endpoint. Every route handler keeps its own counter. GET /v1/signals
and GET /v1/signals/{id} do not share one, so draining your signal search
budget leaves reads of a single signal untouched. The tier numbers above are
per endpoint, not a total across the API.
The caller's address. A bucket is per client IP address, not per API key, user or workspace.
Three consequences worth planning around. Everything behind one egress
address shares a bucket, so a CI runner and a colleague's script on the same
office network spend from the same 100. Rotating a key, switching users or
re-authenticating does not give you a fresh bucket. And almost every request
to a route that exists counts, including ones that end in a 403 from a role
check, a 404 for a missing resource or a 400. The exceptions are requests
refused while your API key or OAuth token is checked (an invalid or expired
key or token, or one without the route's scope), which are rejected before the
limiter runs.
Calls that reach nima through api.getnima.io or mcp.getnima.io are always
keyed on your address. Only nima's own hosted agents, whose traffic does not come
through that public edge, are keyed on the calling workspace and session
instead.
A request that costs nothing is one to a path that matches no route at all: nothing of ours runs, so the limiter never sees it. The same holds for a request refused because its API key or OAuth token is invalid or lacks the route's scope.
The address is the one the load balancer saw. X-Forwarded-For is never
consulted, and the header the API does read is stamped by the load balancer on
every request, overwriting whatever you sent. You cannot move yourself into a
different bucket by sending one.
Rate-limit response headers
Successful responses carry X-RateLimit-* headers and the 429 carries
Retry-After. These header claims are as of 2026-09-22, not re-verified.
| Header | Sent on | Value |
|---|---|---|
X-RateLimit-Limit | responses under the limit | the ceiling for that endpoint |
X-RateLimit-Remaining | responses under the limit | requests left in this window |
X-RateLimit-Reset | responses under the limit | seconds until the counter resets, not a timestamp |
Retry-After | the 429 only | whole seconds to wait |
curl -si "https://api.getnima.io/v1/signals?limit=1" \
-H "Authorization: Bearer $NIMA_API_KEY" \
| grep -iE '^(HTTP/|x-ratelimit|retry-after)'HTTP/2 200
x-ratelimit-limit: 100
x-ratelimit-remaining: 97
x-ratelimit-reset: 43Remaining reaches 0 on the last request you are allowed. The next one is
the 429.
As of 2026-09-22, not re-verified: the 429 carries Retry-After and none of
the X-RateLimit-* headers, so track the budget from the successful
responses. None of the four headers was listed in
Access-Control-Expose-Headers, so browser JavaScript could not read them.
Server-side callers see all of them.
What a 429 rate-limit response looks like
Going over blocks that endpoint, for that address, for a full window: 60
seconds on the default tier, 30 on hard, 10 on soft. The block starts at the
request that went over rather than at the start of the window, and Retry-After
counts it down. As of 2026-09-22 (not re-verified), requests made while blocked
are refused and counted, but do not push the deadline back.
HTTP/2 429
retry-after: 27
content-type: application/json; charset=utf-8{
"statusCode": 429,
"error": "RATE_LIMITED",
"message": "ThrottlerException: Too Many Requests",
"timestamp": "2026-09-22T10:14:03.512Z",
"path": "/v1/signals?limit=20"
}Match on statusCode or on error, never on message. The string is a
framework constant and carries nothing about the endpoint or the tier. There is
no details object on it, so Retry-After is the only number you get. See
Errors for the shared envelope.
Rate limits through the MCP server
A tool call on the remote MCP server spends from
two buckets at once, both keyed to your address: POST /mcp on the default
tier, and the underlying route on whatever tier it sits on. Six tools are on the
soft tier: create_signal, get_signal_source, search_code, blast_radius,
get_data_model and ask_knowledge_graph. Every other tool is on the default
tier.
Because each call also spends from POST /mcp on the default tier, the
practical ceiling is about 100 tool calls per minute per address, whichever
tool you call.
When the inner call is limited you do not see an HTTP 429. The MCP response
is 200, and the tool result comes back flagged isError with the status and
body inlined as text, beginning 429 {"statusCode":429,. Back off the same way
you would for an HTTP 429.
When the POST /mcp bucket itself runs out, the whole request answers HTTP
429 with Retry-After, like any other route.
Per-feature caps that are not tiers
Several features enforce their own caps, separate from the three tiers. Each is documented on the page for that feature.
| Feature | Cap | Where it is documented |
|---|---|---|
| Inbound signal webhooks | 50 requests per minute per endpoint, an hourly cap per endpoint (default 50, up to 1000), 10 endpoints per workspace | Webhooks |
| Outbound dispatch | 200 proposed actions per workspace per rolling hour; further proposals are refused with reason cap | Outbound actions, Action policies |
| Watches (triggers) | 200 fires per workspace per hour, then further fires are dropped rather than replayed | Watches and reminders |
| Scheduled reminders | 100 pending reminders per person; the 101st answers 409 until you cancel one | Scheduled messages |
| Manual repository re-index | once per 24 hours per repository | Repository re-index cooldown |
Repository re-index cooldown
Re-indexing a connected repository by hand is capped at once per 24 hours per
repository, and going over answers 429 with a body that tells you when to
come back:
{
"statusCode": 429,
"error": "RATE_LIMITED",
"message": "This repository was re-indexed less than 24 hours ago",
"details": {
"retryAfterSeconds": 64800,
"nextAllowedAt": "2026-09-23T04:14:03.512Z"
},
"timestamp": "2026-09-22T10:14:03.512Z",
"path": "/v1/code-intel/repositories/8f1d2c3a-0b44-4e7d-9a61-2c5f7e8d0a13/reindex"
}It sends no Retry-After header, because it is raised after the tier check has
already passed. The route declares no scope, so an API key or an OAuth token
cannot call it. Automatic re-indexing on push is not affected by the cooldown.
How to back off after a 429
Read Retry-After when it is there, fall back to exponential backoff when it is
not, and add jitter either way so a fleet of workers does not return in
lockstep. Retry 429 and 503. Never retry any other 4xx: the limiter is the
only one that will change its mind.
const BASE_URL = 'https://api.getnima.io';
const MAX_ATTEMPTS = 5;
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
export async function nimaFetch(
path: string,
init: RequestInit = {},
): Promise<Response> {
for (let attempt = 0; ; attempt++) {
const response = await fetch(`${BASE_URL}${path}`, {
...init,
headers: {
Authorization: `Bearer ${process.env.NIMA_API_KEY}`,
...init.headers,
},
});
const retryable = response.status === 429 || response.status === 503;
if (!retryable || attempt >= MAX_ATTEMPTS - 1) return response;
const header = Number(response.headers.get('retry-after'));
const waitSeconds =
Number.isFinite(header) && header > 0 ? header : 2 ** attempt;
await sleep(Math.max(waitSeconds, 1) * 1000 + Math.random() * 500);
}
}Because the bucket is per endpoint, a worker blocked on one route can keep
working on another. Backing off the whole client on a single 429 costs you
throughput you still had.
Paging results
Which nima endpoints return pages, the page parameters each takes, the 3000-row ceiling, and which lists are capped arrays instead of pages.
API conventions
The rules that hold across every nima endpoint: vocabulary, ids, casing, null, timestamps, enums, query arrays, the filter DSL and the base path of each resource.