# Rate limits

URL: https://igregulator.io/docs/rate-limits/
Markdown: https://igregulator.io/docs/rate-limits.md

> iGregulator API limits: 10 requests per IP per hour without a key, monthly and per-second quotas per plan with one, the rate-limit headers, and handling 429s.

Two independent systems: **keyless calls** are capped per IP, and **keyed
calls** are metered per account against your plan. A valid key on a public
endpoint takes that call off the IP cap and onto your plan.

## Public endpoints (no key)

| Endpoint | Limit |
| --- | --- |
| `GET /v1/check` | 10 req / IP / hour |
| `GET /v1/jurisdictions` | 10 req / IP / hour |
| `GET /v1/operators/search` | 10 req / IP / hour (+ 3-row cap on `limit`) |
| `GET /v1/health/coverage` | 10 req / IP / hour |

`GET /v1/health` and `GET /v1/stats` (the headline counts the homepage
shows) are keyless and on no per-IP cap. `/v1/stats` answers everyone with one
payload, recounted at most every 10 minutes, so calling it costs the database
nothing and a homepage visit never spends your lookups. A key sent with it is
ignored and not charged.

The counters are **per route, per IP**: ten `/v1/check` calls don't use up
your `/v1/operators/search` allowance. A route's counter counts every keyless
call to it from your IP, whatever made the call — including the
[MCP server](https://igregulator.io/docs/mcp/#rate-limits), which forwards your IP: a keyless
`check_domain` counts against `/v1/check` exactly like a direct request,
`search_operators` against `/v1/operators/search`, `list_jurisdictions` against
`/v1/jurisdictions` and `check_coverage` against `/v1/health/coverage`. A
`domain` check and a `license_number` check share the one `/v1/check` counter.

The window is clock-aligned: every counter resets at the top of each UTC hour. A
caller that burns 10 requests at 14:58 waits two minutes, not sixty.

## Authenticated endpoints

| Plan | Monthly quota | Per-second limit |
| --- | --- | --- |
| Starter | 10,000 calls | 5 req/sec |
| Pro | 100,000 calls | 20 req/sec |
| Business | Fair use — no monthly cap enforced | 100 req/sec |
| Enterprise | Custom | Negotiated |

Both limits are enforced today, on every keyed request, before the request
reaches the endpoint. Both are **per account**: every key on the account
shares one monthly counter and one per-second counter. The monthly counter
resets at 00:00 UTC on the 1st; the per-second counter is a fixed
one-second window.

**Only answers count against the monthly quota.** A request we refuse (any 4xx:
an invalid parameter, an unknown slug) or fail (5xx) is not charged, and its
`X-Monthly-Quota-Used` / `X-RateLimit-Remaining` headers say so. It still counts
toward the per-second limit.

### Exports

[CSV/JSON export](https://igregulator.io/docs/export/) (`GET /v1/export/:dataset`, Pro and above) has
its own daily cap on top of these: 10 exports per UTC day on Pro, 100 on
Business, none on Enterprise — per account, shared with the dashboard's Export
button. Each export counts as **one** request against the monthly quota,
whatever its size. Starter has no exports (`402 export_requires_pro`).

## Legacy `trial` keys

Accounts created today are on Starter. A few older accounts are still on
the `trial` tier, and their keys carry two extra restrictions on top of the
monthly quota and per-second limit above:

- **`GET /v1/check` and the endpoints that work without a key** (`GET
  /v1/jurisdictions`, `GET /v1/operators/search`, `GET /v1/health/coverage`).
  Any other endpoint answers `402 payment_required` with `details.reason:
  "endpoint_requires_paid_plan"` (`/v1/export` says what it needs instead:
  `export_requires_pro`). (Until API 1.14.1 the public endpoints
  refused a trial key too, so a key left its holder worse off than no key.)
- **1,000 requests per UTC day, per key**, reset at `00:00:00Z`.

Over the daily cap → `429 rate_limited`:

```json
{
  "error": "Pre-launch rate limit exceeded",
  "code": "rate_limited",
  "details": {
    "reason": "prelaunch_daily_cap",
    "current_usage": 1001,
    "limit": 1000,
    "reset_at": "2026-09-30T00:00:00.000Z",
    "suggestion": "Pre-launch keys are capped at 1000 requests/day; the counter resets at reset_at. …"
  }
}
```

The response carries `Retry-After` (seconds until midnight UTC) and
`X-Prelaunch-Daily-Limit`, `-Used`, `-Reset`.

## Response headers

### Keyless calls

Every response to a call made without a key on the four public endpoints
includes:

| Header | Meaning |
| --- | --- |
| `X-RateLimit-Limit` | Ceiling for this IP on this endpoint in the current window: `10`. |
| `X-RateLimit-Remaining` | Requests left. Never negative. |
| `X-RateLimit-Reset` | Unix epoch seconds when the window rolls over (the top of the next UTC hour). |
| `X-RateLimit-Policy` | Human-friendly policy string: `tier=public;limit=10;window=hour`. Hand-readable, easy to `awk`. |
| `RateLimit-Policy` | IETF draft format ([draft-ietf-httpapi-ratelimit-headers](https://datatracker.ietf.org/doc/draft-ietf-httpapi-ratelimit-headers/)): `"default";q=10;w=3600`. Modern HTTP clients (Cloudflare SDK, Kong, etc.) auto-parse this. |
| `X-Upgrade-URL` | `https://igregulator.io/pricing` — the header's value today. To keep going past a limit: without a key, get a free one at [app.igregulator.io/signup](https://app.igregulator.io/signup); with a key, email founder@igregulator.io. |

The two policy headers in this form are sent **only on keyless calls**.

### Keyed calls

A keyed call reports your **monthly** quota — the number you budget
against — not the per-second limit:

| Header | Meaning |
| --- | --- |
| `X-RateLimit-Limit` | Your plan's monthly quota (e.g. `10000`). |
| `X-RateLimit-Remaining` | Monthly calls left. |
| `X-RateLimit-Reset` | Unix epoch seconds of the monthly reset (00:00 UTC on the 1st). |
| `X-Monthly-Quota-Limit` / `-Used` / `-Remaining` / `-Reset` / `-Warning` | The same monthly view, with an ISO-8601 reset and an 80 % warning — see [authentication](https://igregulator.io/docs/authentication/#6-rate-limits-and-quotas). |
| `X-RateLimit-Policy` | Sent in two cases only: `tier=unlimited` on plans with no monthly cap (Business, Enterprise — which get no `X-RateLimit-Limit` / `-Remaining` / `-Reset`), and `tier=authenticated` when the key is used on one of the four public endpoints. |
| `X-Upgrade-URL` | As above. |

There is no `RateLimit-Policy` on keyed calls, and the per-second limit is
not advertised on a successful response. Only a per-second 429 describes
it: there `X-RateLimit-Limit` is the per-second limit and
`X-RateLimit-Reset` is the next second.

### Parsing the policy headers (keyless calls)

```js
// X-RateLimit-Policy — custom: tier=public;limit=10;window=hour
const policyCustom = Object.fromEntries(
  res.headers.get('X-RateLimit-Policy').split(';').map((kv) => kv.split('=')),
);
// { tier: 'public', limit: '10', window: 'hour' }

// RateLimit-Policy — IETF: "default";q=10;w=3600
const policyIetf = res.headers.get('RateLimit-Policy');
const q = policyIetf.match(/q=(\d+)/)?.[1];  // quota
const w = policyIetf.match(/w=(\d+)/)?.[1];  // window in seconds
```

## Handling 429

Six different limits answer 429. `code` alone does not tell them apart —
branch on `code` **and** `details.reason`:

| `code` | `details.reason` | Cause | Wait until |
| --- | --- | --- | --- |
| `rate_limited` | `quota_exceeded` | Keyless 10 / hour / IP cap. Here `quota_exceeded` means the hourly IP allowance, not your monthly quota. | `details.reset_at` / `X-RateLimit-Reset` — the top of the next UTC hour (no `Retry-After`) |
| `rate_limited` | `per_second_limit_exceeded` | Your plan's per-second limit. | `Retry-After: 1` |
| `quota_exceeded` | `monthly_request_limit_reached` | Your plan's monthly quota is used up. | `details.reset_at` / `X-Monthly-Quota-Reset` — the 1st of next month (no `Retry-After`) |
| `rate_limited` | `prelaunch_daily_cap` | Legacy `trial` key over 1,000 requests/day. | `Retry-After` |
| `rate_limited` | `watchlist_events_poll_limit` | `GET /v1/watchlist/events` over your plan's polls per hour ([watchlist](https://igregulator.io/docs/watchlist/)). | `details.reset_at` |
| `rate_limited` | `export_daily_limit_reached` | `GET /v1/export/:dataset` over your plan's exports per UTC day ([export](https://igregulator.io/docs/export/#limits)). | `Retry-After` — 00:00 UTC |

The keyless 429, in full:

```http
HTTP/2 429
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1790694000
X-RateLimit-Policy: tier=public;limit=10;window=hour
RateLimit-Policy: "default";q=10;w=3600
X-Upgrade-URL: https://igregulator.io/pricing

{
  "error": "Public rate limit reached (10/hour/IP).",
  "code": "rate_limited",
  "details": {
    "reason": "quota_exceeded",
    "suggestion": "Get a free API key (no card, 10,000 requests/month) at https://app.igregulator.io/signup and send it as \"Authorization: Bearer <key>\" — keyed calls skip this per-IP cap. Or wait until reset_at, the top of the next hour (UTC; also the X-RateLimit-Reset header).",
    "limit": 10,
    "reset_at": "2026-09-29T15:00:00.000Z"
  }
}
```

`details.reset_at` and `X-RateLimit-Reset` name the same instant — the top of the
next UTC hour, when this hour's counter stops counting — one as ISO-8601, the
other as Unix epoch seconds (`1790694000` above).

Recommended client behaviour:

1. Check `X-RateLimit-Remaining` before every call.
2. On 429, wait until the time in the table above, then retry once.
3. If the same caller keeps hitting 429, that's a signal to change something — not to back-off-and-retry indefinitely. Without a key: get a free one at [app.igregulator.io/signup](https://app.igregulator.io/signup). With a key: email founder@igregulator.io.

## Tips

- **Don't scrape the public endpoint.** Paginate the authenticated `/v1/jurisdictions/:code/operators` list once a day and cache — every register is read once a day, so polling more often buys nothing.
- **Front-ends that surface check results to end-users** — apply the 10/hour IP limit on *your* server and call the API with a single authenticated key; don't let every browser session hit us directly or the shared IP will burn out your quota.
- **Bulk re-verification** (weekly AML sweep of 2,000 domains) — use [`POST /v1/check/batch`](https://igregulator.io/docs/batch/): 100 domains per request, each request one call against your quota. Use the operator / licence endpoints for the detail behind a verdict.
