Rate Limiting
Limits are enforced per access token and per client IP. Exceeding one returns 429 with a JSON error body and the headers below.
Limits
| Scope | Limit | Applies to |
|---|---|---|
| Per access token | 1,000 requests / hour | every API endpoint |
| Per IP address | 5,000 requests / hour | every API endpoint |
| Public catalog per IP | 60 requests / minute | GET /explore, GET /offerings/{id}, GET /companies/search |
| Publishable key per key | 300 requests / 5 minutes | every public read sent with a pk_… bearer |
| Token endpoint per client | 60 requests / hour | POST /oauth/token for one client_id |
| Token endpoint per IP | 300 requests / 5 minutes | POST /oauth/token |
| Webhook test sends | 10 / minute per endpoint | POST /webhook_endpoints/{id}/test |
| Pending intents | 10 new pending intents / hour per token | POST /intents and the deal close / finalize endpoints |
The token-endpoint limit is the one that surprises people: cache access tokens for their lifetime (24 hours for a client-credentials token, 2 hours for user and installation tokens) rather than minting one per request. For public reads, skip the token entirely and send your publishable key.
The 429 response
Rate-limit headers are sent only on the 429 response, not on successful requests, so you cannot read a remaining quota from a 200.
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1703116800
{
"error": {
"type": "rate_limit_exceeded",
"message": "Rate limit exceeded. Please try again later.",
"details": {
"limit": 1000,
"period": 3600,
"retry_after": 3600
}
}
}
X-RateLimit-Reset is a Unix timestamp. There is no Retry-After header; use details.retry_after (seconds) or X-RateLimit-Reset.
Intent rate limiting uses the same status code but the standard error envelope with type: "rate_limit_exceeded" and a message naming the 10-per-hour ceiling. Replaying an existing pending intent (same idempotency key, or a repeated deal close) never counts against it.
Handling 429
async function withRateLimitRetry(fn, maxRetries = 5) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
if (error.response?.status !== 429) throw error;
const resetAt = Number(error.response.headers["x-ratelimit-reset"]);
const body = error.response.data?.error?.details;
const waitSeconds = body?.retry_after
?? (resetAt ? Math.max(1, resetAt - Math.floor(Date.now() / 1000)) : 2 ** attempt);
await new Promise((r) => setTimeout(r, waitSeconds * 1000));
}
}
throw new Error("Rate limit retries exhausted");
}
Staying under the limits
- Cache access tokens. A client-credentials token lives 24 hours, other tokens 2 hours; mint a new one on expiry or on the first
401, not per request. - Use webhooks for change detection instead of polling list endpoints on a tight loop. See Handling Webhooks.
- Page at the maximum size on endpoints that take
per_page(100 on the investment sync endpoints andGET /explore). Atper_page=100a catalog poll costs one request per 100 offerings; see the polling schedule in the Explore guide and Pagination. - Spread bursts. The hourly windows are fixed, so a burst that exhausts them locks you out for the remainder of the window.