Rate Limiting
The Mandaitor API enforces rate limits to ensure fair usage and protect service stability. This guide explains what is enforced today, the limits per route, and how to handle rate limit errors in your application.
How Rate Limiting Works
Limits are enforced at three layers, all in front of the application:
- Per-address flood protection (AWS WAF). Each source IP address may make at most 6,000 requests in any five-minute window (about 20 requests per second sustained). Tighter per-address caps apply to the public, credential-free routes listed below. Partners whose traffic leaves through a shared egress address can have that range registered so it is not treated as a single client; contact support@mandaitor.io.
- Platform throttles (AWS API Gateway). A token-bucket limit of 500 requests per second with a burst of 1,000 applies to the API as a whole, and stricter per-route limits apply to expensive or public operations. These limits are the ceiling for all callers together.
- Per-tenant allowance (AWS API Gateway usage plans). Under that ceiling, every tenant is throttled in its own bucket, so another tenant's burst cannot push you into
429. Your allowance depends on your plan (see Per-Tenant Limits). It is attached to your tenant by the platform when your credentials are validated; you do not send an API Gateway key or any extra header.
When a limit is exceeded, the API returns 429 Too Many Requests. The response carries a Retry-After header when the server can tell you how long to wait.
Paid plans have no monthly quota: a limit that cut a paying tenant off mid-month would be worse than the problem it solves. Volume commitments for partners are contractual rather than technical.
Free-tier accounts do have a monthly ceiling — see Monthly Allowance below. Your usage and remaining allowance are visible via GET /tenants/{id}/usage (see Billing & Usage).
Per-Tenant Limits
Each tenant has one token bucket per plan, shared by all of that tenant's API keys and dashboard users. Values are for production.
| Plan | Sustained rate | Burst | POST /mandates | Lifecycle routes (revoke, suspend, reactivate) | POST /tenants/{id}/api-keys |
|---|---|---|---|---|---|
standard | 50 req/s | 100 | 5 req/s, burst 10 | 3 req/s, burst 6 | 1 req/s, burst 2 |
enterprise | 200 req/s | 400 | 10 req/s, burst 20 | 5 req/s, burst 10 | 1 req/s, burst 3 |
New tenants start on the standard plan. Exceeding your allowance returns 429 Too Many Requests for your tenant only; the per-route limits below and the platform ceiling still apply on top.
Monthly Allowance
The limits above bound how fast you can call. Free-tier accounts are additionally bounded in how much: 100,000 billable units per calendar month.
Units are weighted per operation, so the ceiling is not a flat request count:
| Operation | Units |
|---|---|
POST /verify | 1 |
POST /verify?pom=... | 2 |
POST /verify?drift=true | 1.5 |
POST /decisions | 2 |
POST /mandates | 5 |
Lifecycle (revoke, suspend, reactivate) | 2 |
Reads (GET /mandates, GET /events) | 0 — never counted |
Reads are free and never consume allowance.
A decision counts whether it allows or denies. A DENY costs the same
mandate lookup, policy evaluation and audit write as an ALLOW, so it consumes
the same units. A request-bound decision counts at the
Proof-of-Mandate rate, as the verification with a Proof-of-Mandate it succeeds. Redeeming its
authorization (POST /decisions/{id}/redemption) is not counted.
Until this date, denials and verifications that issued a Proof-of-Mandate were
not counted at all, and POST /mcp/authorize, removed since, was not counted at all. This was a
defect, not a discount. Metered volume on those paths will appear higher from
now on — the traffic did not change, the counting did. If it changes what you
expect to owe, write to support@mandaitor.io.
What happens at the ceiling. Your API keys are refused for the rest of the calendar month; requests authenticated with an API key return 403. Dashboard sign-in is deliberately unaffected, so you can still open the dashboard, review your usage and subscribe to a paid plan. The allowance resets at the start of the next month with no action needed.
Checking where you stand. GET /tenants/{id}/usage returns an allowance block:
{
"allowance": {
"metered": true,
"period": "2026-09",
"limit": 100000,
"used": 41280,
"remaining": 58720
}
}
A tenant on a paid plan reports "metered": false with limit and remaining as null: paid plans are not capped.
If you need more during an evaluation, write to support@mandaitor.io before you hit the ceiling — the limit is per-account and can be raised without moving you to a paid plan.
Per-Route Limits
The following routes have their own throttles, shared by all callers and below the platform-wide limit. Values are for production.
| Route | Limit | Rationale |
|---|---|---|
POST /mandates | 10 req/s, burst 20 | Each creation involves KMS signing |
POST /mandates/{id}/revoke | 5 req/s, burst 10 | Lifecycle state change + audit event |
POST /mandates/{id}/suspend | 5 req/s, burst 10 | Lifecycle state change + audit event |
POST /mandates/{id}/reactivate | 5 req/s, burst 10 | Lifecycle state change + audit event |
POST /tenants/{id}/api-keys | 1 req/s, burst 3 | Sensitive, rare operation |
POST /onboarding/request | 2 req/s, burst 5; plus 20 requests per address per 5 minutes and 10 accepted submissions per address per hour | Public endpoint |
POST /assistant/docs-chat | 3 req/s, burst 6; plus 60 requests per address per 5 minutes | Public endpoint, one model call per request |
POST /admin/auth/{action}, POST /mcp/token | 10 req/s, burst 20; plus 100 credential-free requests per address per 5 minutes | Sign-in and token endpoints (brute-force protection) |
POST /mcp | 25 req/s, burst 50 | The MCP server (Preview), for every agent together. Each agent also has a limit of its own, below |
GET /eudi/request/{id}, POST /eudi/sessions/{id}/response | 20 req/s, burst 40 | Public wallet endpoints |
GET /widgets/{widgetId}/config | 20 req/s, burst 40 | Public read |
POST /verify, GET /mandates, GET /events | Platform limit | Read-only, no per-route restriction |
POST /decisions, POST /decisions/{id}/redemption | Platform limit | Request-bound decisions (Preview); your plan's per-tenant limit applies |
Executions (Preview)
The execution API limits each agent rather than each route:
| Limit | Past it |
|---|---|
30 POST /executions requests a minute, counted whether or not a request is refused. A tool call on the MCP server counts as one; get_execution does not | 429 RATE_LIMITED, with Retry-After; on the MCP server, a tool error that says when to try again |
| 60 requests a minute to the MCP server, whatever they ask, counted whether or not a request is refused. A tool call counts here as well as against the 30 above | 429, with Retry-After and a JSON-RPC error (-32029). Nothing is read for the request |
| 20 approval requests a UTC day | A request that needs a person's approval is denied with APPROVAL_LIMIT_REACHED |
Execution requests, and requests to the MCP server, do not count against the monthly allowance today.
Handling Rate Limit Errors
When you exceed a limit, the API returns:
{
"error": "TOO_MANY_REQUESTS",
"message": "Rate limit exceeded"
}
Requests blocked at the WAF layer return a 403 with an empty body instead; treat a 403 on a route you are otherwise authorised for, arriving after a burst of traffic, the same way as a 429.
Recommended approach: Exponential backoff
Implement exponential backoff with jitter to gracefully handle rate limiting:
async function withBackoff<T>(
fn: () => Promise<T>,
maxRetries = 3,
baseDelay = 1000
): Promise<T> {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error: any) {
const status = error?.status ?? error?.response?.status;
if (status !== 429 || attempt === maxRetries) throw error;
const delay = baseDelay * Math.pow(2, attempt);
const jitter = delay * 0.5 * Math.random();
await new Promise((r) => setTimeout(r, delay + jitter));
}
}
throw new Error("Unreachable");
}
Best practices for staying within limits
-
Batch where possible: If you need to create multiple mandates, space out the requests rather than sending them all at once.
-
Cache verification results: If you verify the same action repeatedly within a short time window, cache the result on your side instead of calling the API each time.
-
Use read endpoints efficiently: Use the
limitandcursorpagination parameters to fetch only the data you need. -
Monitor your usage: Track your request volume with
GET /tenants/{id}/usageso that agreed volumes are visible to both sides.
Need Higher Limits?
If your integration needs more than the standard per-tenant allowance, more than the per-address allowance (for example, many agents behind one egress address), or a per-route limit is too low for your use case, contact us at support@mandaitor.io. Partners can be moved to the enterprise plan without any change on their side, and registered partner egress ranges receive a separate, higher per-address cap.