API keys and security

Rate limits and errors

How many requests each route accepts per minute, what a 429 looks like, the error format and every error code with its HTTP status.

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

RoutesPer callerPer IP address
POST /v1/search/:profileKey600 per key1200
POST /v1/suggest/:suggestKey1200 per key2400
/mcp600 per key1200
POST /v1/auth/loginnone10
POST /v1/auth/registernone5
POST /v1/auth/forgot-passwordnone5
POST /v1/auth/refreshnone120
GET /v1/auth/verify-email, POST /v1/auth/reset-passwordnone30
POST /v1/auth/resend-verification3 per usernone
GET /v1/statusnone600
Every other route, with a secret key1200 per keynone
Every other route, signed in to the cabinet600 per usernone

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" }]
  }
}
  • code is stable. Branch on it in your code.
  • message is for people. It may change, so do not parse it.
  • details lists the fields that failed, each with a path and a message. 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)

CodeMeaning
VALIDATION_FAILEDThe body, query or path does not match the route's schema. details names each field.
INVALID_DOCUMENTSSome documents do not match the index mapping. details names them.
INVALID_FILTERA search filter has the wrong form for its field, or the field is not mapped.
FILTER_NOT_ALLOWEDThe search filters on a field the profile does not offer as a facet or filter field.
SORT_NOT_ALLOWEDThe search sorts by an option the profile does not offer.
PER_PAGE_TOO_LARGEperPage is above the profile's maximum.
PAGE_OUT_OF_RANGEpage times perPage is above 10000.
PROFILE_INVALIDThe profile uses fields the mapping does not allow.
FACETS_INVALIDThe facets use fields the mapping does not allow.
RULE_INVALIDThe rule uses fields or profiles the index does not have.
RULE_ORDER_MISMATCHThe new rule order does not list every rule of the index exactly once.
SUGGEST_CONFIG_INVALIDThe autocomplete settings do not fit the index.
SYNONYM_PARSE_FAILEDA synonyms import line cannot be read.
LANGUAGE_UNAVAILABLEThe search cluster cannot analyze this language.
INVALID_TOKENAn email verification or password reset link is invalid, expired or already used.
WRONG_PASSWORDThe current password is wrong.

Unauthorized (401)

CodeMeaning
UNAUTHENTICATEDNo credentials, credentials the API does not know, or a key that cannot call this route.
API_KEY_REVOKEDThe key was revoked.
API_KEY_EXPIREDThe key's expiry date has passed.
INVALID_CREDENTIALSThe email or the password is wrong.
INVALID_REFRESH_TOKENThe cabinet session expired or was signed out. Sign in again.
REFRESH_TOKEN_REUSEDAn old refresh token was used again, so that device was signed out. Sign in again.

Forbidden (403)

CodeMeaning
FORBIDDENYour role or the key's access does not allow this route.
ORIGIN_NOT_ALLOWEDA public key was used from an origin that is not on its allowed origins, or with no Origin header.
INLINE_PROFILE_FORBIDDENA public key sent an inline search profile. Only secret keys may.
INDEX_SCOPED_KEYA key limited to some indices tried to create an index.
SECRET_KEY_REQUIREDThe MCP server was called with a public key.
EMAIL_NOT_VERIFIEDVerify your email before you create API keys.
IMPERSONATION_FORBIDDENSupport cannot do this while acting as you.
CSRF_HEADER_REQUIREDA 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)

CodeMeaning
EMAIL_TAKENAn account with this email already exists.
ALREADY_MEMBERThe person is already a member of the organization.
LAST_OWNERThe change would leave the organization without an owner.
INDEX_NAME_TAKENThe project already has an index with this name.
PROFILE_NAME_TAKENThe index already has a profile with this name.
SUGGESTION_TAKENThe index already has a custom suggestion with this text.
PROFILE_LIMIT_REACHED, RULE_LIMIT_REACHED, SYNONYM_LIMIT_REACHED, SUGGESTION_LIMIT_REACHEDThe index holds the most profiles, rules, synonym rules or custom suggestions it can.
IDEMPOTENCY_KEY_REUSEDThis Idempotency-Key was already used for another index, session or operation.
VERSION_BUILDINGA new index version is being built. Retry once it finishes.
REINDEX_NOT_RUNNINGThere is no reindex to cancel.
SESSION_OPEN, SESSION_NOT_OPENA full catalog sync is open and blocks this, or the sync is no longer open.

Other statuses

StatusCodeMeaning
413PAYLOAD_TOO_LARGEThe body is larger than the route accepts: 10 MB on document uploads, synonym imports and /mcp, 100 kB everywhere else.
429RATE_LIMITEDToo many requests. Wait for the Retry-After seconds.
500INTERNALSomething failed on our side. Retry later, and contact support if it keeps failing.