eekaam.docs

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.

MethodPathBody or queryReturns
GET/cart.jsCart
POST/cart/add.jsvariant_id, id or product_id, quantityThe whole cart
POST/cart/change.jsline or id, quantityThe whole cart
POST/cart/update.js{ "updates": { "<id>": quantity } }The whole cart
POST/cart/clear.jsAn empty cart
GET/products/:handle.jsProduct
GET/collections/:handle/products.jsonlimit: default 50, maximum 250. The handle all lists every product{ "products": [ … ] }
GET/search/suggest.jsonq{ "resources": { "results": { "products", "collections", "pages" } } }
GET/recommendations/products.jsonproduct_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:

BodyAdds
{ "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 out
  • This product is currently sold out
  • All 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 in items
  • id: 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

FieldValue
tokenThe cart session ID
item_countTotal quantity
total_price, items_subtotal_price, original_total_price, checkout_charge_amountAll equal: the sum of line prices, in minor units
currencyThe market's currency code, or PKR when the store has no market
itemsLines, below
note, attributes, total_discount, total_weight, cart_level_discount_applicationsPresent for compatibility, always empty or 0
Item fieldValue
key<line id>:<product id>
id, variant_idAlways 0
product_id, handle, urlProduct UUID, handle and /products/<handle>
title, product_title, variant_title, sku, vendorDisplay fields
quantityLine quantity
price, final_price, original_price, discounted_priceUnit price, in minor units
line_price, final_line_priceUnit price × quantity
imageFeatured image URL
product_has_only_default_varianttrue 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.

GlobalValue
Shopify.shopThe request hostname
Shopify.locale, Eekaam.localeStore language, for example en
Shopify.currency.active, Eekaam.currency.activeThe market's currency code
Shopify.currency.rateAlways "1.0"
Shopify.routes.root/
Eekaam.shopStore 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:

HelperDefined 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