Rate limits
Two independent ceilings: public per-IP (no key) and authenticated per-plan (monthly quota). Authenticated requests skip the IP ceiling entirely.
Public endpoints (no key)
Section titled “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) |
The window is clock-aligned: the 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
Section titled “Authenticated endpoints”| Plan | Monthly quota | Burst |
|---|---|---|
| Starter | 10,000 calls | 5 req/sec |
| Pro | 100,000 calls | 20 req/sec |
| Business | Fair use | No fixed cap |
| Enterprise | Custom | Negotiated |
Per-plan enforcement lands with Phase 2 billing. Plan-aware monthly quotas and burst ceilings activate once Stripe goes live. Until then a lightweight pre-launch daily cap of 10,000 requests/day per API key acts as a safety net — see below.
Pre-launch limitations
Section titled “Pre-launch limitations”While Stripe integration is pending, authenticated keys on any plan
tier except business / enterprise carry an additional soft
cap of 10,000 requests/day per key, counted per UTC day and reset
at 00:00:00Z. Rationale: a key shared with a reviewer or journalist
shouldn’t be able to silently exhaust our Cloudflare bandwidth budget
before billing lands. The 10k/day ceiling is deliberately picked to
exceed the Pro plan average (~3,333 req/day over a 30-day month at
100k/mo), so a realistic Pro customer never hits it.
Hit the cap → 429 rate_limited:
{ "error": "Pre-launch rate limit exceeded", "code": "rate_limited", "details": { "reason": "prelaunch_daily_cap", "current_usage": 10000, "limit": 10000, "reset_at": "2026-04-21T00:00:00.000Z", "suggestion": "Pre-launch keys are capped at 10000 requests/day. Upgrade to a paid plan at https://igregulator.io/pricing for production use." }}Response carries X-Prelaunch-Daily-Limit, -Used, -Reset headers
so clients can monitor consumption. Cap lifts automatically when the
plan enforcement layer takes over; no client change needed.
Response headers
Section titled “Response headers”Every public-endpoint response includes:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Ceiling for this caller on this endpoint in the current window (e.g. 10). |
X-RateLimit-Remaining | Requests left. Never negative. |
X-RateLimit-Reset | Unix epoch seconds when the window rolls over. |
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-09): "default";q=10;w=3600. Modern HTTP clients (Cloudflare SDK, Kong, etc.) auto-parse this. |
X-Upgrade-URL | https://igregulator.io/pricing — surfaced so a UI can link “upgrade to keep going” on 429. |
Parsing the policy headers
Section titled “Parsing the policy headers”// X-RateLimit-Policy — custom: tier=public;limit=10;window=hourconst 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=3600const policyIetf = res.headers.get('RateLimit-Policy');const q = policyIetf.match(/q=(\d+)/)?.[1]; // quotaconst w = policyIetf.match(/w=(\d+)/)?.[1]; // window in secondsHandling 429
Section titled “Handling 429”HTTP/2 429X-RateLimit-Limit: 10X-RateLimit-Remaining: 0X-RateLimit-Reset: 1776604800X-Upgrade-URL: https://igregulator.io/pricing
{ "error": "Public rate limit reached (10/hour/IP).", "code": "rate_limited", "details": { "limit": 10, "window_seconds": 3600, "upgrade_url": "https://igregulator.io/pricing" }}Recommended client behaviour:
- Check
X-RateLimit-Remainingbefore every call. - On 429, sleep until
X-RateLimit-Reset, then retry once. - If the same caller keeps hitting 429, that’s a signal to authenticate or upgrade — not to back-off-and-retry indefinitely.
- Don’t scrape the public endpoint. Paginate the authenticated
/v1/jurisdictions/:code/operatorslist once a day and cache — the data only refreshes at 03:00 UTC anyway. - 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 operators) — use the authenticated operator/licence endpoints, not
/v1/check.