Rate limits and errors
How rate limits work
Every limit counts requests in a fixed window of 60 seconds. A counter starts with the first request and resets 60 seconds later. Every API server shares the same counters, so spreading requests over servers does not raise a limit.
A route counts against two kinds of limits:
- Per caller: one counter per API key or per signed-in user.
- Per IP address: one counter per client IP, whatever key or token it sends.
A request has to stay under both. The IP limit is counted first, so a client over its IP limit does not use up any key's budget.
Limits per route
| Routes | Per caller | Per IP address |
|---|---|---|
POST /v1/search/:profileKey | 600 per key | 1200 |
POST /v1/suggest/:suggestKey | 1200 per key | 2400 |
/mcp | 600 per key | 1200 |
POST /v1/auth/login | none | 10 |
POST /v1/auth/register | none | 5 |
POST /v1/auth/forgot-password | none | 5 |
POST /v1/auth/refresh | none | 120 |
GET /v1/auth/verify-email, POST /v1/auth/reset-password | none | 30 |
POST /v1/auth/resend-verification | 3 per user | none |
GET /v1/status | none | 600 |
| Every other route, with a secret key | 1200 per key | none |
| Every other route, signed in to the cabinet | 600 per user | none |
All values are requests per 60 seconds.
The last two rows are one budget per caller across all routes. A secret key that uploads documents, polls tasks and reads mappings spends 1200 requests per minute in total, not 1200 on each route. Search, suggest and /mcp have their own counters and do not spend that budget.
Plan limits are separate from rate limits. The monthly searches of your plan are counted for billing and warn you at 80% and 100%, but they never block a search. Plans and billing lists the limits of each plan.
When you hit a limit
A request over a limit answers 429 with a Retry-After header. The header holds the number of seconds until the counter resets.
HTTP/1.1 429 Too Many Requests
Retry-After: 23
Content-Type: application/json
{"error": {"code": "RATE_LIMITED", "message": "Too many requests, retry later", "details": []}}
Wait for the seconds in Retry-After, then send the request again. Do not retry right away. Every rejected request still counts against the IP limit of the current window.
Stay under the limits
- Batch your documents. One upload takes up to 1000 documents. A catalog of 50,000 products is 50 requests, not 50,000.
- Poll tasks slowly. Check a task every second or two, not in a tight loop.
- Wait for a pause before you suggest. Send the suggest request when the shopper stops typing for about 150 ms, not on every key press.
- Count shoppers behind your server. When your storefront server calls search for every shopper, they all share your server's key and IP. 600 searches per minute is 10 per second. Cache popular results on your server if you need more.
The error format
Every error answers with the same JSON body:
{
"error": {
"code": "VALIDATION_FAILED",
"message": "Request validation failed",
"details": [{ "path": "allowedOrigins.0", "message": "expected scheme://host[:port], optionally *.host" }]
}
}
codeis stable. Branch on it in your code.messageis for people. It may change, so do not parse it.detailslists the fields that failed, each with apathand amessage. It is empty for errors that are not about one field.
Error codes
Ids of another organization's projects, indices or keys answer 404, the same as ids that do not exist. So a 403 FORBIDDEN means the resource exists and you may see it, but your role or your key may not do this to it.
Bad request (400)
| Code | Meaning |
|---|---|
VALIDATION_FAILED | The body, query or path does not match the route's schema. details names each field. |
INVALID_DOCUMENTS | Some documents do not match the index mapping. details names them. |
INVALID_FILTER | A search filter has the wrong form for its field, or the field is not mapped. |
FILTER_NOT_ALLOWED | The search filters on a field the profile does not offer as a facet or filter field. |
SORT_NOT_ALLOWED | The search sorts by an option the profile does not offer. |
PER_PAGE_TOO_LARGE | perPage is above the profile's maximum. |
PAGE_OUT_OF_RANGE | page times perPage is above 10000. |
PROFILE_INVALID | The profile uses fields the mapping does not allow. |
FACETS_INVALID | The facets use fields the mapping does not allow. |
RULE_INVALID | The rule uses fields or profiles the index does not have. |
RULE_ORDER_MISMATCH | The new rule order does not list every rule of the index exactly once. |
SUGGEST_CONFIG_INVALID | The autocomplete settings do not fit the index. |
SYNONYM_PARSE_FAILED | A synonyms import line cannot be read. |
LANGUAGE_UNAVAILABLE | The search cluster cannot analyze this language. |
INVALID_TOKEN | An email verification or password reset link is invalid, expired or already used. |
WRONG_PASSWORD | The current password is wrong. |
Unauthorized (401)
| Code | Meaning |
|---|---|
UNAUTHENTICATED | No credentials, credentials the API does not know, or a key that cannot call this route. |
API_KEY_REVOKED | The key was revoked. |
API_KEY_EXPIRED | The key's expiry date has passed. |
INVALID_CREDENTIALS | The email or the password is wrong. |
INVALID_REFRESH_TOKEN | The cabinet session expired or was signed out. Sign in again. |
REFRESH_TOKEN_REUSED | An old refresh token was used again, so that device was signed out. Sign in again. |
Forbidden (403)
| Code | Meaning |
|---|---|
FORBIDDEN | Your role or the key's access does not allow this route. |
ORIGIN_NOT_ALLOWED | A public key was used from an origin that is not on its allowed origins, or with no Origin header. |
INLINE_PROFILE_FORBIDDEN | A public key sent an inline search profile. Only secret keys may. |
INDEX_SCOPED_KEY | A key limited to some indices tried to create an index. |
SECRET_KEY_REQUIRED | The MCP server was called with a public key. |
EMAIL_NOT_VERIFIED | Verify your email before you create API keys. |
IMPERSONATION_FORBIDDEN | Support cannot do this while acting as you. |
CSRF_HEADER_REQUIRED | A cabinet session request is missing its X-Requested-With header. |
Not found (404)
The resource does not exist or belongs to another organization. The code names what was not found: ORG_NOT_FOUND, PROJECT_NOT_FOUND, INDEX_NOT_FOUND, DOCUMENT_NOT_FOUND, TASK_NOT_FOUND, PROFILE_NOT_FOUND, SUGGEST_NOT_FOUND, SUGGESTION_NOT_FOUND, SYNONYM_NOT_FOUND, RULE_NOT_FOUND, API_KEY_NOT_FOUND, MEMBER_NOT_FOUND, USER_NOT_FOUND, SESSION_NOT_FOUND. A route that does not exist answers NOT_FOUND.
Conflict (409)
| Code | Meaning |
|---|---|
EMAIL_TAKEN | An account with this email already exists. |
ALREADY_MEMBER | The person is already a member of the organization. |
LAST_OWNER | The change would leave the organization without an owner. |
INDEX_NAME_TAKEN | The project already has an index with this name. |
PROFILE_NAME_TAKEN | The index already has a profile with this name. |
SUGGESTION_TAKEN | The index already has a custom suggestion with this text. |
PROFILE_LIMIT_REACHED, RULE_LIMIT_REACHED, SYNONYM_LIMIT_REACHED, SUGGESTION_LIMIT_REACHED | The index holds the most profiles, rules, synonym rules or custom suggestions it can. |
IDEMPOTENCY_KEY_REUSED | This Idempotency-Key was already used for another index, session or operation. |
VERSION_BUILDING | A new index version is being built. Retry once it finishes. |
REINDEX_NOT_RUNNING | There is no reindex to cancel. |
SESSION_OPEN, SESSION_NOT_OPEN | A full catalog sync is open and blocks this, or the sync is no longer open. |
Other statuses
| Status | Code | Meaning |
|---|---|---|
| 413 | PAYLOAD_TOO_LARGE | The body is larger than the route accepts: 10 MB on document uploads, synonym imports and /mcp, 100 kB everywhere else. |
| 429 | RATE_LIMITED | Too many requests. Wait for the Retry-After seconds. |
| 500 | INTERNAL | Something failed on our side. Retry later, and contact support if it keeps failing. |