- Documentation
- Get started
Rate limits & quotas
What each plan allows, how throttling behaves, and how to stay under it.
Two independent limits apply to every key: a per-minute rate and a monthly request quota. Both come from your plan.
Plan limits#
| Plan | Price | Requests / month | Requests / minute |
|---|---|---|---|
| Free | $0 | 500 | 10 |
| Starter | $49/mo | 50,000 | 60 |
| Growth | $199/mo | 500,000 | 300 |
| Scale | $499/mo | 5,000,000 | 1,000 |
Every plan gets the full dataset and every filter; plans differ only in volume. Annual billing is 10× the monthly price — two months free — but isn't self-serve yet; email us to set it up. Above Scale, talk to us too — custom limits and flat-file delivery are available.
Plan limits sync from the portal to the API about every five minutes, so the limits you bought are the limits that are enforced — an upgrade is live within minutes, no redeploy or new key needed.
Limits are per account, not per key. Ten keys on a Growth plan share one 500,000-request monthly pool. Keys exist to separate environments and to be revoked independently, not to multiply quota.
Your current usage is on the dashboard, which shows month-to-date consumption against your quota and a 30-day daily breakdown per key.
What counts as a request#
Every authenticated HTTP request that reaches the API, including ones that return zero results.
/v1/health is free — it's unauthenticated, isn't
rate-limited, and doesn't touch your quota. Use it for liveness probes rather
than a limit=1 job query. (/openapi.json
is likewise keyless and free.)
Rate-limit headers#
Every authenticated response carries three headers:
| Header | Value |
|---|---|
X-RateLimit-Limit | Your per-minute limit. |
X-RateLimit-Remaining | Requests left in the current window. |
X-RateLimit-Reset | Seconds until the window frees up. |
They describe the per-minute limit only — monthly quota consumption lives
on the dashboard. 401 responses carry none of these; the
headers appear once a request has authenticated.
Exceeding the per-minute rate#
{ "error": "rate limit exceeded", "rate_per_min": 300, "retry_after_s": 12 }The window is a sliding 60 seconds, not a fixed calendar minute: the limit counts requests in the last 60 seconds from now. So a burst of 300 at 12:00:00 on a Growth plan clears at 12:01:00, not at the top of the next minute.
retry_after_s — also sent as a standard Retry-After header — is how long
until the window frees. Wait that long and retry; if you're consistently
hitting the limit, pace the client rather than retry-looping.
Exceeding the monthly quota#
{
"error": "monthly quota exceeded",
"monthly_quota": 50000,
"resets": "first of next month (UTC)"
}Also a 429, but with no Retry-After header — retrying won't help. Requests
are rejected until the quota resets at the start of the next calendar month
(UTC), or until you upgrade. Upgrading takes effect within minutes — the new
quota applies to the current month, and there's no proration penalty for
mid-month upgrades. Both 429 shapes are in errors.
Staying under the limit#
The cheapest optimisation by far is asking for fewer, bigger pages. limit
goes to 200 on /v1/jobs and 1,000 on /v1/jobs/feed, and both cost exactly
one request:
50 pages × limit=20 = 50 requests
5 pages × limit=200 = 5 requests ← same data, 10× cheaperThe second cheapest is not fetching descriptions you won't read.
description_format defaults to omitting them; leave it off until you need
them. It doesn't affect your request count, but it's the difference between a
20KB and a 2MB response.
For a client that needs to sustain throughput, pace from the headers rather
than retrying on 429 — every response tells you exactly how much headroom is
left:
// Read the budget off each response. When the window is nearly spent, wait out
// X-RateLimit-Reset. Pacing at the source costs nothing; discovering the limit
// via 429s costs a round-trip each time.
async function paced(url, key) {
const res = await fetch(url, { headers: { Authorization: `Bearer ${key}` } });
const remaining = Number(res.headers.get("X-RateLimit-Remaining"));
const reset = Number(res.headers.get("X-RateLimit-Reset"));
if (remaining <= 1) {
await new Promise((r) => setTimeout(r, reset * 1_000));
}
return res.json();
}
for (const url of urls) {
await paced(url, key);
}The window slides, so headroom refills continuously — no need to hardcode your
plan's rate; X-RateLimit-Limit tells you. If you run multiple workers, they
share one account-wide limit, so either give each worker a safety margin or
have them share the header state.
Syncing efficiently#
Re-crawling /v1/jobs to find what changed is the most expensive possible way
to stay current, and on a Free or Starter plan it will exhaust the monthly quota
before it finishes. Use /v1/jobs/feed — one
request returns everything posted since your last cutoff. See
syncing the feed.