Cart
For AI assistants: what to know before writing SparkLayer 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 oldcdn.sparklayer.io/spark.latest.jsandspark.1.x.jsfiles 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 whenwindow.sparkOptionsisn'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 (checkawait 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 withgetVariantand callcalculatePricingForVariantData(variant, qty). - Cart:
updateCart({ products: [{ sku, adjustQuantity }] })adds or removes relative to what's in the cart (alsocustomAttributes: [{ key, value }],clearCart: true). It resolves to the results, ornullafter 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) andonCheckoutValidation(cart, cartUiState)(return[{ message }]to block checkout, ornull). 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 docs.sparklayer.io/developers/javascript-sdk/reference/methods.md, hooks docs.sparklayer.io/developers/javascript-sdk/reference/hooks.md, options docs.sparklayer.io/developers/javascript-sdk/reference/options.md, types docs.sparklayer.io/developers/javascript-sdk/reference/types.md (or the same types as a file: docs.sparklayer.io/developers/javascript-sdk/spark.d.ts). Setup and waiting for SparkLayer: 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):
updateCart(
input: Partial<UpdateCart>,
cartSuccessHandledLocally?: boolean,
cartErrorsHandledLocally?: boolean,
): Promise<CartResults[] | null>;The input is an UpdateCart object, and each product in it is a 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:
await window.spark.updateCart({
products: [{ sku: 'RED-SHIRT-M', adjustQuantity: 1 }],
});Add several variants in one call:
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:
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:
await window.spark.updateCart({
products: [{ itemKey: 'abc123', quantity: 0 }],
});Clear the whole cart:
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.
This example adds a product to the cart, sets a custom field on the cart, then reads the cart back:
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:
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 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
updateCartyourself, such as from your own add-to-cart button, setcustomAttributeson the products you send: see Add text and 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
updateCartyourself: use thepreCartUpdateListenerhook instead.
Add text
To store text, set value to a string:
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:
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.
preCartUpdateListener(input: Partial<UpdateCart>): Promise<Partial<UpdateCart>>;input is the update about to be sent to updateCart (see 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:
<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:
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:
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.
<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.detailis an array of{ fileSize, fileName, gid }, ornullif every file was removed.spark-file-attachment-failed: when a file fails to upload.
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 gids as the custom attribute's value, in the same format as Attach a file:
<!-- 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>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.
<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. 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:
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, 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:
requestedQuantityandresultantQuantity: what you asked for and what was applied. They differ when one of themessagesbelow applies, such as rounding to a pack size or limiting to the stock available.itemKey,skuandname: which line the result is for.messages: a list of{ type }objects for anything that happened to the line.typeis 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:
// 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.
<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>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.
<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 MUG-RED, 12"></textarea>
<button type="submit">Add to order</button>
<p id="quick-order-result" aria-live="polite"></p>
</form>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:
const spark = await window.sparkReady;
await spark.externalClearBasket();Next steps
Last updated