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
| Endpoint | Cost |
|---|---|
GET /api/v4/lookup/ip/{ip} | 1 credit per request. |
POST /api/v4/lookup/ips | ceil(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.
| Endpoint | Sustained rate | Burst |
|---|---|---|
GET /lookup/ip/{ip} and POST /lookup/ips | 100 req/sec | 200 |
GET /lookup/domain/{domain} | 100 req/sec | 200 |
GET /account/me | 10 req/sec | 10 |
GET /feeds/{stream}/stream (connection establishment) | 0.5 req/sec | 20 |
GET /feeds/{stream}/export and …/meta | 2 req/sec | 60 |
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:
| Header | Meaning |
|---|---|
RateLimit-Limit | Total tokens in the bucket. |
RateLimit-Remaining | Tokens left after this request. |
RateLimit-Reset | Seconds 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:
- Start with a 1 second base delay.
- On each successive failure, double the delay.
- Cap the delay at 60 seconds.
- Add ±25% random jitter so concurrent clients don't synchronize.
- 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,5xxrates separately. A creeping429rate 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.creditsperiodically 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.