Skip to main content
Grantex enforces per-IP rate limits using 1-minute sliding windows. These limits protect the service from abuse while allowing normal usage patterns. All rate limits apply at the IP level — there are no per-key limits.

Default Limits

EndpointLimitNotes
All endpoints (global)100 req/minDefault for every route
POST /v1/authorize10 req/minStricter — creates auth requests
POST /v1/token20 req/minStricter — exchanges codes for tokens
POST /v1/token/refresh20 req/minStricter — refreshes grant tokens
GET /.well-known/jwks.jsonExemptPublic key distribution is never throttled

Response Headers

Every response includes rate limit headers so your application can track its budget:
HeaderDescription
X-RateLimit-LimitMaximum requests allowed in the current window
X-RateLimit-RemainingRequests remaining in the current window
X-RateLimit-ResetUnix timestamp (seconds) when the window resets
Retry-AfterSeconds to wait before retrying (only on 429 responses)

429 Error Response

When you exceed a rate limit, the API returns a 429 Too Many Requests status with the following body:
{
  "message": "Rate limit exceeded, retry in 42 seconds",
  "code": "BAD_REQUEST",
  "requestId": "a1b2c3d4-e5f6-..."
}
The Retry-After header tells you exactly how long to wait.

Reading Rate Limits from SDKs

All three SDKs automatically parse rate limit headers from every response. You can read them via client.lastRateLimit (TypeScript/Python) or client.LastRateLimit() (Go).

After a Successful Call

import { Grantex } from '@grantex/sdk';

const client = new Grantex({ apiKey: 'gx_...' });
const agents = await client.agents.list();

const rl = client.lastRateLimit;
if (rl) {
  console.log(`${rl.remaining}/${rl.limit} requests left, resets at ${rl.reset}`);
}

Handling 429 Errors

When a 429 is returned, the error object includes rate limit info with the retryAfter value:
import { GrantexApiError } from '@grantex/sdk';

try {
  await client.tokens.verify(token);
} catch (err) {
  if (err instanceof GrantexApiError && err.rateLimit?.retryAfter) {
    console.log(`Rate limited — retry in ${err.rateLimit.retryAfter}s`);
  }
}

Retry Strategy

Use exponential backoff with jitter to avoid thundering-herd problems when multiple clients hit the limit simultaneously.
import { GrantexApiError } from '@grantex/sdk';

async function withRetry<T>(fn: () => Promise<T>, maxRetries = 3): Promise<T> {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await fn();
    } catch (err: unknown) {
      if (!(err instanceof GrantexApiError) || err.statusCode !== 429 || attempt === maxRetries) throw err;

      const base = (err.rateLimit?.retryAfter ?? 2 ** attempt) * 1000;
      const jitter = Math.random() * 500;
      await new Promise((r) => setTimeout(r, base + jitter));
    }
  }
  throw new Error('Unreachable');
}

Best Practices

The JWKS endpoint (/.well-known/jwks.json) is exempt from rate limits. Prefer offline token verification over online POST /v1/tokens/verify calls to avoid hitting limits entirely.
  • Cache tokens — Grant tokens are valid JWTs. Store and reuse them until they expire instead of requesting new ones per operation.
  • Use offline verification — Call verifyGrantToken() with the JWKS URI to validate tokens locally. The JWKS endpoint is never rate-limited.
  • Use webhooks instead of polling — Subscribe to webhook events like grant.created and grant.revoked rather than polling grant or audit endpoints.
  • Honor Retry-After — When you receive a 429, always use the Retry-After header value as your minimum wait time.
  • Spread requests — If your system makes burst requests (e.g., batch token exchanges), add short delays between calls.

Self-Hosted Deployments

If you’re running the Grantex auth service yourself, rate limits are fully configurable. The global limit is set in apps/auth-service/src/server.ts and per-route limits are set in individual route files. See the Self-Hosting guide for deployment instructions.