Skip to content

Products

Mehrdad Pajuhaan edited this page Oct 23, 2026 · 1 revision

Products

window.$storefront.products gives you everything a catalog needs: paginated product and category listings with search, filters and sorting, full product details, customer ratings and the content of subscription products. This page also explains the built-in response cache (optimize, cache, force, cancellation), which every namespace shares.

Method Endpoint (XAPI)
products.list(dir, offset, limit, options) GET /shops/@{shop}/products/all
products.get(product_id, options) GET /shops/@{shop}/products/{product_id}/info
products.rate.submitMyRate(product_id, user_rating) POST /shops/@{shop}/products/{product_id}/set-my-rating
products.subscription.getContents(product_id, offset, limit, options) GET /shops/@{shop}/products/{product_id}/contents

List products and categories

products.list(
  dir: string | null = null,   // category; null = top level, "*" = search everywhere
  offset: number = 0,          // items to skip (pagination)
  limit: number = 10,          // items to return
  options?: XapiProductListTypes.IOptions,
): Promise<XapiProductListTypes.IResponse>

dir is the category to list. The official Storefront passes the category name from its /:category_name-category route, null for the shop root, and "*" to search all products and categories.

const handleSuccessResponse = ({ products, folders, parent, total }) => {
  this.products = products;   // Product.IProduct[]
  this.folders = folders;     // sub-categories
  this.parent = parent;       // current category (with_parent: true)
  this.total = total;         // with_total: true
};

window.$storefront.products
  .optimize(600) // Cache products for 600 seconds
  .list(null, 0, 20, {
    categories_count: 10,
    with_parent: true,
    with_page: true,
    sort: "most_popular",
    available: true,
    search: null,
    filter: JSON.stringify({ only_has_discount: true }),
    with_total: true,
  })
  .cache(handleSuccessResponse)
  .then(handleSuccessResponse)
  .catch((error) => console.error(error));

Note

products.fetchProducts(...) is a deprecated alias of products.list(...). It still works, but use list() in new code.

Options

All options are optional and sent as query parameters (XapiProductListTypes.IOptions):

Option Type Description
sort "most_visited" | "most_popular" | "newest" | "bestselling" | "cheapest" | "most_expensive" | "random" | "related" Sort order.
search string Search text. Use "*" to return all products.
search_type "exact" | "quote" | "tax" | "valuation" | "new" Search mode.
available boolean Only products that are in stock. Default false.
types Product.ProductType[] Product types: PHYSICAL, VIRTUAL, SERVICE, FILE, SUBSCRIPTION.
statuses Product.ProductStatus[] Product statuses.
categories_count number Maximum number of sub-categories (folders) to return.
with_parent boolean Return the current category as parent.
with_page boolean With with_parent, also return the custom page linked to the category.
with_total boolean Return the total count as total.
products_only / categories_only boolean Return only products or only categories (useful for "load more").
dirs string[] Several category IDs at once.
surrounded boolean true: show only the selected categories; false: show items inside them.
filter IFilter Advanced filters, see below. The official components send it as a JSON string (JSON.stringify(filter)).
tags string[] Filter by product tags.
vendor_id string Only products of one marketplace vendor.
bounds [lng1, lat1, lng2, lat2] Only products located inside a map bounding box.
map number | null Only products of a map tag.
only_stared boolean Only products the customer starred (favorites). Note the spelling.
only_in_wishlist boolean Only products in the customer's wishlist.

The official related-products component also passes { product_id, sort: "related" } to list items related to a product.

Advanced filter (IFilter)

Field Type Description
only_is_original boolean Only original products.
only_has_discount boolean Only discounted products.
prices [min, max][] Price ranges.
brands string[] Brands.
selected_variants any[][] Variant values (color, size…).
selected_spec any[][] Specification values.
limited_offer boolean | number Only limited-time offers; a number limits to offers with that many hours left.

Response

Field Type Description
products Product.IProduct[] Products on this page.
folders Category.ICategory[] Categories.
parent Category.ICategory Current category (with with_parent).
total number Total products (with with_total).
after, before string | null Time-span search metadata.
relation-mode string[] | "same-category" How related products were chosen.
tax_profile, valuation, time_filter object | null Extra search objects.

Pagination ("load more")

async function loadMore() {
  const { products } = await window.$storefront.products.list(
    currentCategory,
    this.products.length,  // offset = what you already have
    20,
    { sort: "newest", products_only: true },
  );
  this.products.push(...products);
}

Show product images and links

const imageUrl = window.CDN.GET_SHOP_IMAGE_PATH(product.icon, 300);

To build product links that match the official Storefront, use ShopURLs.GetProductLink(shop, product.id) from Selldone Core, or the route name window.$storefront.routes.PRODUCT_PAGE if you use the same router names.

Get product details

products.get(
  product_id: string | number,
  options?: {
    "augmented-categories"?: number;   // return n categories in the parent folder of the current category
    "augmented-cross-sells"?: boolean; // return cross-selling items (max 3)
    no_article?: boolean;              // skip the article (long description)
  },
): Promise<XapiProductGetTypes.IResponse>
const handleSuccessResponse = ({ product }) => {
  this.product = product;
};

window.$storefront.products
  .optimize(60)
  .get(12345, { "augmented-categories": 3, "augmented-cross-sells": true })
  .cache(handleSuccessResponse) // may resolve instantly from a previously loaded list
  .then(handleSuccessResponse)
  .catch((error) => console.error(error));

Response (XapiProductGetTypes.IResponse):

Field Description
product The product, including article_pack (article, tags, can_edit, and the customer's liked / stared / follow / reported state), categories, and cross-sell data when requested.
campaign_id, link_id Campaign attribution of the visit.
preferred-language Preferred language for the product content.
vendors Marketplace only: vendor offers for this product (id, name, icon, variant_id, price, currency, discount, quantity, lead, …).
categories Deprecated; use product.categories.

The language of the returned article follows the X-Localization request header.

Smart cache: when you chain .cache(), get() searches products already returned by list() calls in the cache and calls your cache callback with { product } immediately, so the product page can render before the network request finishes.

Ratings

A product exposes its rating criteria in product.ratings (id, name, value, count). When product.my_ratings is not null, the signed-in customer has bought the product and can rate it. Submit a map of rating criterion ID → value from 1 to 5:

// product.ratings = [{ id: 7, name: "Quality", ... }, { id: 8, name: "Value", ... }]
window.$storefront.products.rate
  .submitMyRate(product.id, { 7: 5, 8: 4 })
  .then(({ success, my_ratings, ratings, rate_count, rate }) => {
    product.my_ratings = my_ratings;
    product.ratings = ratings;
    product.rate_count = rate_count;
    product.rate = rate;
  });

Subscription content

Subscription products can have members-only content (posts and files). Fetch it page by page:

const showContents = ({ contents, total }) => {
  // contents[i]: id, title, description, rate, rate_count, created_at, files…
};

window.$storefront.products.subscription
  .optimize(60)
  .getContents(product.id, 0, 10, { search: "", sortBy: "id", sortDesc: true })
  .cache(showContents)
  .then(showContents);

This is paywalled content, intended for customers subscribed to the product.

Caching and request control

Every API namespace extends APIAbstract from Selldone Core, so these helpers are available on products, shop, coupon, offer and the others. Caching and cancellation apply to promise-based GET calls such as list(), get(), fetchShop() or fetchOffers(); POST, PUT and DELETE calls are never cached.

Helper Effect
.optimize(seconds = 60) If a stored response for the same request is newer than seconds, return it without calling the server. The setting stays on the namespace object until you call force().
.force() Clear the optimize duration so the next call always hits the server.
.cancellation(true) Cancel an in-flight request to the same URL when a new one starts (great for fast category switching or search-as-you-type). A canceled request rejects with undefined.
promise.cache(callback) Chain it before .then(). If a stored response exists, callback is called immediately with it. It also marks the request so the fresh response is stored. Skipped when optimize already served the stored value.

Important

Responses are stored only for requests chained with .cache(). optimize() alone has nothing to reuse, so always combine the two: .optimize(600).list(...).cache(render).then(render).

window.$storefront.products
  .cancellation()          // drop older requests to the same URL
  .optimize(300)           // reuse data up to 5 minutes old
  .list("shoes", 0, 20, { sort: "cheapest" })
  .cache(render)           // instant UI from cache
  .then(render)            // fresh data
  .catch((error) => {
    if (error === undefined) return; // canceled by a newer request
    console.error(error);
  });

The cache is an in-memory LRU of 100 responses, shared by all namespaces and reset on page reload. CleanCache() from @selldone/core-js/server/APIAbstract clears it.

Next steps

Clone this wiki locally