Using JavaScript to manage a Shopify cart

Shopify theme developers are probably well aware that you can use JavaScript and AJAX to manage a Shopify cart, but us Shopify app developers aren't in the loop. After all, while the endpoints for managing a cart from a Shopify storefront are documented, they don't document everything -- a common theme when it comes to Shopify's documentation, sadly. In fact, some of the best documentation that I could find for this feature isn't even Shopify's own documentation page, it's a Shopify store with interactive examples.

While that website is an excellent resource (I still have it bookmarked myself and often refer to it), it's only showcasing the jQuery functions found on Shopify's jQuery script -- a script that isn't even loaded by default on every Shopify store these days. While jQuery was an incredible package back in its hay day, most JS developers probably don't need it anymore. If I had to choose between including jQuery just for the cart management functions, or using fetch and calling the endpoints directly, I'd opt for the latter and save some kilobytes in the process.

To that end, I've gone spelunking through the jQuery functions and network calls, and then compared them to the somewhat sparse documentation Shopify has already published. I've documented all of the cart management endpoints on this page, and I've added examples for all of them using plain old JavaScript. Just remember that fetch isn't supported in Internet Explorer 11, so if you need to support that browser you'll need to use the whatwg-fetch polyfill.

I'm going to be publishing a package for using these endpoints very soon, alongside TypeScript declarations for all of the calls and objects. This post will be updated as soon as I've completed the package.

Before you read on, note two things:

  1. This is not documentation for Shopify's JS Buy SDK -- that's something entirely different, and you can find ample documentation for that right here.
  2. The endpoints documented below cannot be used from a different domain. They rely on cookies and must be used on the storefront. This is not a method for replacing the Shopify storefront with your own thing and just making calls to the cart endpoints. If you want to replace the storefront, you need to create a storefront app or use the JS Buy SDK.

The basics

Shopify's cart system has a few "gotchas" that developers should know about.

First, and perhaps most notably, the line items within don't have a unique id assigned to them (well, they do have an id, but it's the id of the product/variant, not a line item id). Instead, these cart endpoints all rely on you either telling Shopify the id of the product variant you have in the cart, or which "line" of the cart you're referring to. To make things more confusing, the lines are one-based instead of the more traditional zero-based that you're probably used to as a developer.

Second, each line item in a cart can be assigned custom properties. These custom properties will be returned whenever you get the cart, and will be carried through to checkout where the merchant can view them. This is how you attach extra data from custom fields on a product page, such as a name, a delivery date, an image, and so on. These properties will be visible to the customer in the shopping cart and during checkout, but you can hide them from the customer by starting the key with an underscore. The merchant will see them no matter what.

Third, since there is no unique id to differentiate line items in the cart, Shopify uses the custom line item properties to do so instead. If you add Product A to the cart with no properties two times, you'll wind up with one line item of Product A but with a quantity of 2. However, if you add Product A to the cart, and then do it again but with custom properties, you'll have two line items of Product A, each with a quantity of one (and one of them will have those custom properties).

Fourth, if you inspect the cart object you'll see that it has a token string property. This sounds like it can be used to identify a cart, but you should never rely on it. The value is regenerated on every page load, even if nothing has changed. It cannot be used to identify or differentiate a cart.

Fifth, you can't use the Checkout REST API with this shopping cart. That API is for custom storefront apps; it is not for managing the cart on a traditional Shopify storefront.

The cart object

With those caveats aside, we need to know what the cart actually looks like. This is an example of the data you'll get back when you get the cart (documented below):

{
    "token": "f069c6f70f0daa2db4f484b26b667bb7",
    "note": null,
    "attributes": {},
    "original_total_price": 15600,
    "total_price": 15600,
    "total_discount": 0,
    "total_weight": 1814.3695,
    "item_count": 4,
    "items": [{
        "id": 12637545431151,
        "properties": null,
        "quantity": 4,
        "variant_id": 12637545431151,
        "key": "12637545431151:afffd90f55318cfd435a26710b03a9d3",
        "title": "Photobook",
        "price": 3900,
        "original_price": 3900,
        "discounted_price": 3900,
        "line_price": 15600,
        "original_line_price": 15600,
        "total_discount": 0,
        "discounts": [],
        "sku": "",
        "grams": 454,
        "vendor": "Mediaclip Test Shop",
        "taxable": true,
        "product_id": 1390093500527,
        "gift_card": false,
        "final_price": 3900,
        "final_line_price": 15600,
        "url": "\/products\/test-product?variant=12637545431151",
        "image": "https:\/\/cdn.shopify.com\/s\/files\/1\/0016\/6861\/2207\/products\/sample16.png?v=1533932076",
        "handle": "test-product",
        "requires_shipping": true,
        "product_type": "",
        "product_title": "Photobook",
        "product_description": "",
        "variant_title": null,
        "variant_options": ["Default Title"],
        "line_level_discount_allocations": []
    }],
    "requires_shipping": true,
    "currency": "USD",
    "items_subtotal_price": 15600,
    "cart_level_discount_applications": []
}

I'm including some TypeScript types for the cart below, since I'm a big TypeScript user myself. If you don't use TypeScript, skip down to the next section!

export interface LineItem {
    id: number;
    title: string;
    price: number;
    line_price: number;
    quantity: number;
    sku?: string;
    grams: number;
    vendor: string;
    properties?: { [key: string]: string };
    variant_id: number;
    gift_card: boolean;
    url: string;
    image: string;
    handle: string;
    requires_shipping: boolean;
    product_title: string;
    product_description: string;
    product_type: string;
    variant_title: string;
    variant_options: string[];
}

export interface Cart {
    token: string;
    note: string;
    attributes: { [key: string]: string };
    total_price: number;
    total_weight: number;
    item_count: number;
    requires_shipping: boolean;
    currency: string;
    items: LineItem[];
}

Getting the cart and all of the items in it

Getting the shopping cart is probably the simplest thing you can do. There are no parameters or querystring values that will change the output. You make a request to /cart.json and you'll get the cart object (documented directly above this) back:

URL: /cart.json

Method: GET

Returns: The cart object.

async function getCart() {
    const result = await fetch("/cart.json");

    if (result.status === 200) {
        return result.json();
    }

    throw new Error(`Failed to get request, Shopify returned ${result.status} ${result.statusText}`);
}

// Example
const cart = await getCart();

Adding items to the cart

Items can be added to the shopping cart by making a POST request to /cart/add.json. You need to attach the ID of the line item being added to the cart, and the quantity.

Property Description Type Required
id ID of the Shopify variant being added to the cart string Yes
quantity Total quantity of the new line item being added to the cart number Yes
properties Extra information about the line item. The merchant will see these values in the Shopify admin dashboard. object No

URL: /cart/add.json

Method: POST

Returns: the line item that was just added to the cart.

async function addItem(variantId, quantity) {
    const result = await fetch("/cart/add.json", {
        method: "POST",
        headers: {
            "Content-Type": "application/json",
            "Accept": "application/json"
        },
        body: JSON.stringify({
            id: variantId,
            quantity: quantity
        })
    })
}

// Example
const lineItem = await addItem(123456, 1);

You can optionally attach properties to the line item. Remember that, as described above, these properties will make the line item "unique" -- if two line items have identical properties (or no properties), adding an item will just increase the quantity of the existing item. If the properties are not identical, you'll get two separate line items.

async function addItem(variantId, quantity, properties = {}) {
    const result = await fetch("/cart/add.json", {
        method: "POST",
        headers: {
            "Content-Type": "application/json",
            "Accept": "application/json"
        },
        body: JSON.stringify({
            id: variantId,
            quantity: quantity,
            properties: properties
        })
    })

    return cart.json();
}

// Example
const lineItem = await addItem(123456, 1, { foo: "bar" });
console.log(lineItem.properties); // { foo: "bar" }

Updating an item in the cart

Shopify lets you modify any item in the shopping cart by making a POST request to /cart/change.json. You can easily change a line item's quantity here, or modify its line item properties.

Parameter Description Required
quantity The line item's new quantity Yes
properties The line item's properties No, but any existing properties will be erased if left out
id A variant ID, used to determine which line item you're referring to Yes, if line is not present
line A one-based index, used to determine which line item you're referring to Yes, if id is not present

URL: /cart/change.json

Method: POST

Returns: the entire cart object.

async function updateItem(data) {
    const result = await fetch("/cart/change.json", {
        method: "POST",
        headers: {
            "Content-Type": "application/json",
            "Accept": "application/json"
        },
        body: JSON.stringify(data)
    });

    return cart.json();
}

function updateItemById(lineItemId, newQuantity, newProperties = {}) {
    return updateItem({
        id: lineItemId,
        quantity: newQuantity,
        properties: newProperties
    });
}

// Example
const result = await updateItemById(123456, 2, { foo: "bar" });
console.log(result.items); // [{quantity: 2, properties: { foo: "bar" }, ...}]

// Example 2
const result2 = await updateItemById(123456, 3);
console.log(result2.items); // [{quantity: 3, properties: null, ...}]

There's a little bit of a tricky interaction when you have more than one line item in the cart with the same variant id (i.e. they're the same variant but have different properties, so they're appear as different line items). You need to use a slightly different method for updating them, which is to specify which "line" you're referring to.

The line is just a simple integer, e.g. 1, 2, 3 meaning line 1, line 2 and line 3, respectively. To put it into a programming context, the lines are just an index in an array. Unlike the more traditional zero-based arrays that most developers are probably familiar with, though, the Shopify cart array is one-based -- it starts at 1 instead of 0.

Let's say you have two line items in the cart, each with a name property attached which makes the unique. They look something like this (with extra properties removed for this example):

[
    {
        id: 123456,
        quantity: 1,
        properties: {
            name: "John Doe"
        }
    },
    {
        id: 123456,
        quantity: 1,
        properties: {
            name: "Jane Doe"
        }
    }
]

If you wanted to update the quantity of the "Jane Doe" line item, you can't use the method for updating by id, because both line items have the same variant id. If you were to do that, you'd actually change the quantity of both line items. Instead, you need to tell Shopify which "line" is being changed. In this case, since the "Jane Doe" line item is the second one in the array, you'd use line: 2.

Remember, if you don't pass in the properties when changing a line item, they will be erased.

function updateItemByLine(oneBasedLineIndex, newQuantity, newProperties = {}) {
    return updateItem({
        line: oneBasedLineIndex,
        quantity: newQuantity,
        properties: newProperties
    });
}

// Example
const result = await updateItemByLine(2, 2, { name: "Jane Doe" });
console.log(result.items); 
// [{id: 123456, quantity: 1, properties: { name: "John Doe" }}, {id: 123456, quantity: 2, { name: "Jane Doe" }}]

⚠ If you have two or more items in the cart with the same variant id, and you then try to update the quantity by variant id, you will receive an HTTP 400 Bad Request error. You must instead update them using the line method.

Removing items from the cart

Items can be removed from the Shopify cart by making a call to the same /cart/change.json that the update method uses, but instead you set the quantity to 0. And just like the update method, you may need to specifiy the line number rather than a variant id, if you have two or more line items with the same variant id in the cart.

Parameter Description Required
quantity Must be set to 0 to remove the item Yes
id A variant ID, used to determine which line item you're referring to Yes, if line is not present
line A one-based index, used to determine which line item you're referring to Yes, if id is not present

URL: /cart/change.json

Method: POST

Returns: the entire cart object.

function removeItem(data) {
    const result = await fetch("/cart/change.json", {
        method: "POST",
        headers: {
            "Content-Type": "application/json",
            "Accept": "application/json"
        },
        body: JSON.stringify({
            ...data,
            quantity: 0
        })
    });

    return result.json();
}

function removeItemById(id) {
    return removeItem({ id: id });
}

function removeItemByLine(oneBasedLineIndex) {
    return removeItem({ line: oneBasedLineIndex });
}

⚠ If you have two or more items in the cart with the same variant id, and you then try to remove items by that variant id, you will receive an HTTP 400 Bad Request error. You must instead remove them using the line method.

Setting cart notes

Sometimes the shop customer needs to add notes to their order, but it doesn't make sense to attach the notes to a specific line item. Instead, you can use the cart's note property, which the merchant will see once the order has been placed.

Warning: make sure you preserve any existing cart note! Your app or script may not be the only thing setting notes on the cart.

Parameter Description Required
note Notes attached to an entire cart/order Yes

URL: /cart/update.json

Method: POST

Returns: the entire cart object.

async function setNote(note) {
    const result = await fetch("/cart/update.json", {
        method: "POST",
        headers: {
            "Content-Type": "application/json",
            "Accept": "application/json"
        },
        body: JSON.stringify({
            note: note
        })
    });

    return result.json();
}

// Example
let cart = await setNote("Hello world");
console.log(cart.note) // "Hello world"

cart = await setNote("Foo");
console.log(cart.note) // "Foo"

cart = await setNote(cart.note + ". Don't erase the last note.");
console.log(cart.note) // "Foo. Don't erase the last note."

Setting cart attributes

Cart attributes are just like line item properties, except they're used to store information about the whole order instead of just one line item. However, unlike line item properties and the cart note, merchants cannot see cart attributes. These attributes are largely for scripts and apps to store/read extra information about the order.

Just like the cart note, keep in mind that your script or app may not be the only one using cart attributes! If you overwrite attributes set by another script or app, that script or app may not work correctly. Merchants install these things for a reason, and they won't be happy if your tool breaks their other tools.

Parameter Description Required
attributes An object containing extra information to attach to the cart/order Yes

URL: /cart/update.json

Method: POST

Returns: the entire cart object.

async function setCartAttributes(attributes = {}) {
    const result = await fetch("/cart/update.json", {
        method: "POST",
        headers: {
            "Content-Type": "application/json",
            "Accept": "application/json"
        },
        body: JSON.stringify({
            attributes: attributes 
        })
    });

    return result.json();
}

// Example
let cart = await setCartAttributes({ foo: "bar" }); 
console.log(cart.attributes) // { foo: "bar" }

cart = await setCartAttributes({ hello: "world ");
console.log(cart.attributes) // { hello: "world" }

cart = await setCartAttributes({ ...cart.attributes, foo: "bar" });
console.log(cart.attributes) // { hello: "world", foo: "bar" }

Uploading a file and saving it to line item properties

It's possible to attach a file to a line item by uploading it to the line item properties. While the other line item properties appear as plain text, file properties will be turned into clickable links. The customer can click them in the cart or during checkout (as long as the property wasn't hidden), and the merchants can click them in their Shopify dashboard.

Clickable file link

Unlike the other endpoints, though, you'll need to post the data as a form rather than JSON. To set the line item property in a form, you need to have a properties[prop_name] in the form. Shopify will upload your file to their CDN, and only files from that CDN will be clickable during checkout or in the merchant's Shopify dashboard; external URLs and CDNs are not clickable.

This example assumes you have a file selector input on the storefront, and the customer has already selected a file with it. If you're using this in your own script, you'll obviously need to perform your own validation.

async function addToCartWithFile(variantId, file) {
    const data = new FormData();
    data.append("properties[file]", file);
    data.append("id", variantId);

    const result = await fetch("/cart/add.json", {
        method: "POST",
        body: data
    });

    return result.json();
}

// Example
const input = document.querySelector("#file-input");
const newLineItem = await addToCartWithFile(123456, input.files[0]);

console.log(newLineItem.properties.file);
// https://cdn.shopify.com/s/files/1/0040/7092/uploads/6ed984c1eca14abcfe11e4b56958dd21.jpg

You can add files and images to existing line items by using the /cart/change.json endpoint. Remember that omitting a property or quantity here will remove it completely, so you're responsible for preserving values.

async function changeItem(data) {
    const result = await fetch("/cart/change.json", {
        method: "POST",
        body: data
    });

    return result.json();
}

function appendFileToLineById(variantId, quantity, file) {
    const data = new FormData();
    data.append("id", variantId);
    data.append("properties[file]", file);
    data.append("quantity", quantity);

    return changeItem(data);
}

function appendFileToLineByIndex(oneBasedLineIndex, quantity, file) {
    const data = new FormData();
    data.append("line", oneBasedLineIndex);
    data.append("properties[file]", file);
    data.append("quantity", quantity);

    return changeItem(data);
}

Clearing the cart

You can clear the cart and empty out all of the line items by making a POST call to /cart/clear.json. This accepts no parameters, and it will not clear the cart attributes or cart note.

URL: /cart/clear.json

Method: POST

Returns: the entire cart object.

async function clearCart() {
    const result = await fetch({
        method: "POST",
        headers: {
            "Accept": "application/json"
        }
    });

    return result.json();
}

Updating the quantities or properties of multiple items at once

It's possible to update the quantities of multiple line items at once, by making a POST call to /cart/update.json and passing in an object containing an array of quantities. This endpoint superficially looks very similar to the /cart/change.json endpoint, but there are two major differences:

  1. The update endpoint will not only change the quantities of items in the cart, but it will also add any items in the array that aren't already in the cart. The change endpoint would ignore missing items.
  2. The update endpoint cannot be used to change line item properties.
Parameter Description Required
updates An array containing either objects with ids and quantities, or numbers representing the new quantities of each line Yes

URL: /cart/update.json

Method: POST

Returns: the entire cart object.

The updates array can contain either objects with ids and quantities:

const data = {
    updates: [
        { id: 123, quantity: 1 },
        { id: 234, quantity: 2 }
    ]
}

or the updates array can contain numbers, where the number at index 0 refers to the line item at index 0, the element at index 1 refers to the line item at index 1, and so on.

const data = {
    // Set the quantity of the first line item to 1, the second line item to 2, and the third line item to 3
    updates: [1, 2, 3]
}

And here it is, all tied together:

async function updateQuantities(data) {
    const result = await fetch({
        method: "POST",
        headers: {
            "Content-Type": "application/json",
            "Accept": "application/json"
        },
        bodoy: JSON.stringify({ updates: data });
    });
}

// Example
let cart = await updateQuantities([{id: 123, quantity: 2}, { id: 234, quantity: 4 }]);
console.log(cart.items) // [{ id: 123, quantity: 2, ... }, { id: 234, quantity: 4, ... }]

cart = await updateQuantities([4, 2]);
console.log(cart.items) // [{ id: 123, quantity: 4, ... }, { id: 234, quantity: 2, ... }]

Like the change endpoint, setting the quantity to 0 will remove items from the cart. This is an easy way to quickly remove multiple items without clearing the cart entirely.

cart = await updateQuantities([0, 3]);
console.log(cart.items) // [{id: 234, quantity: 3, ...}]

Getting shipping rates

If there are items in the cart, you can query to see the available shipping rates by making a GET request to /cart/shipping_rates.json. You'll need to include the destination address in the querystring, or else the call will fail with 422 Unprocessable Entity and an error object that looks like {"error": ["Invalid shipping address"]}.

Parameter Description Required
shipping_address[zip] The shipping destination's ZIP or postal code. Yes
shipping_address[country] The shipping destination's country. Can be either a full name (United States) or two-digit code (US). Yes
shipping_address[province] The shipping destination's province or state. Can be either a full name (Iowa) or two-digit code (IA). Yes

URL: /cart/shipping_rates.json

Method: POST

Returns: An object containing an array of shipping rates

async function getShippingRates(zip, state, country) {
    const url = encodeURI(`shipping_address[zip]=${zip}&shipping_address[country]=${country}&shipping_address[province]=${state}`);
    const result = await fetch(`/cart/shipping_rates.json?${url}`, {
        method: "GET",
        headers: {
            "Accept": "application/json"
        }
    });

    return result.json();
}

// Example
const rates = await getShippingRates("12345", "Minnesota", "United States");
console.log(rates.shipping_rates);
// [{name: "First Class Package", price: "2.66", code: "FirstPackage", ...}, ...]

One thing you should watch for: if the cart does not require shipping, you'll receive a 422 Unprocessable Entity and an error object that looks like {"error": ["This cart does not require shipping"]}. You'll either need to write your function to handle 422 errors, or loop through the items in the cart to check if any require shipping before making the call to this endpoint.

async function cartRequiresShipping() {
    const cart = await fetch("/cart.json", {
        method: "GET",
        headers: {
            "Accept": "application/json"
        }
    });

    return cart.items.some(i => i.requires_shipping === true);
}

And once again, because I'm a big fan of TypeScript and use it often, here are the type definitions for the entire shipping rates response:

interface GetShippingRatesResponse {
    shipping_rates: ShippingRate[];
}

export interface ShipmentOption {
    code: string;
    name: string;
    description: string;
    amount: number;
    details: Details;
}

export interface Details {
    insured_amount?: number;
}

export interface ChargeItem {
    group: string;
    code: string;
    name: string;
    description: string;
    subtotal: string;
    taxes: string;
    total: string;
    details: Details;
    tax_items: unknown[];
}

export interface ShippingRate {
    name: string;
    presentment_name: string;
    code: string;
    price: string;
    markup: string;
    source: string;
    delivery_date: string;
    delivery_range: string[];
    delivery_days: number[];
    compare_price: string;
    phone_required: boolean;
    currency: string;
    carrier_identifier?: unknown;
    delivery_category?: unknown;
    using_merchant_account: boolean;
    carrier_service_id: unknown;
    description?: unknown;
    api_client_id?: unknown;
    requested_fulfillment_service_id?: unknown;
    shipment_options: ShipmentOption[];
    charge_items: ChargeItem[];
    has_restrictions: boolean;
    rating_classification: string;
}

Get a product

Your script can get the JSON data for a product by making a GET request to /products/PRODUCT-HANDLE.json. The response will include almost all data that an app would get if it made the same request via the API; the only difference is that scripts cannot get product metafields.

Parameter Description Required
{handle} The product's handle. This is not the product's name or title, but it's URL handle. Example: my-fabulous-product Yes

URL: /products/${handle}.json

Method: GET

Returns: The product object

async function getProduct(handle) {
    const result = await fetch(`/products/${handle}.json`, {
        headers: {
            "Content-Type": "application/json",
            "Accept": "application/json"
        }
    });

    return result.json();
}

// Example
const product = await getProduct("my-fabulous-product");
console.log({ title: product.title, handle: product.handle });
// { title: "My Fabulous Product", handle: "my-fabulous-product" }

Checking whether a discount code is valid

While it is impossible to get discount objects or details about a discount code (such as the amount, usage count, etc.) from storefront scripts, it is possible to check if a discount code exists. Check out this post here which explains it in more depth.


Learn how to build rock solid Shopify apps with C# and ASP.NET!

Did you enjoy this article? I wrote a premium course for C# and ASP.NET developers, and it's all about building rock-solid Shopify apps from day one.

Enter your email here and I'll send you a free sample from The Shopify Development Handbook. It'll help you get started with integrating your users' Shopify stores and charging them with the Shopify billing API.

We won't send you spam. Unsubscribe at any time.