API integration

XEYE search API reference: the POST /api/v1/search endpoint, API key authentication, parameters, response, errors and rate limits.

API keys

API keys authenticate calls to the search API. Create them from the API keys page, label them after where they are used, and rotate them by creating a new one and deleting the old.

A key gives access to the public lists of your account, and only those. Private lists are tried from the console's Search page (which uses your session, not a key). To let an integration query a list, mark it public.

Anyone with a key can search all your public lists. Keep keys on your server — never ship them in frontend code or mobile apps.

The search endpoint

Search is a single HTTP POST with a JSON body, authenticated with the X-API-Key header. It only serves public lists.

cURL
curl -X POST https://search.xeye.es/api/v1/search \
  -H "Content-Type: application/json" \
  -H "X-API-Key: YOUR_API_KEY" \
  -d '{
    "list_name": "products",
    "search_term": "wireless headphones",
    "limit": 5
  }'

Request body

The body is strict: an unknown field, a blank term or an out-of-range value returns 422 with per-field details.

  • list_name

    required Name of the (public) list to search, exactly as it appears in the console (max 100 characters).

  • search_term

    required The user query (1–500 characters, whitespace is trimmed). Typos and partial words are fine — matching is fuzzy and semantic.

  • limit

    optional Maximum number of results to return, 1 to 1000. Default 50.

  • session

    optional Free-form identifier of the end user's session (max 255). Groups their searches in the list history and links them to /target.

  • include_score_breakdown

    optional If true, every result also carries text_score and semantic_score next to score. Default false.

  • register_log

    optional Deprecated and ignored: every search is stored in the list history. Still accepted so older integrations keep working.

JavaScript
const response = await fetch('https://search.xeye.es/api/v1/search', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-API-Key': process.env.XEYE_API_KEY,
  },
  body: JSON.stringify({
    list_name: 'products',
    search_term: 'wireless headphones',
    limit: 5,
    session: sessionId,          // optional: groups a user's searches
    include_score_breakdown: true,
  }),
})

const { results } = await response.json()

The response

Results come sorted by score (0–1, higher is better). Each one carries the element text ("item"), its params untouched and, if requested, the text/semantic breakdown. duration_ms is the server-side search time.

200 OK
{
  "success": true,
  "results": [
    {
      "item": "Sony WH-1000XM5 wireless headphones",
      "score": 0.93,
      "params": {
        "sku": "SONY-XM5",
        "url": "/products/sony-wh-1000xm5",
        "price": 348
      },
      "text_score": 0.71,
      "semantic_score": 0.96
    }
  ],
  "total_results": 1,
  "search_term": "wireless headphones",
  "list_name": "products",
  "duration_ms": 42,
  "degraded": false,
  "degradation_reasons": []
}

Degraded responses

Every response carries degraded and degradation_reasons. When degraded is true the search was served with lower quality and the reason says why: no_embeddings (the list has no training in use: text only), model_unavailable (the embedding model could not be loaded), model_mismatch (the vectors do not match the model: retrain) or stale_data (stale data that could not be refreshed). The X-Search-Degraded: true header signals it too.

Recording the pick: /target

Optional. When the end user picks a result, send POST /api/v1/target with list_name, target_term (the chosen element text) and the same session as the search. It only records the choice in the history; nothing is searched. It answers {"success": true}.

cURL
curl -X POST https://search.xeye.es/api/v1/target \
  -H "Content-Type: application/json" \
  -H "X-API-Key: YOUR_API_KEY" \
  -d '{
    "list_name": "products",
    "target_term": "Sony WH-1000XM5 wireless headphones",
    "session": "b1f2-…"
  }'

Usage limits

The searches-per-minute quota is per account: all your API keys (and the console playground) share it. The default is 60 searches/min; an administrator can set a different value for your account. There is also a per-IP limit applied before the key is checked.

Every authenticated response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (seconds until the next window). Once the quota is exhausted you get 429 with Retry-After.

Errors

All errors share the same JSON body: status, error (HTTP phrase), code (a stable identifier for your code) and message (English text that may change); 422 responses add details with the reason per field. Branch on code, not on message.

429
HTTP/1.1 429 Too Many Requests
Retry-After: 23
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 23

{
  "status": 429,
  "error": "Too Many Requests",
  "code": "RATE_LIMITED",
  "message": "Rate limit of 60 requests per minute per account exceeded; retry in 23 seconds"
}
HTTPcodeMeaningWhat to do
400INVALID_HOSTThe Host header is not the service's.Call the public URL of the search service, without proxies rewriting Host.
401API_KEY_MISSING · API_KEY_INVALIDMissing X-API-Key header, or the key does not exist or was revoked.Check the key or create a new one.
403LIST_NOT_PUBLICThe list is private.Make the list public from its settings; private lists are only tried from the console.
404LIST_NOT_FOUNDNo list with that name in your account.Check list_name — it must match the list name exactly.
413REQUEST_TOO_LARGEThe request body exceeds the maximum size (16 KB).Send only the documented fields; search_term allows up to 500 characters.
422VALIDATION_FAILEDInvalid body: unknown field, blank term or out-of-range value.Look at details: it names the field and the reason.
429RATE_LIMITEDYour account's (or your IP's) searches-per-minute quota is exhausted.Wait the Retry-After seconds and retry; watch X-RateLimit-Remaining to stay under the limit.
503SERVICE_NOT_READY · BACKEND_UNAVAILABLEThe search service is still loading its catalogs (SERVICE_NOT_READY) or could not load the list data (BACKEND_UNAVAILABLE).Retry after the Retry-After seconds; if it persists, check the status page (/status).

Recommendations

  • Call the API from your backend and keep the key in an environment variable, not in client code.

  • Use one key per environment or integration, so you can revoke one without breaking the rest.

  • Handle 429 with a small retry/backoff honouring Retry-After — the quota is per account, per minute.

  • Send session and report the pick with /target: the search history will tell you which queries do not find what your users expect.