# Products and pricing

URL: https://docs.sparklayer.io/developers/javascript-sdk/products-and-pricing

Show each B2B customer's own price, RRP and price breaks in your theme, price a quantity as it changes, and swap the product image when a variant is selected.

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

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.

| Method | Use it to |
| --- | --- |
| [`getPricingForVariant()`](https://docs.sparklayer.io/developers/javascript-sdk/reference/methods.md#getpricingforvariant) | Get a variant's price, RRP and price breaks for the logged-in customer. |
| [`getPriceForProduct()`](https://docs.sparklayer.io/developers/javascript-sdk/reference/methods.md#getpriceforproduct) | Get a product's "from" price across its variants, and how many prices there are. |
| [`calculatePricingForVariant()`](https://docs.sparklayer.io/developers/javascript-sdk/reference/methods.md#calculatepricingforvariant) | Price a quantity of a variant, including price breaks. |
| [`calculatePricingForVariantData()`](https://docs.sparklayer.io/developers/javascript-sdk/reference/methods.md#calculatepricingforvariantdata) | Price a quantity from variant data you already have, without a request. |
| [`getPackSizeForVariant()`](https://docs.sparklayer.io/developers/javascript-sdk/reference/methods.md#getpacksizeforvariant) | Get a variant's pack size (`1` if none is set). |
| [`getRrpPriceForVariant()`](https://docs.sparklayer.io/developers/javascript-sdk/reference/methods.md#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](https://docs.sparklayer.io/developers/javascript-sdk/setup.md#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:

```javascript title="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`](https://docs.sparklayer.io/developers/javascript-sdk/reference/types.md#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:

```html title="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>
```

```javascript title="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:

```liquid title="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()`](https://docs.sparklayer.io/developers/javascript-sdk/reference/methods.md#getrrppriceforvariant) or the `<spark-product-rrp>` [component](https://docs.sparklayer.io/developers/javascript-sdk/reference/events-and-components.md#web-components).
- 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.

```html title="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>
```

```javascript title="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:

```javascript title="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:

```javascript title="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

- [Cart](https://docs.sparklayer.io/developers/javascript-sdk/cart.md): Add products to the cart from your own buttons and forms, rounded to pack sizes.
- [Events and analytics](https://docs.sparklayer.io/developers/javascript-sdk/events-and-analytics.md): The hooks and DOM events you can run your own code from.
- [Pricing methods](https://docs.sparklayer.io/developers/javascript-sdk/reference/methods.md#products-and-pricing): Every product and pricing method, and the types they return.
