Rate Limits
100 requests per minute per IP address. How to stay within limits and retry correctly.
Rate Limits
There is no per-key quota. All /v1/aeo/* endpoints share a per-IP limit:
| Window | Limit |
|---|---|
| Per minute | 100 requests per IP address (default) |
There is no hourly limit. Because scans run daily, read your data once per scan cycle and cache it instead of re-reading it per prompt.
When you hit a limit
Exceeding the limit returns HTTP 429 Too Many Requests:
{ "statusCode": 429, "message": "Too many requests" }Wait until the current window resets before retrying. The per-minute window resets every 60 seconds.
Which endpoints are expensive vs. cheap
Not all endpoints have the same response cost. Plan your request patterns accordingly:
Cheaper endpoints (fast, low data volume — safe to call frequently):
GET /v1/aeo/meGET /v1/aeo/enginesGET /v1/aeo/countriesGET /v1/aeo/score-methodologyGET /v1/aeo/brandsGET /v1/aeo/topics
Moderate endpoints (aggregated data, typically fast):
GET /v1/aeo/visibility/by-brandGET /v1/aeo/visibility/by-engineGET /v1/aeo/visibility/by-countryGET /v1/aeo/visibility/by-topicGET /v1/aeo/visibility/timelineGET /v1/aeo/prompts
Heavier endpoints (row-level data, larger payloads — use pagination):
GET /v1/aeo/prompts/:id/runsGET /v1/aeo/sources/domainsGET /v1/aeo/sources/detailGET /v1/aeo/citationsGET /v1/aeo/visibility/explainGET /v1/aeo/runs/:id/explain
For heavy endpoints, always use limit and offset pagination rather than pulling everything in one call. Default limit is 20 for most paginated endpoints.
Tip: GET /v1/aeo/prompts/:id/runs accepts include_answer=false, which leaves out answer_text, answer_html and sources. Use it to list runs cheaply, then fetch one full answer with GET /v1/aeo/runs/:scan_result_id.
Retry advice
Use exponential backoff with jitter when retrying after a 429.
A simple pattern:
async function fetchWithRetry(url: string, headers: Record<string, string>, maxRetries = 4) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
const res = await fetch(url, { headers });
if (res.status !== 429) return res;
const baseDelay = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s, 8s
const jitter = Math.random() * 500;
await new Promise((r) => setTimeout(r, baseDelay + jitter));
}
throw new Error('Max retries exceeded');
}For batch data sync jobs, spread requests over several minutes rather than issuing them all at once. Since data only changes with the daily scan, one sync per cycle stays far below the limit.