# Cart

URL: https://docs.sparklayer.io/developers/javascript-sdk/cart

Add, update and remove cart items with updateCart, set PO numbers and cart fields, attach custom attributes and files, and open the cart drawer from code.

> **For AI assistants:** what to know before writing SparkLayer JavaScript SDK code.
>
> - **What it is:** `window.spark`, added to the storefront by the SparkLayer Core Script (version 4.x). On Shopify the SparkLayer app embed loads it; elsewhere it's `<script async src="https://sparkcdn.io/sparkjs/<SITE_ID>/<live|test>"></script>` in the theme's `<head>`. The old `cdn.sparklayer.io/spark.latest.js` and `spark.1.x.js` files are legacy: never use them.
> - **Options:** Configure it with `window.sparkOptions`, set before the Core Script loads. Extend the object rather than replacing it: `window.sparkOptions = { ...window.sparkOptions, … }`, or the Site ID and other snippets' hooks are lost. Headless only: `window.initSpark(options)` is defined when `window.sparkOptions` isn't set.
> - **When to call it:** Only after SparkLayer is ready: inside the `onReady(spark)` hook, or after a promise resolved from it. Most methods are async; prices and the cart are only available to signed-in B2B customers (check `await spark.isLoggedIn()`).
> - **Products and prices:** Products are looked up by the platform's parent product ID (on Shopify, `product.id`) and variants by SKU: `getPricingForVariant(parentProductId, sku)`, `calculatePricingForVariant(parentProductId, sku, qty)`, `getPackSizeForVariant(parentProductId, sku)`. For instant recalculation, fetch once with `getVariant` and call `calculatePricingForVariantData(variant, qty)`.
> - **Cart:** `updateCart({ products: [{ sku, adjustQuantity }] })` adds or removes relative to what's in the cart (also `customAttributes: [{ key, value }]`, `clearCart: true`). It resolves to the results, or `null` after showing its own error message; it doesn't throw. `getCart()` reads the cart; `openDrawer('cart')` opens it.
> - **Hooks:** `onCartUpdate(cart, results)`, `onCartLoad(cart)`, `onLogout()`, `preCartUpdateListener(input)` (async: return the possibly changed input) and `onCheckoutValidation(cart, cartUiState)` (return `[{ message }]` to block checkout, or `null`). When adding a hook, call any existing one first.
> - **Customising:** Build around SparkLayer or customise its own components, including their HTML. Prefer documented options, hooks, CSS variables and custom slots where they fit; keep direct HTML changes targeted and re-check them after SparkLayer updates.
> - **Docs:** The reference: methods https://docs.sparklayer.io/developers/javascript-sdk/reference/methods.md, hooks https://docs.sparklayer.io/developers/javascript-sdk/reference/hooks.md, options https://docs.sparklayer.io/developers/javascript-sdk/reference/options.md, types https://docs.sparklayer.io/developers/javascript-sdk/reference/types.md (or the same types as a file: https://docs.sparklayer.io/developers/javascript-sdk/spark.d.ts). Setup and waiting for SparkLayer: https://docs.sparklayer.io/developers/javascript-sdk/setup.md. Use only what the reference documents.

Change the B2B cart from your own code: add, update and remove items, read the cart back, set cart-level fields, attach custom attributes and files, and open the cart drawer. This page uses `updateCart`, `getCart`, `getCartCache`, `uploadFile`, `openDrawer` and `externalClearBasket`, and the `preCartUpdateListener` and `onCartUpdate` hooks.

`window.spark.updateCart` makes every change to the cart. Call it once SparkLayer is ready (see [Wait for SparkLayer to be ready](https://docs.sparklayer.io/developers/javascript-sdk/setup.md#wait-for-sparklayer-to-be-ready)):

```typescript title="Type"
updateCart(
  input: Partial<UpdateCart>,
  cartSuccessHandledLocally?: boolean,
  cartErrorsHandledLocally?: boolean,
): Promise<CartResults[] | null>;
```

The input is an [`UpdateCart`](https://docs.sparklayer.io/developers/javascript-sdk/reference/types.md#updatecart) object, and each product in it is a [`CartItemInput`](https://docs.sparklayer.io/developers/javascript-sdk/reference/types.md#cartiteminput). Send only the fields you want to change.

## Add, update and remove items

Add a variant by SKU with `adjustQuantity`, which **increments** whatever quantity is already in the cart:

```javascript
await window.spark.updateCart({
  products: [{ sku: 'RED-SHIRT-M', adjustQuantity: 1 }],
});
```

Add several variants in one call:

```javascript
await window.spark.updateCart({
  products: [
    { sku: 'RED-SHIRT-M', adjustQuantity: 2 },
    { sku: 'BLUE-HAT-L', adjustQuantity: 1 },
  ],
});
```

Use `quantity` instead of `adjustQuantity` to **set** the line to an exact quantity rather than add to it, for example from a quantity input on the cart page:

```javascript
await window.spark.updateCart({
  products: [{ sku: 'RED-SHIRT-M', quantity: 5 }],
});
```

### Remove an item or clear the cart

Set `quantity` to `0` to remove a line. If you have the cart line's `itemKey` (from `getCart()`, for example), use it instead of the SKU:

```javascript
await window.spark.updateCart({
  products: [{ itemKey: 'abc123', quantity: 0 }],
});
```

Clear the whole cart:

```javascript
await window.spark.updateCart({ clearCart: true });
```

## Read the cart

`updateCart` returns the result for each line, not the cart. To read the cart, call `getCart()`, which fetches it, or `getCartCache()`, which returns the cached copy without a request (or `null` if there isn't one yet). Both return a [`Cart`](https://docs.sparklayer.io/developers/javascript-sdk/reference/types.md#cart).

This example adds a product to the cart, sets a custom field on the cart, then reads the cart back:

```javascript title="sparkOptions"
window.sparkOptions = {
  ...window.sparkOptions,
  onReady: async (spark) => {
    if (!(await spark.isLoggedIn())) {
      return;
    }

    // Add 3 of a variant to the cart
    const results = await spark.updateCart({
      products: [{ sku: 'BRACE-COLOURED', adjustQuantity: 3 }],
    });
    console.log(results); // What was applied to each line

    // Set a custom field on the cart
    await spark.updateCart({
      customFields: [{ name: 'season', value: 'in-season' }],
    });

    console.log(await spark.getCart()); // The updated cart
  },
};
```

## Cart-level fields

Update `customerReference`, `poNumber`, `shippingRequestedDate` and `customFields` the same way, with or without `products`:

```javascript
await window.spark.updateCart({
  customerReference: 'This is a note on the order',
  poNumber: 'PO-4567',
});
```

`customerReference`, `poNumber` and `shippingRequestedDate` hold SparkLayer's standard checkout fields, and `customFields` holds your own.  To show these fields to customers at checkout, see [Checkout fields](https://docs.sparklayer.io/help/ordering/checkout-fields.md) in the Help Center.

## Custom attributes and files

Any product line can carry `customAttributes`: free-form key and value pairs that stay with the line through the cart and into the order, such as engraving text or a fabric choice. The `key` and `value` are shown to the customer and your staff under the line item.

- If you call `updateCart` yourself, such as from your own add-to-cart button, set `customAttributes` on the products you send: see [Add text](#add-text) and [Attach a file](#attach-a-file).
- To add attributes when a customer uses one of SparkLayer's own add-to-cart buttons (in the product detail or product card interface), you don't call `updateCart` yourself: use the [`preCartUpdateListener` hook](#add-attributes-to-sparklayers-add-to-cart) instead.

### Add text

To store text, set `value` to a string:

```javascript
await window.spark.updateCart({
  products: [
    {
      sku: 'SHIRT-CUSTOM',
      adjustQuantity: 1,
      customAttributes: [
        { key: 'Shirt Length', value: '32in' },
        { key: 'Shirt Fabric', value: 'Cotton' },
      ],
    },
  ],
});
```

### Attach a file

To attach a file, use the same array, but set `value` to a JSON string of an array of file GIDs. Upload the file with `spark.uploadFile()` to get its GID:

```javascript
const gid = await window.spark.uploadFile('design.png', fileBlob);

await window.spark.updateCart({
  products: [
    {
      sku: 'SHIRT-CUSTOM',
      adjustQuantity: 1,
      customAttributes: [
        { key: 'Design', value: JSON.stringify([gid]) },
      ],
    },
  ],
});
```

> **Match the file name to the file type**
>
> The file name you pass to `uploadFile()` (`design.png` above) must match the type of the data in `fileData`: don't name a JPEG `design.png`. A mismatch fails a check on the server after upload, the file is deleted, and the GID you got back stops working. Any later lookup of it errors, including reading the cart.

### Add attributes to SparkLayer's add to cart

`preCartUpdateListener` is a hook that runs just before one of SparkLayer's built-in add-to-cart buttons calls `updateCart`. It lets you change the update before it's sent: most often, to add custom attributes or files to the products.

```typescript title="Type"
preCartUpdateListener(input: Partial<UpdateCart>): Promise<Partial<UpdateCart>>;
```

`input` is the update about to be sent to `updateCart` (see [`UpdateCart`](https://docs.sparklayer.io/developers/javascript-sdk/reference/types.md#updatecart)), not the current cart: it has no `items` or `totals`, only the products that interface is about to add. Return the update, changed or not, from an `async` function: SparkLayer sends whatever you return.

### Collect customisations from the page

The most common use is to read form fields on the product page and attach their values as `customAttributes` to the products being added. Tag each relevant `input` and `select` with `data-spark-custom-attributes`:

```html title="Product page"
<label for="shirt-length">Shirt Length</label>
<input id="shirt-length" name="Shirt Length" data-spark-custom-attributes />

<label for="shirt-fabric">Shirt Fabric</label>
<select id="shirt-fabric" name="Shirt Fabric" data-spark-custom-attributes>
  <option value="cotton">Cotton</option>
  <option value="polyester">Polyester</option>
</select>
```

Then collect the tagged fields in `preCartUpdateListener`. Each field's label (or its `name`, if it has no label) becomes the attribute's `key`:

```javascript title="sparkOptions"
window.sparkOptions = {
  ...window.sparkOptions,
  preCartUpdateListener: async (input) => {
    if (!input.products?.length) {
      return input;
    }

    const nodes = document.querySelectorAll(
      'input[data-spark-custom-attributes], select[data-spark-custom-attributes]',
    );

    const handledRadioGroups = new Set();
    const attrs = [];

    nodes.forEach((el) => {
      const type = (el.type || '').toLowerCase();

      if (type === 'radio') {
        if (handledRadioGroups.has(el.name)) return;
        handledRadioGroups.add(el.name);
        el = document.querySelector(
          `input[name="${CSS.escape(el.name)}"][data-spark-custom-attributes]:checked`,
        );
        if (!el) return; // nothing selected in this radio group
      } else if (type === 'checkbox' && !el.checked) {
        return;
      }

      const labelEl = el.id ? document.querySelector(`label[for="${CSS.escape(el.id)}"]`) : null;
      const key =
        labelEl?.textContent.trim().replace(/:$/, '') ||
        el.name;
      const value =
        el.tagName === 'SELECT'
          ? el.options[el.selectedIndex]?.textContent.trim()
          : el.value.trim();

      if (key && value) {
        attrs.push({ key, value });
      }
    });

    if (!attrs.length) {
      return input;
    }

    input.products.forEach((item) => {
      item.customAttributes = [...(item.customAttributes ?? []), ...attrs];
    });

    return input;
  },
};
```

For one or two fixed fields, it's simpler to read them directly:

```javascript title="sparkOptions"
window.sparkOptions = {
  ...window.sparkOptions,
  preCartUpdateListener: async (input) => {
    if (!input.products?.length) {
      return input;
    }

    const shirtLength = document.querySelector('#shirt-length')?.value;
    const shirtFabric = document.querySelector('#shirt-fabric')?.value;
    if (!shirtLength && !shirtFabric) {
      return input;
    }

    const customShirt = input.products.find((p) => p.sku === 'SHIRT-CUSTOM');
    if (!customShirt) {
      return input;
    }

    customShirt.customAttributes = customShirt.customAttributes ?? [];
    if (shirtLength) {
      customShirt.customAttributes.push({ key: 'Shirt Length', value: shirtLength });
    }
    if (shirtFabric) {
      customShirt.customAttributes.push({ key: 'Shirt Fabric', value: shirtFabric });
    }

    return input;
  },
};
```

### Let customers upload a file

Use the `spark-file-upload-field` web component to let customers upload a file, such as an image or design, on the product page. Then attach it with the same hook.

```html title="Product page"
<spark-file-upload-field
  id="shirt-design-upload"
  allowed-extensions='["png", "jpg", "jpeg"]'
></spark-file-upload-field>
```

| Attribute | Description |
| --- | --- |
| `allowed-extensions` | The file extensions to allow, as a JSON array. Defaults to the most common file extensions. |
| `max-file-size` | Maximum file size, in bytes. |
| `max-files` | Maximum number of files. |
| `required` | Whether a file must be uploaded. |

It dispatches two events, which bubble up to `window`:

- `spark-file-attachment-changed`: whenever the list of files changes. `event.detail` is an array of `{ fileSize, fileName, gid }`, or `null` if every file was removed.
- `spark-file-attachment-failed`: when a file fails to upload.

```javascript title="Listening for upload events"
window.addEventListener('spark-file-attachment-changed', (e) => {
  // e.detail is an array of the file details in the following format:
  // {
  //   fileSize: number,
  //   fileName: string,
  //   gid: string,
  // }

  // Handle files
});

window.addEventListener('spark-file-attachment-failed', (e) => {
  // e.detail format:
  // {
  //   fileSize: number,
  //   fileName: string,
  //   errorCode: string,
  // }
  //
  // errorCode can be one of the following:
  // - "unsupported-file-type"
  // - "file-too-large"
  // - "default"

  // Handle the failed file
});
```

`preCartUpdateListener` and the upload field usually live in different parts of your theme (`sparkOptions` in a layout file, the upload field in a product template), with no shared JavaScript scope. A hidden input is a simple way to pass the uploaded file data between them. Anything works, as long as `preCartUpdateListener` can read it when it runs.

Store the event detail in the hidden input, then use just the `gid`s as the custom attribute's `value`, in the same format as [Attach a file](#attach-a-file):

```html title="Product page"
<!-- holds the uploaded file data, written to and read back below -->
<input id="shirt-design" type="hidden" />

<script>
  {
    const uploadEl = document.querySelector('#shirt-design-upload'); // <-- id of the spark-file-upload-field above
    const inputEl = document.querySelector('#shirt-design');
    if (uploadEl && inputEl) {
      uploadEl.addEventListener('spark-file-attachment-changed', (e) => {
        inputEl.value = e.detail ? JSON.stringify(e.detail) : '';
      });
    }
  }
</script>
```

```javascript title="sparkOptions"
window.sparkOptions = {
  ...window.sparkOptions,
  preCartUpdateListener: async (input) => {
    if (!input.products?.length) {
      return input;
    }

    const filesJson = document.querySelector('#shirt-design')?.value; // <-- read back the hidden input from above
    if (!filesJson) {
      return input;
    }

    const gids = JSON.parse(filesJson).map((file) => file.gid);

    const customShirt = input.products.find((p) => p.sku === 'SHIRT-CUSTOM'); // <-- match your own custom-line-item SKU
    if (!customShirt) {
      return input;
    }

    customShirt.customAttributes = [
      ...(customShirt.customAttributes ?? []),
      { key: 'Front Design', value: JSON.stringify(gids) }, // <-- key is shown to the customer/staff as the attachment's label
    ];

    return input;
  },
};
```

### Tag order lines with a campaign

Record which campaign brought a buyer in, as a custom attribute on every line they add, so it shows on the order. It reads `utm_campaign` from the landing page URL and keeps it for the visit. It also calls any `preCartUpdateListener` you already have, so it works alongside the examples above.

```html title="Theme <head>, before the SparkLayer script"
<script>
  (function () {
    var campaign = new URLSearchParams(window.location.search).get('utm_campaign');
    if (campaign) sessionStorage.setItem('b2bCampaign', campaign);

    var options = (window.sparkOptions = window.sparkOptions || {});
    var previous = options.preCartUpdateListener;

    options.preCartUpdateListener = async function (input) {
      var update = previous ? await previous(input) : input;
      var tag = sessionStorage.getItem('b2bCampaign');
      if (!tag || !update.products) return update;

      update.products = update.products.map(function (product) {
        return Object.assign({}, product, {
          customAttributes: (product.customAttributes || []).concat([{ key: 'Campaign', value: tag }]),
        });
      });
      return update;
    };
  })();
</script>
```

## Open the cart drawer

`openDrawer()` opens the SparkLayer drawer, on the Cart tab unless you pass another [tab](https://docs.sparklayer.io/developers/javascript-sdk/reference/types.md#draweruistate). To open it each time the cart changes, call it from the `onCartUpdate` hook. This example also switches a drawer that's already open to the Cart tab:

```javascript title="sparkOptions"
window.sparkOptions = {
  ...window.sparkOptions,
  onCartUpdate: async (cart, results) => {
    // Opens the drawer if it isn't already open
    window.spark.openDrawer('cart');

    // Switches an open drawer to the Cart tab
    window.dispatchEvent(
      new CustomEvent('view-update', {
        bubbles: true,
        composed: true,
        detail: { view: 'cart' },
      }),
    );
  },
};
```

## Handle the result and show feedback

### What updateCart returns

`updateCart` resolves to an array of [`CartResults`](https://docs.sparklayer.io/developers/javascript-sdk/reference/types.md#cartresults), one for each product line you sent, or `null` if the request failed. It doesn't throw: on failure it shows an error toast and logs the error to the console.

Each result has:

- `requestedQuantity` and `resultantQuantity`: what you asked for and what was applied. They differ when one of the `messages` below applies, such as rounding to a pack size or limiting to the stock available.
- `itemKey`, `sku` and `name`: which line the result is for.
- `messages`: a list of `{ type }` objects for anything that happened to the line. `type` is one of:

| Type | Meaning |
| --- | --- |
| `success` | The line was added or updated with no issues. |
| `pack-size` | The quantity was rounded down to the nearest pack size. |
| `qty-excessive` | The quantity was lowered to the allowed maximum. |
| `qty-insufficient` | The quantity was raised to the allowed minimum. |
| `qty-unavailable` | The quantity was lowered to what's in stock. |
| `qty-unavailable-no-clamp` | The line was added, but the requested quantity isn't fully available. Check the cart. |
| `excessive-lines` | The cart's line limit was reached, so the item wasn't added. |
| `not-found` | The SKU doesn't exist. |
| `unavailable` | The product can't be bought at the moment. |
| `product-settings-overridden` | The product's default settings were overridden. |

These are status codes, not display text. `updateCart` uses them to show its own toasts (see below). If you show your own messages, switch on `type`.

### Show your own feedback

By default, `updateCart` shows toast messages. Pass `true` as the second argument (`cartSuccessHandledLocally`) to turn off the success toast, or as the third (`cartErrorsHandledLocally`) to turn off toasts for the `messages` in the results:

```javascript
// Suppress the built-in success toast, but keep automatic error toasts
await window.spark.updateCart(
  { products: [{ sku: 'RED-SHIRT-M', adjustQuantity: 1 }] },
  true, // cartSuccessHandledLocally
);
```

## Add to cart from your own button

Add a product to the B2B cart from any button, rounded up to its pack size, then open the cart drawer. SparkLayer shows its own messages if the quantity is changed, for example to match the stock available. This uses `window.sparkReady` from [Wait for SparkLayer](https://docs.sparklayer.io/developers/javascript-sdk/setup.md#wait-for-sparklayer).

```html title="Any page"
<button type="button" data-add-to-trade-order data-product-id="{{ product.id }}" data-sku="{{ product.selected_or_first_available_variant.sku }}" data-qty="12">
  Add 12 to order
</button>
```

```javascript title="Add to cart"
document.addEventListener('click', async (event) => {
  const button = event.target.closest('[data-add-to-trade-order]');
  if (!button) return;

  const spark = await window.sparkReady;
  const { productId, sku } = button.dataset;
  const packSize = await spark.getPackSizeForVariant(productId, sku);
  const quantity = Math.ceil(Number(button.dataset.qty || 1) / packSize) * packSize;

  button.disabled = true;
  try {
    const results = await spark.updateCart({ products: [{ sku, adjustQuantity: quantity }] });
    if (results) spark.openDrawer('cart'); // null means SparkLayer already showed an error
  } finally {
    button.disabled = false;
  }
});
```

The click listener sits on `document`, so it also works for buttons your theme adds later.

## Quick order from a pasted list

Let buyers paste a list of SKUs and quantities, one per line, such as `MUG-BLUE 24` or `MUG-BLUE,24`, and add everything in one update. Duplicate SKUs are added together.

```html title="Quick order form"
<form id="quick-order">
  <label for="quick-order-lines">SKUs and quantities, one per line</label>
  <textarea id="quick-order-lines" rows="8" placeholder="MUG-BLUE 24&#10;MUG-RED, 12"></textarea>
  <button type="submit">Add to order</button>
  <p id="quick-order-result" aria-live="polite"></p>
</form>
```

```javascript title="Quick order"
document.querySelector('#quick-order').addEventListener('submit', async (event) => {
  event.preventDefault();
  const text = document.querySelector('#quick-order-lines').value;
  const result = document.querySelector('#quick-order-result');

  const totals = new Map();
  const skipped = [];
  for (const line of text.split('\n').map((l) => l.trim()).filter(Boolean)) {
    const [sku, qty] = line.split(/[\s,;\t]+/);
    const quantity = Number.parseInt(qty ?? '1', 10);
    if (!sku || !Number.isFinite(quantity) || quantity < 1) {
      skipped.push(line);
      continue;
    }
    totals.set(sku, (totals.get(sku) ?? 0) + quantity);
  }
  if (!totals.size) {
    result.textContent = 'Add at least one SKU and quantity.';
    return;
  }

  const spark = await window.sparkReady;
  const products = [...totals].map(([sku, adjustQuantity]) => ({ sku, adjustQuantity }));
  const results = await spark.updateCart({ products });
  if (!results) return; // SparkLayer showed the error

  const added = results.filter((r) => r.resultantQuantity > 0).length;
  result.textContent = `Added ${added} of ${products.length} products.${
    skipped.length ? ` Couldn't read: ${skipped.join(', ')}.` : ''
  }`;
  spark.openDrawer('cart');
});
```

## Clear the cart after an order

If customers complete orders outside SparkLayer's checkout and your order confirmation page loads the Core Script, clear their B2B cart there:

```javascript title="Order confirmation page"
const spark = await window.sparkReady;
await spark.externalClearBasket();
```

## Next steps

- [Checkout](https://docs.sparklayer.io/developers/javascript-sdk/checkout.md): Check the cart against your own rules before the customer can check out.
- [Products and pricing](https://docs.sparklayer.io/developers/javascript-sdk/products-and-pricing.md): Show trade prices and live quantity pricing in your theme.
- [Cart types](https://docs.sparklayer.io/developers/javascript-sdk/reference/types.md#cart): The UpdateCart, CartItemInput, CartResults and Cart types in full.
