Cart AJAX API
Themes interact with the cart over a JSON API on the storefront’s own domain.
The endpoints are Shopify-Ajax compatible, so cart code written for Shopify
themes generally ports across. All requests are same-origin, use
Content-Type: application/json, and return the updated cart object. Responses
carry no-store cache headers, so the browser never serves a stale cart.
| Endpoint | Method | Purpose |
|---|---|---|
/cart.js (or /cart.json) | GET | Read the current cart |
/cart/add.js | POST | Add one or more items |
/cart/change.js | POST | Set the quantity of a single line |
/cart/update.js | POST | Bulk-update line quantities and the note |
/cart/clear.js | POST | Empty the cart |
/api/cart/buyNowCheckout | POST | Express single-product checkout |
Each /cart/* endpoint also works without the .js suffix (/cart/add,
/cart/change, and so on). The current cart is always resolved from a signed
cart cookie — never send a cartId from the browser, or the write is
rejected with a cartId forbidden validation error.
CSRF: required on every write
Every POST to a cart endpoint is protected against cross-site request forgery.
A write that omits the token is rejected with HTTP 403 before it reaches the
cart, so a theme that ignores this will see writes silently fail.
On any storefront HTML page the platform sets a readable csrf_token cookie and
exposes the same value in a <meta name="csrf-token"> tag in the document head.
Read it and send it back on every POST as the x-csrf-token header:
function csrfToken() { return document.querySelector('meta[name="csrf-token"]')?.content // fallback: the cookie is not HttpOnly, so it is also readable directly || document.cookie.match(/(?:^|; )csrf_token=([^;]+)/)?.[1] || '';}
async function cartFetch(url, body) { return fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-csrf-token': csrfToken(), }, body: JSON.stringify(body), });}If you ever need the token without a meta tag (for example in a script loaded
before the head renders), GET /api/csrf returns it in the x-csrf-token
response header.
Get the cart
const cart = await (await fetch('/cart.js')).json();console.log(cart.item_count, cart.total_price_cents);/cart.js and /cart.json are the same handler. An empty or never-created cart
returns a valid empty cart object rather than an error.
Add to cart
id is the variant id (variantId and variant_id are accepted aliases):
-
Build the body. Send a single
id/quantity, or anitemsarray to add several variants in one call. -
POST it with the CSRF header (see the
cartFetchhelper above). -
Re-render your cart UI from the returned cart object — it is the post-mutation state.
await cartFetch('/cart/add.js', { id: variantId, quantity: 1,});Multiple items at once:
await cartFetch('/cart/add.js', { items: [ { id: variantA, quantity: 2 }, { id: variantB, quantity: 1 }, ],});If you send neither id nor items, the call returns HTTP 400 with a
validation error (see Error shape). Out-of-stock or
unavailable-inventory additions also fail with a descriptive message — surface
it near the add-to-cart button rather than in a generic alert.
Change one line
/cart/change.js targets a single line and sets its quantity. Quantity 0
removes the line. You can identify the line several ways:
// By 1-based position in cart.itemsawait cartFetch('/cart/change.js', { line: 1, quantity: 3 });
// By line id — accepts cartLineId, cart_line_id, or keyawait cartFetch('/cart/change.js', { key: lineId, quantity: 0 });Accepted line targets are cartLineId, cart_line_id, key, or line (the
1-based index). If none of them resolves to a real line, the call returns a
cartLineId|cart_line_id|key|line required validation error.
Bulk update
/cart/update.js processes updates (a map or array of quantities) and an
optional note. A quantity of 0 removes that line.
await cartFetch('/cart/update.js', { // keyed by line id, or an array positionally matching cart.items updates: { [lineId]: 2 }, note: 'Please gift wrap',});Clear the cart
/cart/clear.js removes every line and returns the emptied cart object:
const cart = await (await cartFetch('/cart/clear.js', {})).json();console.log(cart.item_count); // 0Buy Now (express checkout)
POST /api/cart/buyNowCheckout runs a single-product express checkout entirely
server-side. It builds (or reuses) a one-line ephemeral cart in a separate
signed buy-now cookie — your shopper’s main cart is never read or changed — then
returns the checkout URL:
const res = await cartFetch('/api/cart/buyNowCheckout', { item: { product: { variantId, quantity: 1 } }, // discountCode is optional});const { checkout_url } = await res.json(); // e.g. "/checkout/<id>"window.location.assign(checkout_url);The body requires item; CSRF is enforced exactly like the other POST routes.
This is the flow behind a theme’s “Buy it now” button.
Bundled section rendering
To update the cart drawer, cart count, or any other section without a full page
reload, ask the cart endpoint to render Liquid sections alongside the mutation.
Pass section ids on /cart/add, /cart/change, /cart/update, /cart/clear,
or /cart.js:
sections— a comma-separated list of section ids, orsection_ids— an array of section ids, orsection_id— a single id, and optionallysections_url— a same-store path (beginning with/) whose template context the sections render in; defaults to the referring page.
await cartFetch('/cart/add.js?sections=cart-drawer,cart-icon-bubble', { id: variantId, quantity: 1,});The response adds a sections map of id → rendered HTML. Each section is
wrapped in <div id="shopify-section-...">, so you can swap it straight into the
DOM:
const data = await res.json();for (const [id, html] of Object.entries(data.sections ?? {})) { if (html == null) continue; // a section that failed to render comes back null document.getElementById(`shopify-section-${id}`)?.replaceWith( new DOMParser().parseFromString(html, 'text/html').body.firstElementChild, );}A maximum of 5 sections render per request; extra ids are ignored. An invalid
sections_url (not a same-store path) returns HTTP 400 with
error: "invalid_sections_url".
Response object
A successful read or mutation returns the cart object. Money fields are integers in paise (1/100 of a rupee), matching the platform’s cents-everywhere money model. The key fields:
| Field | Meaning |
|---|---|
id | The cart id (server-side only; never echo it back on writes) |
item_count | Total quantity across all lines |
line_count | Number of distinct lines |
items | The line items array (also available as lines) |
note | The order note, or absent if unset |
attributes | Always {} — cart attributes are not persisted |
requires_shipping | Whether any line needs shipping |
currency | The store currency as an object — read currency.iso_code (e.g. INR) |
items_subtotal_price_cents | Line subtotal before cart discounts, in paise |
total_discount_cents | Total cart discount, in paise |
total_price_cents | Cart total, in paise |
amount_due_cents | Amount payable after gift cards / store credit |
Mutation responses also include the cart again under a cart key and an
issues array of platform-specific warnings (for example inventory caps), plus
the optional sections map.
Error shape
Validation failures return HTTP 400 with a structured error instead of a cart.
The fields member is an array of objects (field, rule, message):
{ "error": "validation_failed", "fields": [ { "field": "id|item|items", "rule": "required", "message": "A variant id or CartItemInput is required." } ]}Other error classes you should handle:
| Status | error | When |
|---|---|---|
400 | validation_failed | Missing/invalid body (see fields) |
400 | invalid_sections_url | sections_url is not a same-store path |
401 | customer_required | A logged-in customer is required for the operation |
403 | csrf_missing / csrf_mismatch / csrf_invalid / csrf_origin_blocked | CSRF token, cookie signature, or origin failed |
502 | cart_service_unavailable | The cart backend could not be reached |
Inventory problems — such as adding more than is in stock — come back as a descriptive message. Show it inline near the control the shopper used, not as a page-level alert.
Next steps
- The
cartandline_itemLiquid objects — the same data this API returns, available in your templates. - Predictive Search API for type-ahead search suggestions.
- Product Recommendations API for “you may also like” and cart cross-sells.
- Theme architecture and Section schema for building the sections you render through this API.