Behio Storefront SDK
Catalog & Products

Products & Search

Browse, filter, and search products

List Products

const result = await client.catalog.getProducts({
  page: 1,
  limit: 24,
  sort: 'price_asc',
  inStock: true,
});
// { items: ProductListItem[], total, page, totalPages }

Filter Options

ParamTypeDescription
pagenumberPage number (1-based)
limitnumberItems per page (default 24)
categorystringFilter by category slug
labelstringFilter by label slug
priceMinnumberMinimum price
priceMaxnumberMaximum price
sortstringprice_asc, price_desc, name_asc, name_desc, newest, featured, bestselling, rating
inStockbooleanOnly in-stock items
ratingMinnumberMinimum aggregate rating (e.g. 4 for "4 and up")
searchstringFull-text search
idsstring[]Filter by product IDs
slugsstring[]Filter by slugs
labelsstring[]Multiple labels (OR)
categoriesstring[]Multiple categories (OR)
hasDiscountbooleanOnly discounted products
isFeaturedbooleanOnly featured products
parametersobjectParameter filters, keyed by parameter slug. A value may be an array = multi-select (OR within the key): { "material": "bavlna", "barva": ["cerna", "bila"], "hmotnost_min": 100 }. Only parameters the merchant marked filterable are accepted
facetsobjectSlug-based facet selection for SEO URLs: parameter slug → value slugs, e.g. { "barva": ["cerna"] }. Resolved server-side to the underlying values

priceMin/priceMax filter the customer's actual selling price in the requested currency. bestselling sorts by units sold; rating by aggregate review score.

Product Detail

const product = await client.catalog.getProduct('wireless-headphones', {
  locale: 'cs',
  currency: 'CZK',
});

Always build the detail URL from the slug the list returned. That value is the product's per-locale slug, the single source of truth for product URLs; it falls back to the product id only when the merchant has not set a slug in any language. getProduct accepts the per-locale slug and the product id alike, so a link built from product.slug always resolves and older id-based links keep working.

Variants

When a product has variants (colour, size, …), product.variants lists them. Each variant is its own sellable product, so variant.id is an eshop product id with its own price and stock. Render a picker and pass the chosen variant.id straight to cart.addItem({productId}):

product.variants.map((v) => ({
  id: v.id,          // eshop product id, use as productId when adding to cart
  name: v.name,
  sku: v.sku,
  price: v.price,    // variant's own price (null for live-quote products)
  inStock: v.inStock,
  attributes: v.attributes, // e.g. [{name: 'Color', value: 'Blue'}]
}));

Only purchasable variants (a published, enabled eshop product) appear in the list.

A variant carries its own content (SDK 1.1.0)

A variant is a full product row in Behio, so the merchant can give it its own localized name, both descriptions, gallery and SEO. Switching the picker should therefore switch the page, not just the price.

import {
  variantFromQuery,
  resolveVariantContent,
  variantHref,
} from '@behio/storefront-sdk';

// 1. Resolve the selection ON THE SERVER, from ?variant=<variantSlug>.
const variant = variantFromQuery(product, searchParams.variant);

// 2. Merge it onto the parent. Empty variant fields INHERIT the parent, so a
//    shop that only differentiated the name never renders an empty block.
const content = resolveVariantContent(product, variant);
content.name;             // variant name or the product name
content.shortDescription; // HTML, render through your RichText component
content.longDescription;
content.images;           // variant gallery, or the parent's when it has none

// 3. Link through the PARENT, because a variant has no page of its own.
variantHref(product.slug, variant); // /product/tricko?variant=tricko-modre

Three rules that are easy to get wrong:

  • Resolve on the server. Selecting only in a client component renders the parent's text first and swaps it after hydration: a visible flash, and the wrong text in view-source for crawlers and social previews.
  • variantSlug is not a URL. The catalog answers 404 for a variant slug and every listing excludes variants, because a variant is bought from the parent's page. Always build /product/{product.slug}?variant={variant.variantSlug}.
  • Empty means inherit. shortDescription: null on a variant is not "no description", it is "the parent's description".

Multi-axis pickers and comparison tables have helpers too:

import {
  findVariantByAttributes,
  availableAxisValues,
  buildVariantComparison,
} from '@behio/storefront-sdk';

findVariantByAttributes(product, {Barva: 'modrá', Velikost: 'M'}); // or null
availableAxisValues(product, 'Velikost', {Barva: 'modrá'});        // ['S', 'M']
buildVariantComparison(product, labels, formatFn); // only rows that DIFFER

findVariantByAttributes returns null while the selection is incomplete or names a combination the merchant never published, and that is the moment to disable the buy button, not to add "some" variant to the cart.

Parents sold only through their variants (variantsOnly + priceFrom)

Very often the parent is not a real sellable thing: the shop sells the red shirt and the yellow shirt, and "Tričko" on its own means nothing. The merchant can mark such a parent in the Behio admin, and the catalog then reports it on both the list item and the detail:

const p = await client.catalog.getProduct('tricko', {locale: 'cs'});

p.variantsOnly;
// true = this product CANNOT be bought under its own id. It keeps its card,
// its PDP and its search presence, it is simply not a sellable unit. The
// server enforces it: cart.addItem({productId: p.id}) and the checkout both
// answer 400. `isPurchasable` is already false, so a template that only
// honours that flag disables the right button for free.

p.priceFrom;
// ProductPrice | null. Holds the CHEAPEST variant price, in the requested currency.
// Render "od 990 Kč". `compareAtPrice` carries that variant's strike-through
// price when it has one.

What a template is expected to render for such a product:

  • headline price: od {priceFrom}, never a bare number. price still carries the parent's own amount, so printing it unlabelled would quote 990 while the shopper's chosen variant charges 1990.
  • no add-to-cart until a variant is chosen. Once one is, switch the headline to variant.price and add variant.id (not product.id) to the cart.
  • listing cards: od {priceFrom} too. priceFrom is identical on the card and on the PDP by construction.

What "from" means when a variant is out of stock

priceFrom is the cheapest of the variants the PDP lists, regardless of stock. A sold-out cheapest variant still sets the "od" number.

That is deliberate. Filtering on availability would make the advertised price jump up and down every time one size sells out and comes back, which breaks the shopper's price memory and desynchronises XML feeds (Google and Heureka penalise a landing page whose price differs from the feed). A variant the merchant really wants gone is unpublished in the admin, and then it disappears from variants[] and from priceFrom together.

priceFrom is populated for every variant parent, not only variantsOnly ones, so a template can offer an "od" line on ordinary variant products too. It is null for products without variants and when prices are gated behind login in B2B mode.

Availability comes from the variants, not from the parent

Unlike priceFrom, inStock and availability on a variantsOnly parent are not stock-blind. They are derived from the parent's purchasable variants:

  • at least one variant with stock: inStock: true, availability.code: 'in-stock'
  • every variant sold out: inStock: false, availability.code: 'sold-out'

A variantsOnly parent is never stocked itself, since the merchant keeps the quantities on the variants, so its own warehouse number says nothing about whether anything is buyable. Reading it produced badges that were right or wrong by luck: a real shop had a parent sitting at 0 with three variants of 5 pcs each, and the card advertised "Vyprodáno" on a product available in three editions.

Only variants a shopper can actually buy count, i.e. the same published and enabled set that variants[] and priceFrom use. An unpublished variant sitting in the warehouse does not make the parent look available.

Ordinary (non-variantsOnly) products are untouched: they keep deriving availability from their own stock, and an availability state assigned by the merchant in the admin still wins over any derivation.

The inStock=true filter, the availability facet counts in /catalog/facets and the shop-wide "hide sold-out products" behaviour all use the same derivation, so a parent whose card says "Skladem" is also returned by the filter and counted as in stock. That was not always true: the filter used to read the parent's own warehouse number, so a parent with stock in its variants was dropped from inStock=true results, and in a shop configured to hide sold-out products it disappeared from the catalog entirely while its own PDP kept advertising it as available.

Backwards compatibility

variantsOnly and priceFrom are additive, and price is untouched: a variantsOnly parent still returns its own amount exactly as before. An older template that knows nothing about this feature renders the parent's own price and, if it honours isPurchasable, a disabled buy button. That is acceptable but not correct: the number is unlabelled and therefore misleading. Updating the template to show "od" is the point of the feature.

inStock and availability are the one exception: on a variantsOnly parent they now answer from the variants (see above). No template change is needed, because every template already renders those two fields; they simply stopped reporting a number nobody maintains. Templates that cached or mirrored the old value should drop that copy.

Variant display config (swatches)

product.variantAxes carries the merchant's per-axis display configuration from the admin (Variant Display), so the picker renders exactly the way the shop configured it: color swatches, image swatches, a dropdown or buttons.

product.variantAxes;
// [
//   {
//     name: 'Barva',                 // matches variant.attributes[].name
//     displayType: 'SWATCH_COLOR',   // 'DROPDOWN' | 'BUTTON' | 'SWATCH_COLOR' | 'SWATCH_IMAGE'
//     order: 0,
//     values: [
//       { value: 'červená', swatchColor: '#e02020', swatchImage: null },
//       { value: 'modrá',   swatchColor: '#2040e0', swatchImage: null },
//     ],
//   },
// ]
  • Join axes to variants by name + value against variant.attributes.
  • An axis the merchant never configured comes back as BUTTON (render plain buttons, the historical default), so nothing changes until the shop opts in.
  • values only contains values present on purchasable variants, in the admin-defined order. Empty array = the product has no variants.

Colour split: sibling cards (catalogSiblings)

Some shops sell one dress as one card per colour, the way almondmuse.com does: /products/serena-blue is its own product with its own photos, address and SEO, and the colour dots are links to sibling products, not an in-page switch.

Turn it on in the admin (E-shop settings, tab Katalog: mode SPLIT_BY_AXIS plus the axis to split by, overridable per product). The catalog then returns one card per value of that axis and the PDP carries catalogSiblings:

product.catalogSiblings;
// [
//   { productId: '01K…', slug: 'serena-blue', name: 'Serena, modrá',
//     axisValue: 'Modrá', swatchColor: '#2040e0', swatchImage: null, isCurrent: true },
//   { productId: '01K…', slug: 'serena-pink', name: 'Serena, růžová',
//     axisValue: 'Růžová', swatchColor: '#e050a0', swatchImage: null, isCurrent: false },
// ]
  • Render them as real <a href> links. They must work without JavaScript, or search engines index one colour and ignore the rest.
  • Preload the neighbours. Each sibling carries imageUrl, the cover photo of that colour. Render it in the dot and prefetch it (<link rel="prefetch" as="image">), and mark the links prefetch. Without both, switching a colour waits for a server round trip and then for the photo, which reads as the page flashing.
  • Each card carries its own photos. The merchant tags product photos with the colour they show, so image on the card and images on its detail are that colour's shots, not the parent's cover. Untagged photos stay shared and show up for every colour.
  • Price and stock are the colour's, not one size's. A card reports variantsOnly: true with priceFrom (cheapest size of that colour) and its stock summed across the colour, and its variants are only that colour's sizes. The split axis is absent from variantAxes, because the colour is already chosen by which card the visitor is standing on.
  • The currently open card is included and flagged isCurrent, so you can draw the whole row without recomputing it.
  • The remaining axes (size) stay inside the card as the usual variant picker.
  • Empty array on every shop that does not use the split, which is the default.

Which page search engines get (seo.canonicalSlug, seo.noIndex)

The split turns one product into as many public pages as it has colours, so the merchant chooses in the admin (E-shop settings, tab Katalog, Stránky pro vyhledávače) which of them represents the product:

ChoiceColour cardParent productSitemap
A page for every colour (default)canonical to itselfnoindex, followthe cards
One product pagecanonical to the parentcanonical to itselfthe parent

Both fields arrive on product.seo, so a template never has to know the rule:

alternates: {
  canonical: product.seo?.canonicalSlug
    ? `${SITE_URL}/product/${product.seo.canonicalSlug}`
    : `${SITE_URL}/product/${product.slug}`,
},
...(product.seo?.noIndex ? {robots: {index: false, follow: true}} : {}),

A colour card with no description of its own inherits the parent's short and long description (and SEO description) at read time, so the page is never empty. The title and H1 stay the card's own, because the name already carries the colour.

Sitemap (getSitemap)

Build the sitemap from this call, not from getProducts: which page belongs in the index is a catalog rule, and a listing does not know it.

const {data} = await behio.catalog.getSitemap();
// { locale: 'cs',
//   products:   [{slug: 'serena-modra', updatedAt: 1754400000000}, …],
//   categories: [{slug: 'saty', updatedAt: …}, …],
//   pages:      [{slug: 'obchodni-podminky', updatedAt: …}, …] }

Products already honour the indexing choice above, categories are the active ones and pages are the published content pages. updatedAt is epoch ms for lastmod. Added in SDK 1.4.0.

Product parameters (spec table)

product.parameterGroups is the spec table the shopper sees under the description. The merchant builds it in the Behio admin: named groups, ordered rows, localized labels, units. The public API returns only these curated groups.

Warehouse data groups are internal bookkeeping and never reach the storefront. No data-group id, no field key and no warehouse field type appears in any public payload, and filtering is possible only on parameters the merchant explicitly marked filterable, so a visitor can never filter by something they cannot see on the product.

const { data: product } = await client.catalog.getProduct('tricko-basic', {
  locale: 'cs',
});

product?.parameterGroups;
// [
//   {
//     slug: 'parametry-obleceni',
//     name: 'Parametry oblečení',
//     parameters: [
//       {label: 'Materiál',      value: 'bavlna', booleanValue: null,  unit: null},
//       {label: 'Gramáž',        value: '180',    booleanValue: null,  unit: 'g'},
//       {label: 'Délka rukávu',  value: '62',     booleanValue: null,  unit: 'cm'},
//       {label: 'Do sušičky',    value: null,     booleanValue: false, unit: null},
//     ],
//   },
//   {
//     slug: 'puvod',
//     name: 'Původ',
//     parameters: [
//       {label: 'Země výroby', value: 'Portugalsko', booleanValue: null, unit: null},
//     ],
//   },
// ]

Contract:

  • Array order is the order. Groups and rows arrive sorted the way the merchant arranged them. There is no order field to sort by.
  • Empty values are already gone. A parameter without a value is omitted server-side, so a template never renders an empty row and never has to filter.
  • value is a preformatted string in the requested language. booleanValue is non-null only for boolean parameters, and then value is null.
  • unit is a display unit ("cm", "g", a currency code, "%") and belongs after the value with a space. It is null when the parameter has none.
  • label, name and text values are merchant-written and already localized by the locale you requested. Print them as they arrive.
  • An empty array means the shop has not assigned any parameter group to the product. Render nothing, not an empty heading.

Variants carry their own parameters

variant.parameterGroups has exactly the same shape. A variant carries its own value wherever it has one (a different length, a different weight) and inherits the parent's everywhere else, so one renderer serves both the product table and a per-variant table under the picker.

Dedicated endpoints

The PDP normally needs no extra request, the detail already carries parameterGroups. The endpoints below are for lazy tabs, comparison pages and partial re-fetches:

EndpointReturns
GET /storefront/v1/catalog/products/{slug}/parameters{groups: ProductParameterGroup[]}
GET /storefront/v1/catalog/products/{slug}/parameters/{groupSlug}a single ProductParameterGroup, 404 when the product does not have that group

Both accept an optional ?locale=; without it the shop default language is used. {slug} is the product slug (the product id also resolves, like on getProduct), {groupSlug} is ProductParameterGroup.slug.

const { data } = await client.catalog.getProductParameters('tricko-basic', {
  locale: 'cs',
});
const groups = data?.groups ?? [];

const { data: group, error } = await client.catalog.getProductParameterGroup(
  'tricko-basic',
  'parametry-obleceni',
  {locale: 'cs'},
);
// error?.code === 'NOT_FOUND' = the product does not have that group

React hook:

import { useProductParameters } from '@behio/storefront-sdk/react';

const { data: groups = [], isLoading } = useProductParameters('tricko-basic', {
  locale: 'cs',
});
// groups: ProductParameterGroup[]

// One named group only (same hook, one request):
const { data: sizing = [] } = useProductParameters('tricko-basic', {
  locale: 'cs',
  groupSlug: 'rozmery',
});

Rendering the table

import type { ProductParameter, ProductParameterGroup } from '@behio/storefront-sdk';

function formatValue(p: ProductParameter): string {
  if (p.booleanValue !== null) return p.booleanValue ? 'Ano' : 'Ne';
  return p.unit ? `${p.value} ${p.unit}` : `${p.value}`;
}

export function SpecTable({ groups }: { groups: ProductParameterGroup[] }) {
  if (groups.length === 0) return null;

  return (
    <div className="space-y-8">
      {groups.map((group) => (
        <section key={group.slug} id={`parametry-${group.slug}`}>
          <h3 className="mb-3 text-lg font-semibold">{group.name}</h3>
          <table className="w-full text-sm">
            <tbody>
              {group.parameters.map((p) => (
                <tr key={p.label} className="border-b last:border-0">
                  <th scope="row" className="py-2 pr-4 text-left font-normal opacity-70">
                    {p.label}
                  </th>
                  <td className="py-2 font-medium">{formatValue(p)}</td>
                </tr>
              ))}
            </tbody>
          </table>
        </section>
      ))}
    </div>
  );
}

// PDP:      <SpecTable groups={product.parameterGroups} />
// Variant:  <SpecTable groups={selectedVariant.parameterGroups} />

product.media is the product gallery: images with responsive derivatives plus optional videos (type: 'VIDEO'). Render videos with a native player only when selected (no upfront download) and use an image derivative as the poster when one exists:

const items = product.media
  .filter((m) => m.url)
  .sort((a, b) => Number(b.isCover) - Number(a.isCover) || a.order - b.order);

// item.type === 'VIDEO'
// <video controls preload="metadata" poster={jpegDerivative?.url} src={item.url} />
// item.type === 'IMAGE': pick the smallest supported format from item.variants

Asset groups (videos, downloads, extra content)

product.assetGroups (SDK 1.7.0) is merchant-curated extra content attached to the product, organized into named groups such as "videos" or "downloads". It is distinct from images (the roled listing gallery) and media (the inventory gallery): asset groups carry anything the merchant wants next to the product, including PDF manuals, size charts, hosted videos and external video embeds. The array is empty when the merchant attached nothing, and empty groups are never emitted.

{
  "assetGroups": [
    {
      "group": "videos",
      "items": [
        {
          "id": "01J...",
          "kind": "EXTERNAL_VIDEO",
          "url": "https://www.youtube.com/embed/dQw4w9WgXcQ",
          "contentType": null,
          "size": null,
          "title": "Product walkthrough",
          "description": null,
          "order": 0
        },
        {
          "id": "01K...",
          "kind": "VIDEO",
          "url": "https://cdn.behio.com/.../unboxing.mp4",
          "contentType": "video/mp4",
          "size": 10485760,
          "title": "Unboxing",
          "description": null,
          "order": 1
        }
      ]
    },
    {
      "group": "downloads",
      "items": [
        {
          "id": "01M...",
          "kind": "FILE",
          "url": "https://cdn.behio.com/.../manual.pdf",
          "contentType": "application/pdf",
          "size": 524288,
          "title": "User manual",
          "description": "Setup and maintenance guide",
          "order": 0
        }
      ]
    }
  ]
}

Each item's kind decides the rendering:

  • EXTERNAL_VIDEO: an embed URL (YouTube/Vimeo). Render it in an <iframe>, never in a <video> tag. size is always null.
  • VIDEO: a file hosted on the Behio CDN. Render with a native <video> player and preload="metadata" so nothing downloads upfront.
  • IMAGE / FILE: direct file URLs; contentType and size (bytes) describe the file, so a downloads list can show "PDF, 512 kB".

title and description are already resolved in the requested locale (falling back to the shop default locale) and may be null. Items arrive sorted by order.

Rendering a video section:

import type { ProductAssetGroup, ProductAssetItem } from '@behio/storefront-sdk';

function VideoItem({ item }: { item: ProductAssetItem }) {
  if (item.kind === 'EXTERNAL_VIDEO') {
    return (
      <iframe
        src={item.url}
        title={item.title ?? 'Video'}
        className="aspect-video w-full rounded-lg"
        allow="accelerometer; autoplay; clipboard-write; encrypted-media; picture-in-picture"
        allowFullScreen
      />
    );
  }
  return (
    <video
      controls
      preload="metadata"
      src={item.url}
      className="aspect-video w-full rounded-lg"
    />
  );
}

export function ProductVideos({ groups }: { groups: ProductAssetGroup[] }) {
  const videos = groups.find((g) => g.group === 'videos');
  if (!videos || videos.items.length === 0) return null;

  return (
    <section className="space-y-6">
      {videos.items.map((item) => (
        <figure key={item.id}>
          <VideoItem item={item} />
          {item.title && <figcaption className="mt-2 text-sm">{item.title}</figcaption>}
        </figure>
      ))}
    </section>
  );
}

// PDP: <ProductVideos groups={product.assetGroups} />

Availability & low stock

Every product (list item and detail) carries a resolved availability status plus an optional low-stock nudge. Both are computed server-side, so the template only renders them:

const p = await client.catalog.getProduct('wireless-headphones', {locale: 'cs'});

p.availability;
// { code: 'in-stock', label: 'Skladem', color: null, restockAt: null }
// code is a stable machine hook: a merchant preset slug ('preorder',
// 'on-order', custom) or the derived 'in-stock' / 'sold-out'.
// label is already localized, render as-is.
// restockAt (epoch ms) = "available from" date for preorder-style states.

p.lowStockRemaining;
// number | null. Non-null ONLY when the merchant enabled the indicator AND
// 0 < stock <= threshold. Render "Zbývá posledních 3 kusy" whenever set.
// No client-side threshold math: null means "show nothing".

Variants carry lowStockRemaining too, so the variant picker can flag the last pieces of a specific size or colour.

Stock behaviour (shop-level contract)

shop.checkout.stockBehavior from getShopInfo() decides what sold-out means, and the server enforces it:

stockBehaviorCatalog listsPDPAdd to cart / qty updateCheckout
HIDESold-out products are filtered out server-sideDirect link still resolves; render as sold out400 above available stock400 above stock
SHOW_SOLD_OUTVisible with sold-out stateVisible, buy button disabled400 above available stock400 above stock
BACKORDERVisiblePurchasableAny quantity acceptedAccepts over-stock

For HIDE / SHOW_SOLD_OUT, cap quantity steppers at stockQuantity, because the cart API rejects anything above it with a 400. For BACKORDER, keep the buy button active on sold-out products and show "Na objednávku" (use availability.label).

Sale window & early bird (launch)

Any product can have a sale window: it is visible in the catalog from the moment it is published, but purchasable only between saleStartAt and saleEndAt (both epoch ms, either can be null). The server enforces the window on add-to-cart, quantity updates and checkout with a 400, so the storefront only renders the state:

const p = await client.catalog.getProduct('online-kurz', {locale: 'cs'});

p.isPurchasable;
// Server-resolved "can this be bought RIGHT NOW": the sale window is open AND
// the product can be ordered (stock / backorder rules included). Disable the
// buy button when false. Never re-derive the rule client-side.

p.saleStartAt; // epoch ms or null. Before it, render "Prodej startuje za ..."
p.saleEndAt;   // epoch ms or null. While open, optionally count down to close

p.earlyBirdActive;
// true = price.amount already IS the discounted early-bird amount and the
// regular price sits in price.compareAtPrice. Early bird is time-based and
// applies in the shop default currency.
p.earlyBirdUntil; // epoch ms. Render "Early bird do ..."; null when inactive

Before the sale opens, collect e-mails with the regular back-in-stock subscription (client.catalog.notifyWhenAvailable(productId, email)). The platform notifies the whole queue automatically the moment saleStartAt passes.

const results = await client.catalog.search('bluetooth speaker', {
  page: 1,
  limit: 10,
});

Dynamic Filters

getFilters returns the parameters the merchant marked as filterable, already localized and grouped the same way as the spec table:

const { data } = await client.catalog.getFilters({ locale: 'cs' });

data?.filters;
// [
//   {
//     key: 'material',                    // parameter slug, the key `parameters` expects
//     name: 'Materiál',
//     type: 'enum',                       // 'enum' | 'range' | 'boolean'
//     groupSlug: 'parametry-obleceni',
//     groupName: 'Parametry oblečení',
//     unit: null,
//     values: ['bavlna', 'len', 'vlna'],
//   },
//   {
//     key: 'hmotnost',
//     name: 'Gramáž',
//     type: 'range',
//     groupSlug: 'parametry-obleceni',
//     groupName: 'Parametry oblečení',
//     unit: 'g',
//   },
// ]

Send the selection back in parameters, keyed by the same key:

const { data } = await client.catalog.getProducts({
  category: 'trika',
  parameters: {
    material: ['bavlna', 'len'], // enum, array = OR within the key
    hmotnost_min: 120,           // range, `<key>_min` / `<key>_max`
    hmotnost_max: 200,
    do_susicky: true,            // boolean
  },
});
  • type describes how to render the control, not how the value is stored: enum renders the values list as checkboxes, range renders two number inputs, boolean renders a single checkbox.
  • groupSlug + groupName let the sidebar use the merchant's own grouping, so the filters read like the product's spec table.
  • unit goes next to the range inputs (g, cm, %, a currency code) and is null when the parameter has none.
  • Pass locale to get labels and enum values in the shopper's language. Without it the shop default language is used.
  • Nothing else is filterable. A parameter the merchant did not mark as a filter never shows up here and is rejected in parameters, so a visitor cannot filter by a field they cannot see.

Facets (values + counts)

getFacets(query) returns an Alza-style faceted-navigation payload for the same query you pass to getProducts: facet groups (filterable parameters, labels, price, availability, rating, subcategories) with selection-aware counts. Counts for a facet are computed with that facet excluded (ticking one brand does not zero the others), and values that fall to 0 under the current selection are still returned so you can render them disabled instead of hiding them.

const { data } = await client.catalog.getFacets({
  category: 'kabely',
  facets: { barva: ['cerna'] }, // slug-based selection (SEO URLs)
});

// data.facets:     [{ key, name, type: 'enum'|'range'|'boolean', groupSlug, groupName, unit?, values?: [{ value, label, slug, count }], range? }]
//                  // key = parameter slug, the same key `parameters` and `facets` use
// data.priceRange: { min, max, currency }
// data.availability: { inStockCount, outOfStockCount }
// data.rating:     [{ from: 4, count }, { from: 3, count }]
// data.labels:     [{ slug, name, color, count }]
// data.categories: [{ slug, name, count }]  // child subcategories of the current category

React hook:

import { useFacets } from '@behio/storefront-sdk/react';

const { data: facets } = useFacets({ category: 'kabely', priceMin: 100 });

Each enum facet value carries a stable slug (e.g. "cerna") so you can build indexable SEO landing pages like /category/kabely/f/barva-cerna and pass the slug back via facets to filter both getProducts and getFacets.

A facet describes a parameter, never a warehouse field: key is the parameter slug, groupSlug is the parameter group, and there is no fieldType. type (enum / range / boolean) is a rendering hint, so build the control from it and never from a storage type.

On this page