Skip to main content
Version: 2.0.0

Recommendation API v2

Recommendation API v2 brings PSYKHE's taste intelligence layer to ranking across key commerce surfaces:

  • Browse: Real-time PLP ranking for each user.
  • Search: Personalized result ordering for search queries.
  • Carousels: Context-aware recommendation widgets such as Not Found, More Like This, and You May Also Like.

Authentication

Authentication details are documented separately:

Personalization

v2 recommendations work for both anonymous (NLI) and logged-in (LI) users.

  • Anonymous (NLI) users are inferred in real time from on-site behavior and request context.
  • Logged-in (LI) users benefit from the same real-time signals plus a deeper, longer-term view of taste and preference via user_id.
  • Ranking is dynamic and updates as user behavior changes.

Filters

Use filters to define which products are eligible before ranking.

  • filters are the base query constraints for Browse/Search (for example collection, product attributes, availability, brand, price ranges).
  • The API uses Mongo-style expressions with operators such as $and, $or, $eq, $in, $gte, and $lte.
  • For equality matching, use the short form directly (for example {"color": "red", "size": "M"}), instead of verbose $eq wrappers.
  • If you provide multiple field conditions at the same level, they are treated as logical AND.
  • Multiple operators in the same field object are also treated as logical AND (for example "price": { "$gte": 100, "$lte": 300 }).

How to use filters in practice:

  1. Send filters on the first page request to define the result set.
  2. Keep the same filters while paginating with the same cursor chain.
  3. Start a new recommendation request when your filters change.

facet_filters are complementary to filters: use filters for base constraints and facet_filters for user-selected facet values (for example color = black, price min/max).

Example filter:

{
"filters": {
"$and": [
{ "plps": { "$in": ["woman/tops"] } },
{ "availability": "in_stock" },
{ "color": "red", "size": "M" },
{ "price": { "$gte": 100, "$lte": 300 } }
]
}
}

Example for array-of-object filtering (item match):

{
"filters": {
"variants": {
"$elemMatch": {
"size": "M",
"color": "black"
}
}
}
}

Use $elemMatch when you need one element in an array of objects to match multiple fields together.

Pagination

We use cursor-based pagination for both Browse and Search, with two pagination behaviors:

  • Personalized mode (infinite scroll): the cursor represents the next fetch in a recommendation stream. The canonical progression is sequential (1 -> 2 -> 3), but clients may still send non-sequential page labels (1 -> 5 -> 10) for UI pagination. In this mode, jumps still return the next unseen result set for the cursor chain and do not skip directly to an absolute page.
  • Ordered mode (for deterministic sorts like price/name/date): pagination behaves like normal ordered paging. Non-sequential jumps (1 -> 4 -> 10) map to true ordered pages.

Important: in personalized mode, a higher page number does not mean better-ranked results than lower page numbers. Page numbers are UI labels; the cursor is the source of truth for continuation.

In personalized mode, moving from page 1 to page 3 does not skip "page 2" results in an absolute sense. The request continues from the same ranked recommendation stream, which is already balanced for relevance, diversity, and constraints.

Both traditional pagination and infinite scrolling are supported. Infinite scrolling can help model feedback loops by using smaller loads (for example, 16 items) and collecting interaction signals sooner, but it is not required.

Pagination rules:

  1. First page requests must omit pagination.
  2. Subsequent pages must send both pagination.page and pagination.cursor.
  3. Keep the same limit across the full pagination chain. Changing limit while paginating is not supported.
  4. Use the previous response recommendation_id as pagination.cursor.
  5. If the cursor is invalid or expired, start a fresh recommendation request without pagination.

Cursor lifetime rules:

  • expires_in is in seconds.
  • expires_in is dynamic per response and often around 300 seconds (~5 minutes).
  • Successful pagination requests extend cursor lifetime.
  • Expired cursors return 404 with a cursor-related error payload.

Initial page example

{
"user": {
"device_id": "device-uuid-1",
"user_id": null
},
"limit": 20,
"sort": ["featured"],
"collection": "woman/tops",
"filters": {
"plps": {
"$in": ["woman/tops"]
}
}
}
{
"recommendation_id": "019c540c-90c5-7a60-9918-6b4dada7c2e9",
"count": 20,
"total": 68,
"expires_in": 300,
"pagination": {
"page": 1,
"has_more": true
}
}

Next page example

{
"user": {
"device_id": "device-uuid-1",
"user_id": null
},
"limit": 20,
"sort": ["featured"],
"collection": "woman/tops",
"filters": {
"plps": {
"$in": ["woman/tops"]
}
},
"pagination": {
"page": 2,
"cursor": "019c540c-90c5-7a60-9918-6b4dada7c2e9"
}
}

Expired cursor example (404)

{
"errors": [
{
"code": "unknown_page",
"message": "Pagination cursor expired; retry without pagination.cursor",
"context": {
"field": "pagination.cursor",
"reason": "expired"
},
"extra": null
}
],
"meta": null,
"data": null
}