Themes
AJAX Cart API
AJAX Cart API
Theme JavaScript reads and changes the cart through JSON endpoints on the store's own origin. The paths match Shopify's, so the fetch calls look familiar. The request and response bodies differ in a few places that break copied Shopify code: IDs are UUID strings, id in an add request means a product, and line items carry no usable id.
Prices are in minor units
Every price in these responses is an integer: the amount multiplied by 100. Rs. 1,500 is 150000. The multiplier is always 100, whatever the currency.
Liquid is different. {{ product.price }} is 1500. Divide by 100 before displaying an API price, and don't compare API prices with Liquid prices without converting.
function formatPrice(minor, currency) {
return new Intl.NumberFormat(undefined, { style: "currency", currency })
.format(minor / 100);
}
formatPrice(cart.total_price, cart.currency); // "PKR 1,500.00"The cart session
A cart is identified by the cart_session_id cookie. It is HttpOnly, SameSite=Lax, lasts 30 days, and is created by the first successful POST /cart/add.js. Same-origin fetch sends it automatically.
Until that cookie exists, GET /cart.js returns an empty cart, and change.js and update.js return 404.
Endpoints
Request bodies can be JSON or form-encoded, except update.js, which takes JSON only.
| Method | Path | Body or query | Returns |
|---|---|---|---|
GET | /cart.js | Cart | |
POST | /cart/add.js | variant_id, id or product_id, quantity | The whole cart |
POST | /cart/change.js | line or id, quantity | The whole cart |
POST | /cart/update.js | { "updates": { "<id>": quantity } } | The whole cart |
POST | /cart/clear.js | An empty cart | |
GET | /products/:handle.js | Product | |
GET | /collections/:handle/products.json | limit: default 50, maximum 250. The handle all lists every product | { "products": [ … ] } |
GET | /search/suggest.json | q | { "resources": { "results": { "products", "collections", "pages" } } } |
GET | /recommendations/products.json | product_id (UUID), limit: default 4, maximum 10 | { "products": [ … ] } |
Predictive search returns up to 6 products, 3 collections and 3 pages, matched against titles. resources[type] and other Shopify parameters are ignored. Recommendations fall back to recent products when product_id is missing or invalid. They return JSON only, never rendered section HTML.
Adding to cart
POST /cart/add.js resolves what to add like this:
| Body | Adds |
|---|---|
{ "variant_id": "<variant UUID>" } | That variant |
{ "id": "<product UUID>", "variant_id": "<variant UUID>" } | That variant of that product. product_id works in place of id |
{ "id": "<product UUID>" } | The product without a variant, at the product price |
quantity defaults to 1. Adding something already in the cart increases that line's quantity.
id is read as a product ID. Shopify's { id: variant.id } fails with 422 Product not found, and numeric IDs are ignored. Send variants as variant_id.
Not supported: the items: [ … ] array, line properties, selling plans and the sections parameter.
When the product tracks inventory and the requested quantity exceeds stock, the response is 422 with one of these messages:
The selected variant is sold outThis product is currently sold outAll items are in your cart
async function addToCart(variantId, quantity = 1) {
const res = await fetch("/cart/add.js", {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify({ variant_id: variantId, quantity }),
});
const body = await res.json();
if (!res.ok) throw new Error(body.message); // 422: { status, message, description }
document.querySelectorAll("[data-cart-count]")
.forEach((el) => { el.textContent = body.item_count; });
return body; // the whole cart, not the added line
}Changing quantities
POST /cart/change.js targets one line:
line: the 1-based position initemsid: the line's ID or its variant's UUID
quantity sets the new quantity. 0 or less removes the line.
Each item's id and variant_id in the cart JSON are always 0, so they can't be sent back. Use line, or the part of key before the colon. key has the form <line id>:<product id>.
POST /cart/update.js takes the same IDs as keys of updates, and sets several lines at once. Unknown keys are ignored.
A missing cart or line returns 404 with { "status": 404, "message": "…" }. An unreadable body returns 400.
async function setLineQuantity(item, quantity) {
const res = await fetch("/cart/change.js", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ id: item.key.split(":")[0], quantity }),
});
if (!res.ok) throw new Error((await res.json()).message);
return res.json();
}
// Or by position:
// body: JSON.stringify({ line: 1, quantity: 0 })Cart JSON
| Field | Value |
|---|---|
token | The cart session ID |
item_count | Total quantity |
total_price, items_subtotal_price, original_total_price, checkout_charge_amount | All equal: the sum of line prices, in minor units |
currency | The market's currency code, or PKR when the store has no market |
items | Lines, below |
note, attributes, total_discount, total_weight, cart_level_discount_applications | Present for compatibility, always empty or 0 |
| Item field | Value |
|---|---|
key | <line id>:<product id> |
id, variant_id | Always 0 |
product_id, handle, url | Product UUID, handle and /products/<handle> |
title, product_title, variant_title, sku, vendor | Display fields |
quantity | Line quantity |
price, final_price, original_price, discounted_price | Unit price, in minor units |
line_price, final_line_price | Unit price × quantity |
image | Featured image URL |
product_has_only_default_variant | true when the product has no variants |
Product JSON
GET /products/:handle.js returns id (UUID), title, handle, description, tags, url, featured_image, images (URLs), options, variants and available. Prices are price, price_min, price_max, compare_at_price and template_suffix, all in minor units except the last.
Each variant has id (UUID), title, price, compare_at_price, available, quantity, sku and option1 to option3. A product without variants gets one stand-in variant with the ID <product id>-default. That ID can't be added to the cart, so add such products by product ID.
vendor and type are empty, and metafields is an empty object.
Market pricing: the JSON endpoints apply the visitor's market price adjustment and rounding, but not exchange rates or fixed market prices. For a market in another currency, these prices can differ from the ones Liquid renders. Render display prices with Liquid, for example into data- attributes, and use the API for cart state.
Platform JavaScript globals
{{ content_for_header }} defines these on every page.
| Global | Value |
|---|---|
Shopify.shop | The request hostname |
Shopify.locale, Eekaam.locale | Store language, for example en |
Shopify.currency.active, Eekaam.currency.active | The market's currency code |
Shopify.currency.rate | Always "1.0" |
Shopify.routes.root | / |
Eekaam.shop | Store name |
Nothing else on window.Shopify is defined. That includes Shopify.theme, Shopify.designMode and Shopify.formatMoney.
When the store has connected tracking pixels, the matching helper is also defined:
| Helper | Defined when |
|---|---|
Eekaam.trackAddToCart(id, name, price) | Meta Pixel is connected |
Eekaam.trackTikTokAddToCart(id, name, price) | TikTok Pixel is connected |
Eekaam.trackSnapchatAddToCart(id, price) | Snapchat Pixel is connected |
Eekaam.trackGoogleAddToCart(id, name, price) | Google Analytics 4 is connected |
With any pixel connected, content_for_header also wraps window.fetch. A successful fetch to a URL string containing /cart/add fires the add-to-cart event automatically, so don't call the helpers after a fetch add as well, or each add is counted twice. Call them only when you add with XMLHttpRequest, a Request object or a form post. Test for them first, because they are undefined when no pixel is connected.
Updated 15 September 2026