Skip to content

Rate limits

Limits apply per key and per store. Every key has an allowance, and every store has a larger one, shared by all of its keys. The allowance refills continuously, so a short burst is fine and a steady stream is what counts. A request is charged to the key’s allowance first; a request the key’s allowance refuses never touches the store’s.

The numbers

PlanBurst per keySustained per keyStore-wide
Plus72 units12 units per second3 x the key’s numbers
Enterprise360 units60 units per second6 x the key’s numbers

GET /v1/store returns the numbers in force for your store under limits; a store may carry its own override. The dashboard shows the same numbers, and what is in use right now across all keys, under Developer > Limits.

Cost of a request

Every operation belongs to a class, and the class sets what it costs: read 1 (every GET except analytics), write 2 (every POST, PATCH and DELETE except a search), analytics 3 (every analytics view) and search 6 (a people search). The reference states the class of each operation (x-rate-class).

An analytics call is charged 1 when the key is accepted and 2 more once an answer body is served, so a 304, a 404 or a 422 on an analytics route costs 1 and a 200 costs 3.

With a Plus key you can burst 72 reads, 36 writes, 24 analytics bodies or 12 searches, then sustain 12 reads, 6 writes, 4 analytics bodies or 2 searches per second.

Headers

Every limited response carries two headers:

RateLimit: limit=72, remaining=42, reset=3
RateLimit-Policy: 72;w=6
  • limit is the size of the allowance the headers describe.
  • remaining is what it can still take now.
  • reset is the number of seconds until it is completely refilled.
  • RateLimit-Policy names the same size and w, the seconds a full allowance takes to refill.

On an admitted request the headers describe the key’s allowance. On a 429 they describe the allowance that refused the request; when that is the store’s, limit is the store’s larger number. The headers are absent on a 401, on a 403 answered before the limit is checked (plan_required, insufficient_scope), and on a read admitted while the limiter could not be reached.

When you go over

Past the limit the answer is 429 rate_limited with a Retry-After header (seconds). The same number is in the body as retry_after:

{
"type": "https://api.octaneai.com/errors/rate_limited",
"title": "rate_limited",
"detail": "Too many requests",
"status": 429,
"request_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"retry_after": 2
}

Wait for Retry-After seconds, then retry. A store-level refusal has already spent the key’s own allowance for that request; there is no refund. Read remaining on every answer and slow down before you get there:

import os
import time
import httpx
HEADERS = {"Authorization": f"Bearer {os.environ['OCTANE_API_KEY']}"}
def get(url, **params):
while True:
r = httpx.get(url, params=params, headers=HEADERS, timeout=30)
if r.status_code != 429:
r.raise_for_status()
return r
time.sleep(int(r.headers.get("Retry-After", "1")))
r = get("https://api.octaneai.com/v1/quizzes", status="published")
print(r.headers["RateLimit"], len(r.json()["data"])) # limit=72, remaining=71, reset=1 2

Searches

A people search costs 6 units and has two more limits. Each statement behind it has 10 seconds; a filter that matches a large share of your people runs past that and answers 422 validation_error whose export field carries the export request that builds the same people as a file (see People). A store runs at most 15 live searches at a time; a search that finds no free slot within a second answers 429 rate_limited with Retry-After: 1. Exports count against neither.

Webhooks

Outgoing webhooks have their own numbers, also returned by GET /v1/store: webhook_endpoints (5 on Plus, 25 on Enterprise) and webhook_sends_per_second per endpoint (50 on Plus, 200 on Enterprise). Creating an endpoint past the quota answers 409 endpoint_limit.