Search collections
A search collection is an index the platform builds from an app's search.json. Every tenant has its own copy, so a query only ever sees the tenant it is sent for. A storefront reaches it from its server, never from the browser. See The BFF pattern.
The collections
| Collection | Declared by | One document per |
|---|---|---|
products | the products app | product and write key (de, or de-DE in a regional tenant) |
Under the default search_untranslated: hide, a product without its own text in a language other than the tenant's main language has no document in that language. See Languages and locale codes.
Category, manufacturer and page collections are being rolled out (as of 2026-10). Until then, category names reach a product search through the categories and category_labels fields of products.
GET /v1/search/collections lists what a tenant actually has, including collections declared by apps you installed. GET /v1/search/collections/{collection} returns one collection's definition: its fields, its defaults and its document count. An unknown collection answers 404.
What a products document carries
| Field | Facet | Sort | Notes |
|---|---|---|---|
id, sku | sku | Identity | |
name, description | name | Translated, in the document's locale | |
kind, enabled | yes | enabled is always filtered to true by the query endpoint | |
manufacturer | yes | ||
ean | |||
categories | yes | Category codes | |
category_labels | Category names in the document's locale | ||
attrs, attrs_locale | Attribute maps, returned with a hit. Not searchable and not facets. | ||
locale | Which locale the document is for | ||
updated_at | yes | Default order |
Querying a collection
curl -X POST https://api.revenexx.com/v1/search/query \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..." \
-H "X-Revenexx-Locale: de-DE" \
-H "X-Revenexx-Channel: b2b" \
-H "Content-Type: application/json" \
-d '{
"collection": "products",
"view": "search",
"q": "e27 led",
"filters": {
"categories": ["led", "e27"],
"manufacturer": ["LEDVANCE"],
"attrs.lichtstrom": { "min": 400, "max": 1100 }
},
"sort": "relevance",
"page": 1,
"per_page": 24
}'
POST /v1/search/multi_search stays available for clients that build the search parameters themselves.
Headers
| Header | Required | Meaning |
|---|---|---|
X-Revenexx-Tenant | Yes | The tenant. Checked against the API key. |
X-Revenexx-Locale | No | The locale to search in, as a full code (de-DE) or a bare language (de). Send the market locale the page renders in. The query matches documents under the full code and its bare language and returns one hit per product. A bare de is read as de-DE and en as en-US; other bare codes stay as they are. A malformed value is ignored. |
X-Revenexx-Channel | No | A channel code. When the collection has a channels field, only documents visible in that channel come back. |
X-Revenexx-Market | No | A market code. Accepted and passed on, not used for search yet (as of 2026-10). |
Without X-Revenexx-Locale the query searches across every locale.
One collection per language
Collections that keep one index per write key are being rolled out (as of 2026-10). For those, the header picks the index: the exact code, else its language (de-AT → de), else an index of the same language (de → de-DE), else the tenant's default locale. Without the header, or with a malformed one, the default locale answers instead of every locale. The response header X-Revenexx-Locale-Served and the body's locale name the write key that answered, so a page can tell that de-AT was served from de. Words are stemmed for the index's language. Existing collections switch over tenant by tenant; until then the header works as described above and X-Revenexx-Locale-Served is absent.
Body
| Key | Default | Meaning |
|---|---|---|
collection | — | The collection name, for example products. |
view | search | What the query is for: search, category, suggest, brand or listing. Free text matching [a-z0-9_]{1,32} is allowed for your own views. The view selects the relevance settings, so the same q ranks differently in an autocomplete than on a results page. |
q | * | The search text. * matches everything, which is what a category listing sends. |
filters | none | Field filters, combined with AND. See below. |
sort | relevance | relevance, or <field>_asc / <field>_desc for a sortable field. |
page | 1 | 1-based. |
per_page | from the view, else 20 | 1 to 100. |
Relevance settings such as the fields searched, their weights, typo tolerance or facet fields are not accepted in the body. They belong to the view and are maintained per tenant. Unknown keys are ignored.
Filters
| Value | Matches |
|---|---|
["led", "e27"] | Any of the values |
"LEDVANCE", 42 | Exactly this value |
{ "min": 400, "max": 1100 } | A numeric range. Either bound may be left out. |
true / false | A boolean field |
A filter on a field that does not exist or is not filterable, or a value containing a backtick, is dropped and named in ignored_filters. A sort on a field that is not sortable is dropped and named in ignored_sort. Neither fails the query.
Some filters are always applied and cannot be lifted by a caller: the tenant, enabled is true where the collection has that field, the channel when X-Revenexx-Channel is sent, and the locale when X-Revenexx-Locale is sent.
Views
Without tenant-specific settings, a view behaves like this:
| View | Searches | Facets | per_page |
|---|---|---|---|
search, listing, category | name, sku, description, manufacturer | manufacturer, kind, categories, plus any attrs.<code> facet field the collection has | 20 |
suggest | name, sku, as prefixes | none | 6 |
brand | name | manufacturer | 0 |
Fields the collection does not have are skipped, and a view name without built-in settings behaves like search. A tenant can tune each view in Cockpit, and the storefront call does not change.
The response
{
"query_id": "q_8f2c1a",
"found": 154,
"page": 1,
"per_page": 24,
"search_time_ms": 6,
"locale": "de-DE",
"hits": [
{ "document": { "id": "…", "sku": "LV-E27-806", "name": "LED-Lampe E27 806 lm" }, "highlight": { }, "text_match": 578 }
],
"facets": [
{ "field": "manufacturer", "values": [ { "value": "LEDVANCE", "count": 412 } ] },
{ "field": "attrs.lichtstrom", "values": [ { "value": "806", "count": 37 } ], "stats": { "min": 250, "max": 1521 } }
],
"actions": [ { "type": "redirect", "url": "/c/buegeln" } ],
"ignored_filters": [],
"ignored_sort": null
}
| Key | Meaning |
|---|---|
query_id | Identifies this query. Keep it if you report clicks later. |
found | Total matches across all pages. |
locale | The locale that was applied, as a full code, "" when none was sent. |
hits | One per product, in rank order. document is the stored document, highlight the matched snippets. |
facets | Per facet field, the values in the current result and how many hits each has. Build the filter panel from this list rather than from a hard-coded one. Numeric facets carry stats. |
actions | What the merchant attached to this query: redirect (send the buyer to url instead of a results page), banner, or metadata with free data. Handle redirect at least. |
ignored_filters, ignored_sort | What was dropped from the request, so you can see a typo instead of an unfiltered result. |
Errors
| Status | When |
|---|---|
400 | The body is malformed, or X-Revenexx-Tenant is missing. The message says what. |
401 | No valid API key or token. |
403 | The key is not valid for this tenant or lacks the search scope. |
404 | The collection does not exist for this tenant, or search is not enabled for it. |
429 | Too many requests. Retry with backoff. |
502, 503 | The search service did not answer. Retry with backoff. |
See Errors for the error envelope.
Suggestions as you type
POST /v1/search/suggest answers a search box with a few hits from several collections in one call. It takes the same headers as a query.
{ "q": "e27", "collections": ["products", "categories"], "per_page": 5 }
| Key | Default | Meaning |
|---|---|---|
q | — | The text typed so far, 1 to 200 characters. Required. |
collections | products, categories, manufacturers, pages | Up to six. Collections the tenant does not have are skipped. |
per_page | from the suggest view | 1 to 20 hits per collection. |
The response carries query_id, locale, actions and one entry per collection in groups, each with collection, found and hits shaped like a query's hits. Each collection uses its suggest view. A collection that fails drops out of groups and the rest still answer.
Reporting what buyers do
Send POST /v1/search/events from your server when a buyer acts on a result, so the tenant's search analytics can show which queries lead somewhere. It takes the same headers as a query, and the locale comes from X-Revenexx-Locale.
| Key | Required | Meaning |
|---|---|---|
type | Yes | click, add_to_cart or purchase. |
collection | Yes | The collection the hit came from. |
doc_id | Except for purchase | The hit's document id, up to 128 characters. |
query_id | For click | The query_id of the query or suggestion the hit came from. |
position | No | The hit's position in the result, 1 to 1000. |
user_token | No | An anonymous id for the buyer, up to 128 characters. It is stored hashed. Never send an email address. |
q | No | The query text, up to 200 characters. |
It answers 202 with { "accepted": true }. Both endpoints take POST only.
Where to go next
- Search — the storefront composables and blocks on top of these endpoints.
- search.json reference — declaring a collection for your own app.
- Languages and locale codes — which locale a document is stored under.
- Channels — the codes
X-Revenexx-Channeltakes.