Rate Limits

Synthient enforces two distinct limits: lookup credits for the synchronous IP and domain APIs, and per-endpoint request-rate limits that apply to every HTTP call. Both are per-team.

Lookup credits

Every call to the IP API or domain lookup consumes credits from your plan's monthly allotment. Inspect the remaining balance and reset cadence on /account/me:

{
  "lookup_quota": {
    "credits": 982341,
    "resets_in": 1893456
  }
}

credits is the integer balance remaining; resets_in is the seconds until the balance refills.

How requests are charged

EndpointCost
GET /api/v4/lookup/ip/{ip}1 credit per request.
POST /api/v4/lookup/ipsceil(n × 0.9) credits, where n is the number of unique, valid IPs in the body (10% batch discount).
GET /api/v4/lookup/domain/{domain}1 credit per request.
Feed exports (/feeds/{stream}/export*)Free counted against your feed subscription, not lookup credits.
Feed streams (/feeds/{stream}/stream)Free counted against your concurrent-stream allowance.

Worked example

A batch request with 100 IPs costs ceil(100 × 0.9) = 90 credits versus 100 credits as individual lookups a 10-credit saving. A batch with 1,000 IPs costs 900 credits.

Duplicate and invalid entries are stripped before charging, so they don't count toward your bill. Track your remaining balance via /account/me.

Out of credits

When credits reaches zero, lookup endpoints return 402 Payment Required. Either add credits, upgrade your plan, or wait for resets_in to elapse. Streaming and export endpoints are unaffected they don't consume lookup credits.

Per-endpoint rate limits

Every endpoint is rate-limited per team (the API key's owning organization), not per IP, so multiple keys for the same team share the same buckets. The defaults below are sized for a healthy production caller; reach out to support before designing around higher throughput.

EndpointSustained rateBurst
GET /lookup/ip/{ip} and POST /lookup/ips100 req/sec200
GET /lookup/domain/{domain}100 req/sec200
GET /account/me10 req/sec10
GET /feeds/{stream}/stream (connection establishment)0.5 req/sec20
GET /feeds/{stream}/export and …/meta2 req/sec60
GET /feeds/{stream}/export/{snapshot_id} (downloads)0.1 req/sec (≈360/hour)120

Stream and export buckets are shared across all eight feeds for a team fanning out to every stream doesn't multiply the budget.

Excess requests get 429 Too Many Requests. Long-lived stream connections are not re-charged once established; only the initial GET counts against the connection-rate bucket.

Rate-limit response headers

Every rate-limited HTTP response carries the standard IETF RateLimit headers:

HeaderMeaning
RateLimit-LimitTotal tokens in the bucket.
RateLimit-RemainingTokens left after this request.
RateLimit-ResetSeconds until the bucket fully refills.

Read these headers to pace your calls instead of polling for 429s.

Retry guidance

For any 429, 500, or 503 response, retry with exponential backoff and jitter. Honour the Retry-After header when present.

The recipe:

  1. Start with a 1 second base delay.
  2. On each successive failure, double the delay.
  3. Cap the delay at 60 seconds.
  4. Add ±25% random jitter so concurrent clients don't synchronize.
  5. Give up after a reasonable number of attempts (typically 5–8) and surface the failure to the caller.

Retry with backoff

import os, random, time, requests

URL = "https://api.synthient.com/api/v4/lookup/ip/8.8.8.8"
HEADERS = {"x-api-key": os.environ["SYNTHIENT_API_KEY"]}

def lookup_with_retry(max_attempts: int = 6):
    for attempt in range(max_attempts):
        r = requests.get(URL, headers=HEADERS, timeout=10)
        if r.status_code < 400:
            return r.json()
        if r.status_code in (429, 500, 502, 503, 504):
            retry_after = int(r.headers.get("Retry-After", 0))
            base = max(retry_after, min(60, 2 ** attempt))
            time.sleep(base * random.uniform(0.75, 1.25))
            continue
        r.raise_for_status()
    raise RuntimeError("max retries exceeded")

Streaming reconnects

Long-lived streams are closed cleanly by the server every ~30 minutes. Treat that as the normal case and reconnect immediately. If the reconnect itself fails, fall through to the same exponential backoff used for one-shot requests so a transient outage doesn't hammer the service.

See the Firehose page for full NDJSON consumer examples in Python, TypeScript, and Go.

What to instrument

A small amount of client-side telemetry pays for itself the first time you hit a limit:

  • Per-status counters track 2xx, 4xx, 5xx rates separately. A creeping 429 rate is the earliest sign you should batch or cache.
  • Backoff distribution log the actual sleep durations. If you're frequently hitting the 60s cap, raise the cap or move work to off-peak.
  • Quota gauge pull lookup_quota.credits periodically and alert on burn rate, not just zero.

Next steps

  • Errors full status code reference and response shapes.
  • Authentication scope and key management.
  • Account programmatic quota inspection.