Cart data
The cart object with its items, options, totals, coupon and free-shipping bar, and how to read and change it.
Use this page when you build a mini-cart, a cart page or a checkout banner and need to know what the cart contains and what each field means.
Read it
The cart lives in the browser: the cart id is stored there, so the server cannot load it. Ask the Salla SDK for the id, then fetch the cart with the cart API module.
import { useEffect, useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { cart } from '@salla.sa/twilight-theme-engine/api/cart';
import { useMoney } from '@salla.sa/twilight-theme-engine/hooks/useMoney';
export function MiniCart() {
const { format } = useMoney();
const [cartId, setCartId] = useState<number | null>(null);
useEffect(() => {
window.salla?.cart.api.getCurrentCartId().then(setCartId);
}, []);
const { data } = useQuery({ ...cart.queries.detail(cartId!), enabled: !!cartId });
if (!data) return null; // no cart yet, or still loading
return (
<p>
{data.items.length} items, {format(data.total)}
</p>
);
}The built-in cart page does exactly this and shows a skeleton until the cart arrives. To follow changes live, listen to cart events (React to cart events) and invalidate the query.
The Cart object
Example (illustrative values):
The tables are generated from Cart in @salla.sa/twilight-theme-engine/types. Nested objects get their own tables: items (the lines), their options, offers and attachments, and free_shipping_bar. Example values are in the explorer above.
| Field | Type | Description |
|---|---|---|
id | string | Cart id. Twig: string — matches API string cart ID (SDK CartSummary incorrectly types this as number) |
count | number | Number of items. Present in SDK CartSummary; not documented in Twig docblock |
items | CartItem[] | The cart lines. An empty array means an empty cart. |
sub_total | number | Items total, the first row of the order summary. |
total | string | number | Final amount. |
discount | number | Discount amount. |
coupon? | string | null | Applied coupon code, or null. |
tax_amount | number | Tax amount. |
is_require_shipping? | boolean | Whether anything in the cart needs shipping. |
has_shipping | boolean | Whether a shipping cost applies now. |
real_shipping_cost | number | The shipping cost. |
free_shipping_bar? | FreeShippingBar | null | Progress towards free shipping. Null or missing when the store does not offer it. |
options | CartOption[] | Order-level options. |
options_total? | number | null | Total price of the order-level options. |
total_discount? | number | null | Discount row of the order summary. Often 0, null or missing: test with !!. |
should_refresh? | boolean | React-only runtime flag — triggers a client-side cart re-fetch after mutations |
gift? | { enabled: boolean; text?: string; type?: string; } | null | Gifting settings, or null. Twig exposes gift as a top-level page variable; the REST API sends it inside the cart object. |
loyalty? | { prize: { points: number; title: string; }; } | null | The loyalty reward the customer can win, or null. Twig exposes loyalty as a top-level page variable, not nested inside cart; the REST API returns it inside the cart object, so the loader maps it here. |
items
| Field | Type | Description |
|---|---|---|
id | string | Line id. Use it to update or remove the line, not product_id. Twig docblock says int; API returns numeric string IDs — kept as string for safe attribute binding |
product_id | number | Id of the product. |
product_name | string | Product name. |
product_image | string | Image URL (a plain string, not an image object). |
url | string | Product page URL. |
quantity | number | Quantity in the cart. |
max_quantity? | number | Most the customer can buy. |
type | string | Product type, such as "product" or "donating". |
price | number | Money | Unit price the customer pays. |
product_price | number | Money | Unit price of the product. |
original_price? | number | Money | Price before a sale. |
total | number | Money | Line total. |
total_special_price? | number | Money | Line total after tiered offers. |
weight_label? | string | Readable weight, such as "2 kg". |
notes? | string | The customer's note. |
is_available | boolean | False when the product is out of stock. |
is_hidden_quantity | boolean | When true, show the quantity as text instead of a stepper. |
is_on_sale? | boolean | A sale price applies. |
has_discount? | boolean | A discount applies. |
can_add_note | boolean | The product accepts a note. |
can_upload_file | boolean | The product accepts file attachments. |
offer? | CartItemOffer | null | A simple offer on this line, or null. |
detailed_offers? | JsonObject[] | null | Detailed offers, shown by the offers component. |
donation? | JsonObject | null | Donation data when type is "donating". |
attachments? | CartItemAttachment[] | Files the customer uploaded. |
options? | CartItemOption[] | The options the customer picked. |
has_pre_order_campaign? | boolean | Item has an active pre-order campaign. TODO: add to API response |
items.offer
| Field | Type | Description |
|---|---|---|
discount | number | Discount amount. |
is_free | boolean | The item is free. |
names? | string | Offer name to show. |
items.attachments
| Field | Type | Description |
|---|---|---|
id | number | Attachment id. |
url | string | File URL. |
product_id | number | Product the file belongs to. |
item_id | number | Cart line the file belongs to. |
name | string | File name. Twig docblock incorrectly types this as int — it is a filename string |
items.options
| Field | Type | Description |
|---|---|---|
id | number | Option id. |
name? | string | Option name. |
value? | string | Chosen value. |
quantity? | number | Quantity, when the option has one. |
options? | Record<string, string | number | boolean>[] | Nested option details. |
free_shipping_bar
| Field | Type | Description |
|---|---|---|
minimum_amount | number | Cart value needed for free shipping. |
has_free_shipping | boolean | True once the customer has reached it. |
percent | number | Progress from 0 to 100, ready for a bar width. |
remaining | number | Amount still missing. It is a plain number: format it yourself. |
options
Same fields as items.options.
gift
| Field | Type | Description |
|---|---|---|
enabled | boolean | Gifting is on. |
text? | string | Gift message text. |
type? | string | Gift type. |
loyalty
| Field | Type | Description |
|---|---|---|
prize | { points: number; title: string; } | The reward. |
loyalty.prize
| Field | Type | Description |
|---|---|---|
points | number | Points the reward costs. |
title | string | Reward title. |
All amounts are in the store's currency. Format them with useMoney, see Money.
Use a line's id to update or remove it, not product_id. CartItemOption belongs to a line (a size, a colour, a text field); CartOption is an order-level option on cart.options, with the same shape. loyalty and gift are top-level in the API response; the engine copies them onto the cart object.
Common tasks
Show the free-shipping progress:
const bar = cart.free_shipping_bar;
if (bar && !bar.has_free_shipping) {
return <progress max={100} value={bar.percent} />;
}Read a price that may be a number or a Money object:
import type { Money } from '@salla.sa/twilight-theme-engine/types';
const amount = (value: number | Money) => (typeof value === 'number' ? value : value.amount);Change the cart with the Salla SDK, then let the query refresh:
await window.salla?.cart.addItem(productId, 1);
await window.salla?.cart.updateItem({ id: item.id, quantity: 3 });
await window.salla?.cart.deleteItem(item.id);Apply or remove a coupon with useCoupon:
import { useCoupon } from '@salla.sa/twilight-theme-engine/hooks/useCoupon';
const { value, apply, remove, error, loading } = useCoupon();
await apply('SAVE10'); // resolves true on success, false on failure; `error` holds the message
// `value` starts as null on every page load: read the applied code from `cart.coupon`Share the cart with child components
CartContextProvider and useCartContext carry a cart down your component tree so you do not pass it through every level.
import { CartContextProvider, useCartContext } from '@salla.sa/twilight-theme-engine/contexts';
function Count() {
const ctx = useCartContext(); // null outside a provider
return <span>{ctx?.cart.count ?? 0}</span>;
}
// <CartContextProvider value={{ cart }}><Count /></CartContextProvider>The engine's own cart page does not mount this provider, and hook slots do not read it. Mount it yourself in your own cart page, and pass the cart to a slot with context={{ cart }}.
Gotchas
price,product_price,totaland the other line amounts can be numbers orMoneyobjects. Do not assume one.- Cart
idis a string in the object, but the API call takes a number (cart.queries.detail(cartId)). - After an SDK change, the cached cart is stale until you invalidate it. See the
cartAPI module. loyaltyis top-level in the API response and is placed on the cart object for you.giftalready arrives inside the cart.free_shipping_bar,coupon,total_discountand the itemofferare often absent. Guard each one.
Next
- Cart route and cart slots for the cart page.
- Shared types for
Money. - React to cart events