Skip to main content

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​

ScopeLimitApplies to
Per access token1,000 requests / hourevery API endpoint
Per IP address5,000 requests / hourevery API endpoint
Public catalog per IP60 requests / minuteGET /explore, GET /offerings/{id}, GET /companies/search
Publishable key per key300 requests / 5 minutesevery public read sent with a pk_… bearer
Token endpoint per client60 requests / hourPOST /oauth/token for one client_id
Token endpoint per IP300 requests / 5 minutesPOST /oauth/token
Webhook test sends10 / minute per endpointPOST /webhook_endpoints/{id}/test
Pending intents10 new pending intents / hour per tokenPOST /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 and GET /explore). At per_page=100 a 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.
Was this page helpful?