Rate Limits
| Surface | Limit |
|---|---|
| API endpoints — ProducerSync, Contracting, Hierarchies, Identity | 300 requests per minute |
| Token endpoint | 200 requests per minute |
Limits are counted per credential — that is, per client_id, not per user, per server, or per IP address.
The practical consequence is that everything authenticating with the same client_id draws from one shared budget. If you run your sync across several parallel workers, or across several machines, they are not each allowed 300 requests per minute — together they get 300. Size your concurrency accordingly, and pace against the ratelimit-remaining header rather than assuming a per-worker allowance.
Rate limits are subject to change to ensure fair and high quality usage for all customers.
Response Headers
Every response carries your current status — pace against these rather than probing for the edge.
| Header | Meaning |
|---|---|
ratelimit-limit | Maximum requests allowed in the current window |
ratelimit-remaining | Requests still available in the current window |
ratelimit-reset | When the limit resets |
retry-after | Present only on 429 — how long to wait before retrying |
ratelimit-limit: 300
ratelimit-remaining: 299
ratelimit-reset: 17
Handling a 429
HTTP/1.1 429 Too Many Requests
{ "message": "API rate limit exceeded" }
Honor retry-after when present. Otherwise back off exponentially, and add randomness so retries from parallel workers don't synchronize into a thundering herd.
import time
import random
import requests
def request_with_backoff(url, headers, max_retries=5):
"""Make a GET request with exponential backoff on 429 responses."""
for attempt in range(max_retries):
response = requests.get(url, headers=headers)
if response.status_code != 429:
response.raise_for_status()
return response
# Honor the retry-after header if present, otherwise use exponential backoff
retry_after = response.headers.get("retry-after")
if retry_after:
wait = float(retry_after)
else:
wait = (2 ** attempt) + random.uniform(0, 1)
if attempt < max_retries - 1:
time.sleep(wait)
raise Exception(f"Request failed after {max_retries} attempts due to rate limiting")
Staying Well Inside the Limits
- Reuse tokens. They're valid for 60 minutes. Fetching a new token per request is the most common way integrations hit the token endpoint limit — see Token Expiration & Reuse.
- Prefer paged bulk reads over per-record polling.
- Use
updated_sinceas a high-water mark so each sync run fetches only what changed, rather than re-reading whole collections. See Filter, Search & Sort.