Product data

Every field of the Product object that useProduct, ProductCard and the product page hand you, with real example values.

Read this when you build a product page, a card or a slider and need to know what a Product looks like and which fields can be missing.

The type is Product from @salla.sa/twilight-theme-engine/types. Prices are numbers in the store's currency. The fields on this page match what the demo store's API returns today; names in the examples are translated to English.

Where products come from

A Product reaches your code in three ways. They are the same type but not the same data.

SourceWhat you get
product.list (cards, sliders, search, wishlist)A light list item: one image, no options, skus, images or full detail flags.
product.find (the product page loader)The detail object: everything in the list item plus images, options, skus, brand, category, tags, rating, bundle and more.
useProduct(initial)The product you gave it, kept live. It returns { product, reload }.

useProduct adds no fields. When the customer picks options it replaces price, sale_price, regular_price, is_on_sale, weight, sku and barcode, clears starting_price and sets is_out_of_stock, and reload() swaps in a fresh detail object. Pass { isPreview: true } while you only have a list item, so the hook re-seeds when the detail arrives.

ProductCard takes a Product (list item or detail). The product page puts the detail object in the loader data as product. ProductContextProvider and useProductContext() from @salla.sa/twilight-theme-engine/contexts share one product down your component tree: useProductContext() returns { product }, or null outside a provider. HookSlot does not read it, so a slot handler gets the product only through the slot's context.

Fields only the detail has

images, options, skus, bundle, included_products, notify_availability, barcode, base_currency_price, has_read_more, can_show_remained_quantity, has_size_guide, is_wallet_gift_card, supports_pickup and supports_delivery. On a list item they are undefined. Do not use them in a card without a fallback.

Read a product in a component

app/components/product/ProductSummary.tsx
import { useProduct } from '@salla.sa/twilight-theme-engine/hooks/useProduct';
import { useMoney } from '@salla.sa/twilight-theme-engine/hooks/useMoney';
import type { Product } from '@salla.sa/twilight-theme-engine/types';

export function ProductSummary({ initial }: { initial: Product }) {
  const { product } = useProduct(initial);
  const { format } = useMoney();

  return (
    <div>
      <img src={product.image?.url} alt={product.image?.alt ?? product.name} />
      <h1>{product.name}</h1>
      {product.is_on_sale ? (
        <p>
          <strong>{format(product.sale_price ?? product.price)}</strong> <s>{format(product.regular_price)}</s>
        </p>
      ) : (
        <p>{format(product.price)}</p>
      )}
      {product.is_out_of_stock && <p>Out of stock</p>}
    </div>
  );
}

Example: a simple product on sale

The detail object of a product with no options, on sale (trimmed: description and images are shortened, images has 2 of its items).

Simple product on sale (demo store)
{ 45 }
:627583647number on detail, string on lists
:"Multi-tier dining table"
:"<p>A simple, good-looking design...</p>"HTML
:"https://raed.salla.design/en/.../p627583647"absolute URL
:"6% off"the badge text, null when none
:null
:"product"
:"sale"sale | out | out-and-notify | hidden
:188what the customer pays now
:188
:199the price before the discount
:nullonly for products with variants
:{ 2 }
:"SAR"
:true
:null
:-1
:55
:false
:true
:false
:true
:"22 KG"
:{ 2 }
:[ 2 ]
:[ 0 ]
:[ 0 ]
:[ 0 ]
:null
:{ 5 }
:{ 5 }
:null
:[ 0 ]
:null
:true
:false
:false
:false
:false
:false
:false
:false
:null
:true
:false
Click a key to copy its path.

Example: a product with options

The demo product "Light": one color option, a SKU per value, a 3D main image, and a starting_price lower than price.

Product with options (demo store)
{ 15 }
:73203761
:"Light"
:"product"
:"sale"
:199
:199
:199equals price even when not on sale (see Gotchas)
:100lowest SKU price: show "Starting from"
:false
:-1
:5
:[ 2 ]
:{ 4 }
:[ 1 ]
:[ 1 ]
Click a key to copy its path.

A list item for the same kind of product is much smaller. Here is the real list item of a product with variants (description removed). Note the string ids, sale_price: null, the single image, and has_options instead of options.

Product list item (demo store)
{ 22 }
:"1303461379"
:"Leather chair with wooden legs 88"
:"https://raed.salla.design/en/.../p1303461379"
:"Special offer"
:"product"
:"sale"
:199
:199
:null
:189
:"SAR"
:false
:true
:true
:false
:null
:null
:null
:{ 1 }no alt
:{ 2 }url is null
:{ 3 }
:[ 1 ]
Click a key to copy its path.

Field reference

The tables are generated from Product in @salla.sa/twilight-theme-engine/types. "Detail" means the field is reliably present only on product.find. Nested objects (images, options, SKUs, category, brand) get their own tables. Prices on Product are plain numbers in currency; use useMoney().format to display them. For the { amount, currency } object, see shared types.

Product

FieldTypeDescription
idnumberProduct id. Number on products/{id}/details, but a numeric STRING on list responses (products, e.g. "677709975"). Typed number: use Number(product.id) when you compare ids from a list item.
namestringProduct name in the current language.
descriptionstringProduct description, as HTML. May be empty.
urlstringAbsolute URL of the product page.
promotion_title?stringBadge text the merchant set. Can be "" or null.
subtitle?stringShort line under the name.
typeProductTypeProduct type.
statusProductStatusProduct availability status.
weight?string | nullWeight with its unit, as text.
calories?number | nullCalories, for food products.
sku?string | nullMerchant SKU. Can be "" or null.
barcode?string | nullBarcode. Can be "" or null.
mpn?string | nullManufacturer part number. Can be "" or null.
gtin?string | nullGlobal trade item number. Can be "" or null.
pricestring | numberWhat the customer pays now. Show this when the product is not on sale.
sale_pricenumber | nullThe discounted price. null on list responses when the product is not on sale; 0 on products/{id}/details in the same case. Check is_on_sale rather than the truthiness of this field.
regular_pricenumberThe price before the discount.
starting_price?number | nullLowest SKU price of a product with variants, otherwise null.
base_currency_pricenumber | { currency: string; amount: number; }Price in the store's base currency.
currencystringCurrency code of the prices.
discount_percentage?stringDeclared but never sent by the Salla API (absent on list and detail). Compute it from regular_price and price.
price_as_float?numberPrice as a plain float.
price_as_float_for_payment?numberPrice as a plain float, as used for payment.
currency_for_payment?stringCurrency code used for payment.
quantity?number | nullStock on hand. -1 means UNLIMITED stock (what products/{id}/details sends for products without a tracked quantity), not "one short". List responses send null and expose unlimited_quantity instead.
sold_quantity?numberUnits sold. Absent or null when not shown.
max_quantitynumberMost the customer can add to the cart. Null on list responses.
discount_ends?stringWhen the discount ends.
rating?ProductRatingReviews summary. Null on the demo store.
category?ProductCategoryThe product's category.
brand?ProductBrandThe product's brand.
tags?ProductTag[]The product's tags. Empty when there are none.
imageProductImageThe main image. On list items it has only url.
images?ProductImage[]The image gallery. Detail responses only.
options?ProductOption[]Options the customer picks, such as size or color. Detail responses only.
skus?ProductSku[]Variant rows. Present only when requested with with[]=skus.
notify_availability?NotifyAvailability"Notify me when available" settings and subscription state. Detail responses only.
donation?ProductDonationDonation progress, for products of type 'donating'.
is_taxablebooleanTax applies to the product.
has_read_morebooleanThe description is long: collapse it behind "Read more".
can_add_notebooleanThe add-to-cart form shows a note field.
can_show_remained_quantitybooleanWhether to show quantity to customers.
can_show_sold?booleanDeclared but never sent by the Salla API (absent on list and detail).
can_upload_filebooleanThe add-to-cart form shows a file upload.
has_custom_formbooleanThe merchant added a custom form to the product.
has_metadatabooleanThe product has extra spec data.
has_options?booleanThe product has options.
has_bundle_products?booleanThe product is a bundle of other products.
is_on_salebooleanA discount is active.
is_hidden_quantity?booleanThe quantity input is hidden (the customer buys one). Sent on products/{id}/details only. List responses use the misspelled is_hidded_quantity.
is_hidded_quantity?booleanThe API's own (misspelled) name for is_hidden_quantity on list responses.
is_availablebooleanThe product can be bought.
is_in_wishlist?booleanWhether the product is in the wishlist. Prefer useWishlist over reading this.
is_out_of_stockbooleanThe customer cannot buy it now.
is_require_shippingbooleanThe product needs shipping.
has_3d_image?booleanThe product has a 3D image.
has_size_guidebooleanA size guide exists for the product.
giftable?booleanThe product can be bought as a gift.
can_quick_buy?booleanShow a "buy now" button.
show_availability?booleanShow the product's availability state.
has_preorder_campaign?booleanA pre-order campaign is active.
is_wallet_gift_card?booleanThe product is a wallet gift card. Sent on products/{id}/details.
supports_pickup?booleanThe product can be picked up.
supports_delivery?booleanThe product can be delivered.
included_products?Product[] | nullProducts bundled into this one (bundle products); null/absent otherwise.
add_to_cart_label?stringCustom add-to-cart button label. Not sent by the demo store.
notes?stringThe customer's note for the product.
digital_files_settings?JsonObjectFile settings of a digital product.
preorder?JsonObjectPre-order details; preorder.label is shown as the card badge.

rating

FieldTypeDescription
countnumberNumber of ratings.
starsnumberAverage star rating.

category

FieldTypeDescription
id?numberCategory id.
namestringCategory name. Can be null on list responses.
urlstringCategory page URL. Can be null on list responses.
icon?stringCSS icon class of the category.
image?string | nullCategory image URL, or null.
sub_categories?Category[]Child categories.

category.sub_categories

FieldTypeDescription
idstring | numberId as the API sends it, a hashed string such as "xzjvNQ".
id_?numberThe raw numeric id, e.g. 868669273.
namestringCategory name.
description?stringCategory description.
urlstringFull URL of the category page.
icon?string | nullCSS icon class, e.g. "sicon-store", or null.
image?string | nullFlat image URL string (NOT a nested ProductImage object), or null. Aligns with both the API response and Twig.
products_count?numberNumber of products in the category.
sub_categories?Category[]Child categories, possibly empty.
items?Category[] | nullMirror of sub_categories on parent categories (an array, possibly empty); null on some root categories without children.

brand

FieldTypeDescription
idnumberBrand id. A slug string on the demo store.
name?stringBrand name.
description?stringBrand description.
url?stringBrand page URL.
logo?stringBrand logo URL.

tags

FieldTypeDescription
id?numberTag id.
namestringTag text.
urlstringTag page URL.

image

FieldTypeDescription
id?numberImage id.
url?stringImage URL.
alt?stringAlt text; usually the product name.
video_url?stringVideo URL when the item is a video, otherwise null.
video_type?"video" | "youtube" | nullyoutube for a YouTube link, video for an uploaded file, null for an image.
type?"image" | "video"Media kind. The API also sends '3d-image' for a 3D main image.
three_d_image_url?string.glb 3D model URL, or "" when none.
main?booleanTrue for the main image.
sort?numberPosition in the gallery.

images

Same fields as image.

options

FieldTypeDescription
idnumberOption id.
namestringOption label.
typestringKind of input. Unknown types should be treated as a plain list.
requiredbooleanThe customer must choose a value.
values?ProductOptionValue[]Values in the older listing shape. Absent on products/{id}/details, which sends details instead.
details?ProductOptionDetail[]Values on a detail response.

options.values

FieldTypeDescription
idnumberValue id.
namestringValue label.
price?numberExtra price when this value is picked.
image_url?stringImage URL of the value.
image?stringImage URL of the value.
is_selected?booleanThis value is preselected.

options.details

FieldTypeDescription
idnumberValue id. This is what a SKU's related_option_values lists.
namestringValue label.
option_idnumberId of the option the value belongs to.
additional_pricenumberExtra price when this value is picked.
option_valuestringThe raw value: a color hex, an image id, or "".
imagestringImage URL, or the hex for a color.
colorstringColor hex for color options.
codestringMerchant code of the value.
is_outbooleanThis value is out of stock.
is_default0 | 11 for the preselected value, otherwise 0.
skus_availabilityRecord<string, boolean>SKU id to in-stock flag, for the SKUs that use this value.

skus

FieldTypeDescription
idnumberSKU id.
product_idnumberId of the parent product.
priceMoneySKU price as a { amount, currency } object.
regular_priceMoneySKU price before any discount.
sale_priceMoney | nullDiscounted SKU price, or null when not discounted.
has_special_pricebooleanA discount applies to this SKU.
stock_quantitynumber | nullStock on hand, or null when unlimited.
unlimited_quantitybooleanStock is not tracked.
is_defaultbooleanThis is the default SKU.
related_optionsnumber[]Ids of the options the SKU is made of.
related_option_valuesnumber[]Ids of the option values that select this SKU.

skus.price

FieldTypeDescription
amountnumberThe amount as a number.
currencystringCurrency code.
formatted?stringReady-made text, when the API sends one.

skus.regular_price

Same fields as skus.price.

skus.sale_price

Same fields as skus.price.

notify_availability

FieldTypeDescription
channelsstring[]Channels the customer can be notified through.
subscribedbooleanThe customer has asked to be notified.
subscribed_options?number[] | JsonObjectOptions the customer subscribed to, when they subscribed per option.
options?booleanWhether subscription is offered per option.

donation

FieldTypeDescription
target_message?stringMessage shown with the donation goal.
collected_amountnumberAmount collected so far.
target_amountnumberAmount the donation aims to collect.
target_percentnumberProgress towards the goal, as a percentage.
target_end_date?stringWhen the donation campaign ends.
can_donatebooleanDonations are currently accepted. ProductCard hides the price when this is true.

Allowed values and detail-only flags

type is one of product, service, group_products, codes, digital, food, donating, booking or financial_support. status is one of sale, out, out-and-notify or hidden. An option's type is text, textarea, radio, checkbox, select, image or color; the demo store also sends thumbnail and single-option. has_options is true on list items and is not on the detail object, so check options?.length on a detail.

What useProduct returns

FieldTypeDescription
productProductCurrent product state (updates on option changes)
reload() => Promise<void>Reload product data from API

Variants by product type

The engine has no separate type per kind of product. It reads type and these fields:

KindtypeWhat is different
Donationdonatingdonation is { target_message?, collected_amount, target_amount, target_percent, target_end_date?, can_donate }. ProductCard hides the price when donation.can_donate is true.
Digitaldigitaldigital_files_settings holds the file settings. Check is_require_shipping to know if it ships.
BookingbookingThe quantity is fixed to 1 in the add-to-cart form.
Group bundlegroup_productsbundle and included_products hold the products it is made of, and has_bundle_products is the flag.
Service, food, codes, financial supportservice, food, codes, financial_supportNo extra fields in the type. calories is for food.

Common tasks

Show the discount percentage

function discountPercent(product: Product): number {
  if (!product.is_on_sale || !product.regular_price) return 0;
  return Math.round((1 - Number(product.price) / product.regular_price) * 100);
}

For regular_price: 199 and price: 188 it returns 6. Merchants often type the same text in promotion_title, so the badge below may already say it.

Is it in stock?

const { product } = useProduct(initial);
const canBuy = product.is_available && !product.is_out_of_stock;

useProduct flips is_out_of_stock when the SDK reports that the chosen option combination is not available.

To show a stock message, treat quantity === -1 as unlimited:

const unlimited = product.quantity === -1;
const remaining = product.can_show_remained_quantity && !unlimited ? product.quantity : null;

Which variant is selected?

The product object does not store the customer's choice. Keep the chosen value ids yourself (from your option inputs), then find the SKU:

import type { Product } from '@salla.sa/twilight-theme-engine/types';

function findSku(product: Product, selectedValueIds: number[]) {
  return product.skus?.find(
    (sku) =>
      sku.related_option_values.length === selectedValueIds.length &&
      selectedValueIds.every((id) => sku.related_option_values.includes(id))
  );
}

Then sku.price.amount is the price of that variant and sku.stock_quantity its stock (null means unlimited).

const urls = product.images?.map((img) => img.url) ?? [product.image?.url];
const main = product.images?.find((img) => img.main)?.url ?? product.image?.url;

Check type before you render an item as <img>: it can be a video (video_url) or a 3D image (three_d_image_url).

Show the badge

There is no badge field. The card chooses one text in this order: preorder.label, promotion_title, then (with withQuantity) the remaining quantity, then the out-of-stock label.

const badge = (typeof product.preorder?.label === 'string' && product.preorder.label) || product.promotion_title || null;

Digital, booking and donation

if (product.type === 'donating' && product.donation) {
  const { collected_amount, target_amount, target_percent, can_donate } = product.donation;
}
const needsShipping = product.is_require_shipping;
const fixedQuantity = product.type === 'booking' || product.is_hidden_quantity;

Gotchas

  • Ids differ by source. Lists send string ids, detail sends numbers, but the type says number. Compare with String(a.id) === String(b.id), and pass Number(product.id) to useWishlist.
  • sale_price when there is no sale. With no sale it can be null, 0 or equal to price. Branch on is_on_sale, not on sale_price.
  • Prices are numbers on Product but { amount, currency } objects on skus[] (and base_currency_price). See shared types.
  • null, empty and missing all happen. promotion_title can be "" or null; rating is null without reviews; list items leave out whole sections. Use optional chaining and ?? [].
  • options can be [] on a product without options, not missing. Check .length.
  • List items are partial. image has no alt; category.url, brand.url and tags[].url can be null; quantity and max_quantity are null; is_hidden_quantity is spelled is_hidded_quantity.
  • description is HTML written by the merchant. Render it with dangerouslySetInnerHTML, and pass it through stripUnsafeHtml from @salla.sa/twilight-theme-engine/utils first. List items from the API may have it HTML-escaped (&lt;p&gt;), so use the detail description on a product page.
  • Live changes are browser-only. On the server useProduct returns exactly the product you passed.

Next

On this page