Skip to content

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 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 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):

Type
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:

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:

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 updateCart yourself, such as from your own add-to-cart button, set customAttributes on 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 updateCart yourself: use the preCartUpdateListener hook 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.

Type
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:

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:

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:

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.

Product page
<spark-file-upload-field
  id="shirt-design-upload"
  allowed-extensions='["png", "jpg", "jpeg"]'
></spark-file-upload-field>
AttributeDescription
allowed-extensionsThe file extensions to allow, as a JSON array. Defaults to the most common file extensions.
max-file-sizeMaximum file size, in bytes.
max-filesMaximum number of files.
requiredWhether 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.
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 gids as the custom attribute's value, in the same format as Attach a file:

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>
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.

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. 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:

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, 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:
TypeMeaning
successThe line was added or updated with no issues.
pack-sizeThe quantity was rounded down to the nearest pack size.
qty-excessiveThe quantity was lowered to the allowed maximum.
qty-insufficientThe quantity was raised to the allowed minimum.
qty-unavailableThe quantity was lowered to what's in stock.
qty-unavailable-no-clampThe line was added, but the requested quantity isn't fully available. Check the cart.
excessive-linesThe cart's line limit was reached, so the item wasn't added.
not-foundThe SKU doesn't exist.
unavailableThe product can't be bought at the moment.
product-settings-overriddenThe 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.

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>
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.

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>
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:

Order confirmation page
const spark = await window.sparkReady;
await spark.externalClearBasket();

Next steps

Was this page helpful?

Last updated