Skip to content

Products and pricing

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.

Show B2B prices in your own theme code, work out the price for a quantity, and keep your product images in step with SparkLayer's variant picker. This page uses the pricing methods below and the spark-variant-change event.

MethodUse it to
getPricingForVariant()Get a variant's price, RRP and price breaks for the logged-in customer.
getPriceForProduct()Get a product's "from" price across its variants, and how many prices there are.
calculatePricingForVariant()Price a quantity of a variant, including price breaks.
calculatePricingForVariantData()Price a quantity from variant data you already have, without a request.
getPackSizeForVariant()Get a variant's pack size (1 if none is set).
getRrpPriceForVariant()Get a variant's RRP on its own.

Each method takes your platform's parent product ID and the variant SKU. Prices are for the logged-in customer, so check isLoggedIn() first. Some examples use window.sparkReady: add the Wait for SparkLayer snippet before them.

Calculate the price for a quantity

This example calculates the price of two units of a variant once SparkLayer is ready:

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

    // Parent product ID, variant SKU and quantity
    const pricing = await spark.calculatePricingForVariant('6660191387839', 'MUG-MOM', 2);
    console.log(pricing.totalPrice, pricing.unitPrice, pricing.currencyCode);
  },
};

totalPrice is the quantity × the unit price at that quantity, after any price breaks. See ProductVariantPricing for every field.

Show trade prices on your own product tiles

Your theme's collection tiles show retail prices. This swaps in each signed-in B2B customer's own price, with the RRP, and leaves retail visitors' prices alone. Give each tile its product ID and SKU:

Product tile (Shopify Liquid)
<div class="tile" data-product-id="{{ product.id }}" data-sku="{{ product.selected_or_first_available_variant.sku }}">
  <span class="tile-price">{{ product.price | money }}</span>
  <span class="tile-rrp"></span>
</div>
Trade prices on tiles
const money = (amount, currency) =>
  new Intl.NumberFormat(document.documentElement.lang || 'en-GB', {
    style: 'currency',
    currency,
  }).format(amount);

async function showTradePrices(root = document) {
  const spark = await window.sparkReady;
  if (!(await spark.isLoggedIn())) return; // retail visitors keep your normal prices

  const tiles = root.querySelectorAll('[data-product-id][data-sku]');
  await Promise.all(
    [...tiles].map(async (tile) => {
      try {
        const { price, rrp, currencyCode } = await spark.getPricingForVariant(
          tile.dataset.productId,
          tile.dataset.sku,
        );
        if (price === null) return; // no B2B price for this customer
        tile.querySelector('.tile-price').textContent = money(price, currencyCode);
        if (rrp) tile.querySelector('.tile-rrp').textContent = `RRP ${money(rrp, currencyCode)}`;
      } catch {
        // the product isn't available to this customer: leave the tile as it is
      }
    }),
  );
}

showTradePrices();

Call showTradePrices(container) again after your theme loads more products, for example on infinite scroll.

Read the RRP metafield in Liquid

On Shopify, you can also render the RRP on the server, straight from the variant's sparklayer.rrp metafield. The metafield is JSON on the variant, with one entry per currency and decimal values (not subunits), for example [{"value":9.00,"currency_code":"gbp"}]. This snippet picks the entry for the shopper's currency, converts it to subunits for Shopify's money filter, and only shows it when it differs from the variant's Shopify price:

Product template (Shopify Liquid)
{%- if customer.metafields.sparklayer.authentication -%}
  {%- assign variant = product.selected_or_first_available_variant -%}
  {%- for entry in variant.metafields.sparklayer.rrp.value -%}
    {%- assign entry_currency = entry.currency_code | upcase -%}
    {%- if entry_currency == cart.currency.iso_code -%}
      {%- assign rrp_subunits = entry.value | times: 100.0 | round -%}
      {%- if rrp_subunits != variant.price -%}
        <span class="b2b-rrp">RRP {{ rrp_subunits | money }}</span>
      {%- endif -%}
      {%- break -%}
    {%- endif -%}
  {%- endfor -%}
{%- endif -%}
  • Liquid is rendered once, on the server. The RRP doesn't change when the customer picks another variant until the page reloads. For an RRP that follows the selected variant, use getRrpPriceForVariant() or the <spark-product-rrp> component.
  • Shopify caches pages, so a change to the metafield can take a little while to show.

Live price as the quantity changes

On a product page with your own quantity box, show the unit price, line total and price-break saving as the customer types, snapped to the product's pack size. It fetches the variant once, then works out each price instantly with calculatePricingForVariantData, so there's no request per keystroke.

Product page
<input id="b2b-qty" type="number" min="1" value="1" data-product-id="{{ product.id }}" data-sku="{{ product.selected_or_first_available_variant.sku }}" />
<p id="b2b-line-price" aria-live="polite"></p>
Live quantity pricing
const qtyInput = document.querySelector('#b2b-qty');
const output = document.querySelector('#b2b-line-price');
const { productId, sku } = qtyInput.dataset;

const money = (amount, currency) =>
  new Intl.NumberFormat(document.documentElement.lang || 'en-GB', {
    style: 'currency',
    currency,
  }).format(amount);

const spark = await window.sparkReady;
const [variant, packSize] = await Promise.all([
  spark.getVariant(productId, sku),
  spark.getPackSizeForVariant(productId, sku),
]);
qtyInput.step = String(packSize);
qtyInput.min = String(packSize);

function render() {
  // round up to a whole number of packs
  const qty = Math.max(packSize, Math.ceil((Number(qtyInput.value) || 1) / packSize) * packSize);
  const pricing = spark.calculatePricingForVariantData(variant, qty);
  if (pricing.unitPrice === null) {
    output.textContent = 'Price on request';
    return;
  }
  const saving = pricing.priceBreakSavingsPercentage
    ? ` (you save ${pricing.priceBreakSavingsPercentage}%)`
    : '';
  output.textContent = `${qty} × ${money(pricing.unitPrice, pricing.currencyCode)} = ${money(
    pricing.totalPrice,
    pricing.currencyCode,
  )}${saving}`;
}

qtyInput.addEventListener('input', render);
qtyInput.addEventListener('change', () => {
  qtyInput.value = String(Math.ceil((Number(qtyInput.value) || 1) / packSize) * packSize);
  render();
});
render();

Load this as a module (<script type="module">), so await works at the top level.

Update the product image when a variant is selected

When a customer picks a variant in the spark-pdp or spark-product-card interface, SparkLayer doesn't change your theme's product image. You can do it with a little theme code:

  1. Listen for the spark-variant-change event on each spark-pdp or spark-product-card element.
  2. In the listener, show the image for the selected variant and hide the others.

Listen for the event

spark-variant-change is dispatched on the interface element itself and doesn't bubble, so add a listener to each element. Register the listeners in onReady, once the elements are on the page:

sparkOptions
window.sparkOptions = {
  ...window.sparkOptions,
  onReady: async () => {
    for (const productCard of document.querySelectorAll('spark-product-card')) {
      productCard.addEventListener('spark-variant-change', (event) => {
        console.log(event.detail.product.externalId, event.detail.variant.externalId);
      });
    }
  },
};

event.detail has:

  • event.detail.product.externalId: your platform's ID for the product (for example, the Shopify product ID).
  • event.detail.variant.externalId: your platform's ID for the selected variant.

Elements added to the page later, such as by infinite scroll, need their own listeners.

Show the variant's image

How you swap images depends on your theme's markup. This approach was tested with Shopify's Dawn theme:

  1. Render every variant's image in the product card, for example by looping through the product's variants in the Liquid template. Hide all but the first variant's image, which is selected by default, with style="opacity: 0".
  2. Add a data-variant-id attribute to each variant image, set to the variant ID.
  3. In the listener, hide every variant image in the card and show the one for the selected variant:
sparkOptions
window.sparkOptions = {
  ...window.sparkOptions,
  onReady: async () => {
    for (const productCard of document.querySelectorAll('spark-product-card')) {
      productCard.addEventListener('spark-variant-change', (event) => {
        const cardEl = event.target.closest('.card-wrapper');
        if (!cardEl) {
          return;
        }

        const variantId = event.detail.variant.externalId;

        // Hide all variant images
        for (const img of cardEl.querySelectorAll('img[data-variant-id]')) {
          img.style.opacity = '0';
        }

        // Show the selected variant's image
        const variantImg = cardEl.querySelector(`img[data-variant-id="${variantId}"]`);
        if (variantImg) {
          variantImg.style.opacity = '1';
        }
      });
    }
  },
};

The same code works for spark-pdp: change the selector and the wrapper class to match your product page.

Next steps

Was this page helpful?

Last updated