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 viauser_id. - Ranking is dynamic and updates as user behavior changes.
Filters
Use filters to define which products are eligible before ranking.
filtersare 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$eqwrappers. - 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:
- Send
filterson the first page request to define the result set. - Keep the same
filterswhile paginating with the same cursor chain. - 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:
- First page requests must omit
pagination. - Subsequent pages must send both
pagination.pageandpagination.cursor. - Keep the same
limitacross the full pagination chain. Changinglimitwhile paginating is not supported. - Use the previous response
recommendation_idaspagination.cursor. - If the cursor is invalid or expired, start a fresh recommendation request without
pagination.
Cursor lifetime rules:
expires_inis in seconds.expires_inis dynamic per response and often around300seconds (~5 minutes).- Successful pagination requests extend cursor lifetime.
- Expired cursors return
404with 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
}