Skip to content

Rate Limits & Caching

FirmaDB enforces rate limits per API key, sliding-window, measured per minute. Every response — successful or not — carries the headers you need to pace yourself. This page is the reference an agent should ground on before issuing concurrent requests.

Rate limits by tier

Tier Lookup/min Search/min Cross-country search/min Batch rows/request Concurrent batch
Free 60 10 3 100 1
Solo 120 30 10 100 2
Team 300 60 20 100 3
Scale 600 120 40 100 5

Cross-country search costs 10× rate-limit units

A search call without a country parameter scans all 19 partitions and consumes 10 units against your search budget instead of 1. Always pass country when you know it. Cross-country is rate-limited separately and is much smaller — the table above lists both buckets.

Headers on every response

Rate-limit state is communicated via lowercase HTTP headers on every response, not only on 429s.

ratelimit-limit: 120
ratelimit-remaining: 117
ratelimit-reset: 42
ratelimit-policy: "lookup-minute";q=120;w=60
x-ratelimit-resource: companies-read
x-firmadb-cost-units: 1
x-firmadb-credits-remaining: 8766
x-request-id: req_01HZAB7MJX9V8GG6P3K5R2QC4P
Header Meaning
ratelimit-limit Per-minute cap for the resource you just hit
ratelimit-remaining Requests left in the current window
ratelimit-reset Seconds until the window resets
ratelimit-policy Window definition in IETF format
x-ratelimit-resource Which bucket was consumed (see below)
x-firmadb-cost-units Units charged by this request (1 normally, 10 for cross-country search)
x-firmadb-credits-remaining Billable results left in the current month
x-request-id ULID for support tickets and tracing

Resource buckets

The bucket charged is reported in x-ratelimit-resource:

Resource Triggered by
companies-read Single lookup
companies-search Country-scoped search
companies-search-global Cross-country search (10 units)
companies-enrich Batch lookup
coverage-read Countries endpoints
account-read Usage endpoint

Reading the headers

curl -i "https://api.firmadb.com/v1/companies/FR/552120222" \
  -H "Authorization: Bearer $FIRMADB_API_KEY" \
  | grep -i '^ratelimit\|^x-ratelimit\|^x-firmadb'
import httpx, os

resp = httpx.get(
    "https://api.firmadb.com/v1/companies/FR/552120222",
    headers={"Authorization": f"Bearer {os.environ['FIRMADB_API_KEY']}"},
)
print(f"{resp.headers['ratelimit-remaining']} of {resp.headers['ratelimit-limit']} left")
print(f"window resets in {resp.headers['ratelimit-reset']}s")
const resp = await fetch(
  "https://api.firmadb.com/v1/companies/FR/552120222",
  { headers: { Authorization: `Bearer ${process.env.FIRMADB_API_KEY}` } }
);
console.log(`${resp.headers.get("ratelimit-remaining")} left`);

Handling 429

There are two distinct 429 conditions — you handle them differently.

429 rate_limit_exceeded

You burst too fast. The window will reset shortly.

{
  "type": "https://errors.firmadb.com/rate-limit-exceeded",
  "status": 429,
  "code": "rate_limit_exceeded",
  "retryable": true,
  "retry_after_seconds": 28,
  "limit": {
    "resource": "companies-read",
    "limit": 120,
    "remaining": 0,
    "reset_seconds": 28
  }
}

Always honour Retry-After (seconds). Do not retry immediately — you'll just stack more 429s.

def call_with_backoff(client, request_kwargs):
    while True:
        r = client.request(**request_kwargs)
        if r.status_code != 429:
            return r
        body = r.json()
        if body["code"] != "rate_limit_exceeded":
            return r  # quota_exhausted needs different handling
        time.sleep(int(r.headers.get("Retry-After", 30)))

429 quota_exhausted

You've used your monthly billable results. The window will not reset for hours/days. Sleeping won't help.

{
  "type": "https://errors.firmadb.com/quota-exhausted",
  "status": 429,
  "code": "quota_exhausted",
  "retryable": false,
  "limit": {
    "bucket": "monthly_results",
    "scope": "account",
    "limit": 1000,
    "remaining": 0,
    "reset_iso": "2026-06-01T00:00:00Z"
  },
  "price": {
    "next_tier": "solo",
    "monthly_fee_eur": 29,
    "included_results": 10000,
    "upgrade_url": "https://firmadb.com/upgrade"
  }
}

Distinguish the two via code, not the HTTP status alone. rate_limit_exceeded is transient; quota_exhausted requires either waiting until next billing period or upgrading.

Caching rules

Once you've fetched a company record, you may cache it locally for the duration of your tier's cache window:

Tier Cache window
Free 24 hours
Solo 7 days
Team 30 days
Scale 90 days

After the window expires, re-fetch. The data_freshness.last_confirmed_at field tells you when FirmaDB last reconciled the record with the source registry — independent of your local cache window.

ETag + If-None-Match (free conditional requests)

Every company response carries an ETag. Send it back with If-None-Match and you'll get a 304 Not Modified if the record hasn't changed — at zero billable cost.

curl -i "https://api.firmadb.com/v1/companies/FR/552120222" \
  -H "Authorization: Bearer $FIRMADB_API_KEY" \
  -H 'If-None-Match: "abc123"'
resp = httpx.get(
    "https://api.firmadb.com/v1/companies/FR/552120222",
    headers={
        "Authorization": f"Bearer {api_key}",
        "If-None-Match": cached_etag,
    },
)
if resp.status_code == 304:
    company = local_cache[key]
else:
    company = resp.json()
    local_cache[key] = company
const resp = await fetch(url, {
  headers: {
    Authorization: `Bearer ${apiKey}`,
    "If-None-Match": cachedEtag,
  },
});
if (resp.status === 304) return localCache.get(key);
const company = await resp.json();
localCache.set(key, company);

Idempotency

The batch endpoint requires an idempotency_key (any UUID). Rules:

  • Same key + same body within 24h → cached response, no re-billing.
  • Same key + different body409 idempotency_conflict with differing_fields[].
  • Concurrent requests with the same key → serialized; one runs, the others wait for its result.
  • After 24h → key expires and may be reused.

Generate a fresh UUID per logical batch operation. Reusing the same UUID across retries of the same logical batch is the entire point — that's what makes network errors safe.

Best-practice checklist

  • Always pass country on search (10× cost without it).
  • Read ratelimit-remaining and slow down preemptively, don't wait for 429.
  • On 429, branch on code, then either sleep Retry-After or upgrade.
  • Use ETag + If-None-Match whenever you re-fetch within your cache window.
  • For batches, generate the idempotency_key once per logical job and reuse it on every retry attempt.
  • Keep concurrent batch requests under your tier's Concurrent batch cap.

What's next