docs
API

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

TierLimitWindowWhat sits there
Default100 requests60 secondseverything not listed below
Soft300 requests10 secondssignal ingest and the expensive reads
Hard15 requests30 secondsthe 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.

MethodPathAlso reachable as MCP tool
POST/v1/signalscreate_signal
GET/v1/signals/{id}/sourceget_signal_source
GET/v1/code-intel/searchsearch_code
GET/v1/code-intel/blast-radiusblast_radius
GET/v1/code-intel/data-modelget_data_model
GET/v1/knowledge-graph/askask_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)

MethodPath
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.

HeaderSent onValue
X-RateLimit-Limitresponses under the limitthe ceiling for that endpoint
X-RateLimit-Remainingresponses under the limitrequests left in this window
X-RateLimit-Resetresponses under the limitseconds until the counter resets, not a timestamp
Retry-Afterthe 429 onlywhole 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: 43

Remaining 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.

FeatureCapWhere it is documented
Inbound signal webhooks50 requests per minute per endpoint, an hourly cap per endpoint (default 50, up to 1000), 10 endpoints per workspaceWebhooks
Outbound dispatch200 proposed actions per workspace per rolling hour; further proposals are refused with reason capOutbound actions, Action policies
Watches (triggers)200 fires per workspace per hour, then further fires are dropped rather than replayedWatches and reminders
Scheduled reminders100 pending reminders per person; the 101st answers 409 until you cancel oneScheduled messages
Manual repository re-indexonce per 24 hours per repositoryRepository 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.

On this page