Whim SDK

whim.js

Drop-in script for any website. Web components, JS API, and cart management.

1. Install the SDK

Add the bootstrap snippet and script tag to your page.

<script>"use strict";(()=>{function I(r=window){let c=r.document,n=r.whim=r.whim||{};function a(){return r.whimConfig||{}}function f(e){let t=a();return t.sdkScriptMatch?e.indexOf(t.sdkScriptMatch)>-1:t.merchantId?e.indexOf("/v1/"+t.merchantId+".js")>-1:!1}!a().sdkScriptMatch&&!a().merchantId&&console.warn("Whim SDK: set whimConfig.merchantId before the snippet (see docs)");try{let e="whim-initial-url",t=r.sessionStorage.getItem(e);n.initialUrl=r.whimInitialUrl=t||r.location.href,t||r.sessionStorage.setItem(e,n.initialUrl)}catch(e){}n.isReady===void 0&&(n.isReady=!1),n.error===void 0&&(n.error=null);let d=n._q=n._q||[];function l(e,t,i,s){i.split(" ").forEach(function(o){e[o]||(e[o]=function(...u){if(n._settled)return Promise.resolve(null);let S,P=new Promise(function(N){S=N});return d.push([t+o,u,S]),P})}),s.split(" ").forEach(function(o){e[o]||(e[o]=function(...u){d.push([t+o,u])})})}l(n,"","getMode getProducts getCheckoutUrl getPrice getConfig getPortalUrl checkCustomer checkProducts","setMode setProduct setAddon removeAddon map"),l(n.ui=n.ui||{},"ui.","","ready refresh"),l(n.cart=n.cart||{},"cart.","add has get getCheckoutUrl count distinctCount getProductNames","setQuantity remove clear");let v=n._t0=n._t0||Date.now();n._v=3;let h=!1;function k(e){try{if(r.navigator&&r.navigator.onLine===!1)return"network";if(e&&r.performance&&r.performance.getEntriesByName){let t=r.performance.getEntriesByName(e),i=t[t.length-1];if(i&&(i.duration>50||i.transferSize>0))return"network"}}catch(t){}return"blocked"}function p(e){if(!e)return e;try{var t=e.search(/[?#]/);return t===-1?e:e.slice(0,t)}catch(i){return e}}function R(e,t){try{let i;try{i=!!c.hidden}catch(o){}if(a().disableErrorReporting)return;let s=a().errorEndpoint||"https://api.sdk.whim.com/api/whim/errors";if(!s||!r.navigator||!r.navigator.sendBeacon)return;r.navigator.sendBeacon(s,JSON.stringify({v:1,source:"snippet",reason:e,merchantId:a().merchantId,src:p(t),url:p(r.location.href),sinceSnippetMs:Date.now()-v,snippetVersion:3,hidden:i,tagInDom:y(),extended:h}))}catch(i){}}function m(e,t){if(r.whim!==n||n.isReady||n.error||n._settled)return;n._settled=!0;let i=new Error("Whim SDK load failed: "+e+(e==="timeout"?" after "+g+"ms":""));i.reason=e,t&&(i.src=t),n.error=i;for(let s=0;s<d.length;s++){let o=d[s][2];if(typeof o=="function")try{o(null)}catch(u){}}try{c.dispatchEvent(new CustomEvent("whim-error",{detail:i}))}catch(s){}R(e,t)}r.addEventListener("error",function(e){let t=e&&e.target;if(!t||t.tagName!=="SCRIPT")return;let i=t.src||"";f(i)&&m(k(i),i)},!0);let _=!1;c.addEventListener("load",function(e){let t=e&&e.target;t&&t.tagName==="SCRIPT"&&f(t.src||"")&&(_=!0,m("crashed",t.src))},!0);function y(){try{let e=c.scripts;for(let t=0;t<e.length;t++)if(f(e[t].src||""))return!0}catch(e){}return!1}let g=a().sdkTimeoutMs||5e3;function E(){if(!(r.whim!==n||n.isReady||n.error||n._settled)){if(!h&&(c.hidden||!_&&y())){h=!0,n._wd=r.setTimeout(E,g);return}m("timeout")}}n._wd&&r.clearTimeout(n._wd),n._wd=r.setTimeout(E,g)}I(window);})();</script>
<script async src="https://api.sdk.whim.com/v1/YOUR_MERCHANT_ID.js"></script>

Replace YOUR_MERCHANT_ID with the merchant ID we provide you (e.g. demo.js).

Recommended: also set window.whimConfig = { merchantId: 'YOUR_MERCHANT_ID' } before the bootstrap snippet runs. Without it, a blocked or failed SDK script is only caught by the slower watchdog timeout, not instantly — see Error handling below.

To turn on debug logging while integrating, set window.whimConfig = { debug: true } before the script tag — the SDK logs every state change and event to the console.

Browser support

Whim JS requires Custom Elements and ES2015: Chrome 67+, Edge 79+, Firefox 63+, Safari 10.1+, and their mobile equivalents. Legacy Edge and Internet Explorer are not supported. In an unsupported browser the SDK does nothing to your page: your markup renders as-is and whim-error fires with reason: 'unsupported-browser' where the script can run at all.

Add this to your page's <head>, before any code that uses the SDK.

Add this to your index.html (or HTML shell). The web components work directly in JSX with React 19+.

Tip: You can call SDK methods before the script loads. The snippet queues everything and replays it on init.
  • Getters and cart data methods return promises. Use await or .then():
var price = await whim.getPrice();
var inCart = await whim.cart.has('sku-123');
var added = await whim.cart.add('sku-123');
  • Setters and ui.* calls don't return anything:
whim.setMode('whim');
whim.setProduct('sku-123');
whim.ui.ready(function() { renderMyUi(); });
  • Events are DOM CustomEvents on document (whim-price-change, whim-cart-change, …). Listen with document.addEventListener; it works before the SDK loads.
  • whim.isReady is a boolean. Starts false when the snippet runs, becomes true once the SDK loads.
document.addEventListener('whim-price-change', function(e) { renderPrice(e.detail); });

if (whim.isReady) renderMyUi();
else whim.ui.ready(renderMyUi);

2. Set up your product page

Since customers can now use Whim on your products, you'll need a way for them to pick between buying and Whim on the product page. The SDK manages that choice. Whim mode pulls pricing from Whim and redirects checkout to us, buy mode is untouched.

Here's what that looks like on a product page using web components. You drop these into your existing product detail layout. The JS API is also available if you need more control.

Tip: These components wrap your existing HTML. Your product title, images, and layout stay as-is. You just add the Whim elements around the parts that need buy/whim behavior.
1

Set the product

<whim-selector> tells the SDK which product to load. Everything else goes inside it.

2

Add the buy/whim toggle

<whim-option> lets shoppers switch between buy and whim mode. The SDK handles the state, selection styling, and accessibility.

3

Display the price

<whim-price> shows the Whim price in whim mode. Set buy-price to show your buy price when in buy mode.

4

Handle checkout

<whim-checkout-button> wraps your existing CTA. Redirects to Whim checkout in whim mode.

Preview
Wireless Headphones
Black
$249
<!-- your existing product page markup -->
<div class="product-detail">
  <img src="/images/wireless-headphones.jpg" alt="Wireless Headphones" />
  <h1>Wireless Headphones</h1>
  <p class="color">Black</p>

  <!-- Whim components go inside your layout -->
  <whim-selector product="wireless-headphones-black">

    <!-- your own toggle markup, Whim handles the click + state -->
    <div class="purchase-options">
      <whim-option mode="buy">Buy</whim-option>
      <whim-option mode="whim">Rent</whim-option>
    </div>

    <!-- standalone price display -->
    <whim-price buy-price="$249" suffix="/mo" />

    <!-- wraps your existing CTA -->
    <whim-checkout-button>
      <a href="/cart/add?sku=sku-123456" class="btn">Add to Cart</a>
    </whim-checkout-button>

  </whim-selector>
</div>
// ProductPage.jsx
export default function ProductPage({ product }) {
  return (
    <div className="product-detail">
      <img src={product.image} alt={product.name} />
      <h1>{product.name}</h1>
      <p className="color">{product.color}</p>

      {/* Whim components go inside your layout */}
      <whim-selector product={product.whimId}>

        {/* your own toggle markup, Whim handles the click + state */}
        <div className="purchase-options">
          <whim-option mode="buy">Buy</whim-option>
          <whim-option mode="whim">Rent</whim-option>
        </div>

        {/* standalone price display */}
        <whim-price buy-price={product.priceFormatted} suffix="/mo" />

        {/* wraps your existing CTA */}
        <whim-checkout-button>
          <button className="btn" onClick={() => addToCart(product.sku)}>
            Add to Cart
          </button>
        </whim-checkout-button>

      </whim-selector>
    </div>
  );
}

Components render in light DOM, so your existing CSS applies to them. See Components for all attributes and styling options.

Tip: When you switch modes, all price displays and checkout buttons update automatically. You don't need to manually re-render anything after calling setMode().

3. Product mapping (optional)

If your site uses its own product identifiers (internal SKUs, variant IDs), register a mapping so you don't have to scatter Whim product names through your code:

whim.map({
  'sku-123456': 'wireless-headphones-black',
  'sku-789012': 'wireless-headphones-silver',
});

// Now use your own IDs everywhere
whim.setProduct('sku-123456');
whim.cart.add('sku-789012');
whim.cart.has('sku-123456'); // true

Once mapped, your IDs work everywhere: setProduct, cart.add, cart.remove, cart.has. Unmapped IDs pass through as-is, so you can mix Whim product names and your own.

Tip: If your site has variant selectors (color, size, etc.), hook into those change events and call whim.setProduct() with the new SKU. The SDK updates pricing and checkout URLs automatically.

Using the JS API instead

You can also skip the components and wire everything up yourself with the window.whim JS API. Here's the same buy/whim toggle, price, and checkout done in plain JS:

<button id="buy">Buy</button>
<button id="rent">Rent</button>
<span id="price"></span>
<button id="checkout">Add to Cart</button>

<script>
whim.setProduct('wireless-headphones-black');

whim.ui.ready(function() {
  async function render() {
    var mode = await whim.getMode();
    var price = await whim.getPrice();

    document.getElementById('price').textContent =
      mode === 'whim' && price ? price.priceFormatted + '/mo' : '$249';

    document.getElementById('buy').style.fontWeight = mode === 'buy' ? '700' : '400';
    document.getElementById('rent').style.fontWeight = mode === 'whim' ? '700' : '400';
  }

  document.getElementById('buy').onclick = function() { whim.setMode('buy'); };
  document.getElementById('rent').onclick = function() { whim.setMode('whim'); };

  document.getElementById('checkout').onclick = async function() {
    if (await whim.getMode() === 'whim') {
      var url = await whim.getCheckoutUrl();
      if (url) window.location.href = url;
      return;
    }
    // your buy checkout logic
  };

  document.addEventListener('whim-mode-change', render);
  document.addEventListener('whim-price-change', render);
  render();
});
</script>

You can also mix both — use components where they fit and the JS API for anything custom. See the API Reference for the full list of methods, events, and types.

Tip: The JS API is always available, even if you're using components. Call whim.getPrice() or listen to whim-mode-change alongside web components to build custom UI that stays in sync.

API Reference

All methods are available on window.whim. The SDK loads async, and calls made before it loads are queued and replayed automatically. Getters return promises that resolve once the SDK is ready, so you can await them or use .then(). You can also use whim.ui.ready() for callback-style initialization.

With the JS API you write your own HTML, listen to events, and render prices and checkout however you want. Good for when you need to match an existing PDP layout exactly. The Web Components are built on this same API and can be mixed in.

Everything is event-driven. Call setProduct() or setMode() to change state, listen for mode-change or price-change to update your UI. No polling, no manual re-renders.

Core

whim.setProduct(name)

Sets the active product for price lookups and checkout URLs. If you registered a product map, you can pass your own ID here.

whim.setProduct('wireless-headphones-black');

// Or with a mapped ID
whim.map({ 'sku-123456': 'wireless-headphones-black' });
whim.setProduct('sku-123456');

Emits product-change with the resolved WhimProductConfig and price-change with the resolved WhimPrice. If the product name isn't in your program, emits product-unavailable (status: 'missing'), marks the product out of stock, falls back to buy mode if rent mode was active, and leaves the active product unchanged. It is not an SDK error: whim.error stays null.


whim.getProducts()

Resolves with the full product catalog for your merchant.

var products = await whim.getProducts();
// [{ productName: 'wireless-headphones-black', isOutOfStock: false, price: { amount: 2900, ... }, discountPercent: 20 }, ...]

Returns Promise<WhimProductConfig[]>


whim.getMode()

Resolves with the current mode, either 'buy' or 'whim'. Defaults to 'buy'.

var mode = await whim.getMode(); // 'buy' | 'whim'

Returns Promise<'buy' | 'whim'>


whim.setMode(mode)

Switches between buy and whim. Emits mode-change.

whim.setMode('whim');

whim.isReady

A synchronous read-only boolean. The bootstrap snippet seeds it as false the moment it runs, and the SDK flips it to true once the bundle has loaded and the API is callable. Use it to branch without registering a callback or awaiting anything.

if (whim.isReady) {
  renderPrice();
} else {
  whim.ui.ready(renderPrice);
}

boolean (property — not a method, do not call or await)


whim.getPrice()

Resolves with the Whim price for the current product, or null before setProduct is called. The SDK only provides Whim pricing — buy pricing is yours.

var price = await whim.getPrice();
// {
//   price: 60,                   // ongoing monthly price
//   priceFormatted: '$60',
//   currency: 'USD',
//   introDiscount: {             // null if no discount
//     price: 30,
//     priceFormatted: '$30',
//     percent: 50,
//     months: 3
//   }
// }

if (price.introDiscount) {
  // "$30/mo for 3 months, then $60/mo"
  el.textContent = price.introDiscount.priceFormatted + '/mo for '
    + price.introDiscount.months + ' months, then '
    + price.priceFormatted + '/mo';
} else {
  el.textContent = price.priceFormatted + '/mo';
}

Returns Promise<WhimPrice | null>

introDiscount is null when the product has no intro discount.


whim.getCheckoutUrl()

Resolves with the Whim checkout URL for the current product and addons, built on your checkoutBaseUrl. Resolves null before setProduct is called and whenever the current product is out of stock or unavailable, so a custom button can never send a shopper to checkout for something they can't rent.

var url = await whim.getCheckoutUrl();
// 'https://rent.your-store.com/your-merchant-id/checkout?products=wireless-headphones-black'

Returns Promise<string | null>


whim.getConfig()

Resolves with the raw merchant config object.

var config = await whim.getConfig();
// { merchantId: 'demo', currency: 'USD', products: [...] }

Returns Promise<WhimMerchantConfig>


whim.getPortalUrl()

Resolves with the customer portal URL where subscribers manage their subscriptions.

var url = await whim.getPortalUrl(); // 'https://my.whim.com'

Returns Promise<string>


whim.checkCustomer(identifier)

Checks if a customer with that email or phone number already has a Whim account. Returns a promise. You can use this to conditionally render a portal link for returning customers that are already logged in to your site.

Pass exactly one identifier — either { email } or { phone }. To check a customer who may have signed up with either, call it once per identifier.

// By email
var result = await whim.checkCustomer({ email: 'user@example.com' });

// By phone — E.164 with country code
var result = await whim.checkCustomer({ phone: '+15551234567' });

if (result.exists) {
  // Returning subscriber, show portal link
  window.location.href = await whim.getPortalUrl();
}
Phone format: Pass phone numbers in E.164 format with the country code — e.g. +15551234567. Matching is exact on the stored number, so any formatting characters (spaces, dashes, parentheses) will cause a miss. A bare national number like 5551234567 also matches US accounts, but E.164 is unambiguous across regions and always recommended.

Returns Promise<{ exists: boolean }>


whim.checkProducts(names, options?)

Checks availability for each product in names. If a lazy catalog has not loaded a product yet, the SDK fetches it before returning. You get one entry per requested name, in order, including the negatives.

Use this when a listing page, grid, or search result needs availability before it renders — or wants to sort or badge on it. getProducts() only returns products already in memory. checkProducts() uses the same batched, deduplicated loader as the price elements, so a page's worth of names costs the fetch those elements were going to make anyway.

var productNames = ['camera-a', 'camera-b', 'lens-c'];
var statuses = await whim.checkProducts(productNames, { timeoutMs: 1500 });

var visibleProductNames = statuses === null
  ? productNames // SDK failed to load — show everything
  : statuses
      .filter(function (status) {
        return !status.isKnownUnavailable;
      })
      .map(function (status) {
        return status.name;
      });

Each entry carries:

Filter on isKnownUnavailable, not on status. It is deliberately false when availability could not be determined, so an unreachable API leaves your listing intact. Assembling the decision yourself from status === 'success' silently hides every product during an outage.
Bound the wait when this gates rendering. Without timeoutMs, the call waits on the price request, which is bounded only by priceFetchTimeoutMs (8s by default) — far longer than a listing should hold its paint. Names that time out come back as status: 'error'; products that arrive afterwards still land in the catalog and are returned by later calls.
Configurable products: the batch price API resolves a configurable parent to its default variant, so a parent's price and isOutOfStock describe that variant rather than aggregating across all of them. Where variant-level accuracy matters, resolve the product itself with <whim-selector> instead of reading it from a listing status.

Returns Promise<WhimProductStatus[] | null>. Results match the order of names; duplicate names yield duplicate entries. A null result means the SDK failed to load.


whim.map(record)

Registers a mapping from your product identifiers to Whim product names. After calling this, you can use your own IDs in setProduct, cart.add, cart.remove, cart.has, etc.

whim.map({
  'sku-123456': 'wireless-headphones-black',
  'sku-789012': 'wireless-headphones-silver',
});

whim.setProduct('sku-123456'); // resolves to 'wireless-headphones-black'
whim.cart.add('sku-789012');   // resolves to 'wireless-headphones-silver'

Can be called multiple times. New entries merge with previous ones. Unmapped IDs pass through as-is, so you can mix Whim product names and your own.


whim.setAddon(key, value)

Adds an addon to the current product. Addons are included in the checkout URL.

whim.setAddon('case', 'hard-shell');
whim.setAddon('warranty', 'extended');
// checkout URL: ...?products=product-name(addons(case=hard-shell,warranty=extended))

Emits addon-change.


whim.removeAddon(key)

Removes a previously set addon. Emits addon-change.

whim.removeAddon('case');

UI Helpers

whim.ui.ready(fn)

Calls fn immediately if the SDK is loaded, or queues it until it is. Use it to wrap any code that depends on the SDK API being callable.

whim.ui.ready(async function() {
  var products = await whim.getProducts();
  console.log('SDK ready, products:', products.length);
});
Tip: If you're using the bootstrap queue, you can also just await getter methods directly instead of wrapping in ui.ready(). ui.ready is still useful for callback-style code or when you need to run a block of setup logic together.

whim.ui.refresh()

Triggers a refresh event, causing all <whim-price> and <whim-checkout-button> components to re-render. Call this after client-side page navigation in an SPA.

whim.ui.refresh();

Events

Every SDK event is a CustomEvent dispatched on document, named whim-<event>, with the payload in e.detail. This is the only subscription surface: it needs nothing from the SDK, so a listener registered before the script loads simply fires once the SDK emits. Unsubscribe with removeEventListener, { once: true }, or an AbortSignal.

document.addEventListener('whim-mode-change', function(e) {
  console.log('Mode is now:', e.detail);
});

// Cleanup in SPAs / React effects
var ac = new AbortController();
document.addEventListener('whim-price-change', renderPrice, { signal: ac.signal });
// later: ac.abort();
TypeScript: the SDK's .d.ts augments DocumentEventMap, so e.detail is typed for every whim-* event.

Event types

EventPayloadWhen
ready{ price: WhimPrice | null, checkoutUrl: string | null, product: WhimProductConfig | null }Once after the SDK bundle has loaded and the API is callable. Payload fields are populated when a product is active and null otherwise.
mode-change'buy' | 'whim'After setMode
price-changeWhimPriceAfter setProduct resolves pricing
product-changeWhimProductConfigEvery time setProduct() successfully resolves a product, including the first call.
product-unavailable{ name: string, productName: string, status: 'missing' | 'error', error: Error | null }When setProduct() can't select the product. 'missing': the catalog confirmed it isn't in the program (the SDK has already marked it out of stock and, if rent mode was active, switched to buy). 'error': the price never loaded, so availability is unknown. Not an SDK failure: whim.isReady stays true and whim.error stays null.
variant-changeRecord<string, string>After a <whim-variant-option> selection updates the active variant attributes
addon-changeRecord<string, string>After setAddon or removeAddon
catalog-updateWhimProductConfig[]When lazily-fetched product prices merge into the catalog. The payload is the batch of products that just loaded. Only fires for merchants on a lazy catalog.
errorWhimSdkErrorOnly when the SDK itself fails to load, initialize, or reach ready. Never fires for a product that can't be selected (that's product-unavailable). See Error handling.
refreshnoneAfter ui.refresh()
cart-changeWhimCartChangeEventAfter any cart mutation (add, setQuantity, remove, clear) and when a lazily-loaded price completes a held line. Not fired for the persisted cart restored on page load: read whim.cart.count() in ui.ready for the initial badge. See whim-cart-change.

whim-added-to-cart

A bubbling DOM CustomEvent dispatched from a <whim-cart-add> element when a click on it successfully adds to the cart. The detail is { product, quantity } — the element's product attribute value and the quantity added. Useful for one-shot UI like toasts or animations that should only fire on a real add.

document.addEventListener('whim-added-to-cart', function(e) {
  showToast(e.detail.product + ' added to cart');
});

Not fired when the add was dropped (debounced double-click, unknown product, out of stock), and not fired by programmatic whim.cart.add() or setQuantity calls — only by <whim-cart-add> clicks. Adding a product that's already in the cart increments its quantity and does fire the event. For ongoing cart state, listen to cart-change instead.

Error handling

You don't have to write any error handling to use the SDK safely. If the script fails to load (blocked by an ad blocker, network failure) or fails to initialize, the SDK guarantees:

If you want to react to failures explicitly — e.g. hide a subscription badge or show fallback UI — use whim.error or the whim-error DOM event (which works even before the SDK loads). They mean exactly one thing: the SDK is not working. On a healthy page whim.error is null and whim-error never fires, no matter which products you select.

document.addEventListener('whim-error', function(e) {
  console.warn('Whim SDK failed:', e.detail.message, e.detail.reason || e.detail.phase);
  // Load failures (the script never ran, or ran without initializing) carry e.detail.reason:
  //   'blocked' | 'network' | 'timeout' | 'crashed'
  // Init failures (the bundle ran and failed at a step) carry e.detail.phase:
  //   'config' | 'preflight' | 'store' | 'cart' | 'api' | 'lazy-loader' | 'elements' | 'replay' | 'ready'
  // One case carries both: an unsupported browser is phase 'preflight' with reason 'unsupported-browser'.
  hideSubscriptionUi();
});

A product that can't be selected is not an SDK failure and does not travel on this channel. When setProduct() is called with a product that isn't in the program, the SDK marks it out of stock (elements render data-out-of-stock), falls back to buy mode if rent mode was active, and fires product-unavailable (whim-product-unavailable on document) with the same status vocabulary as checkProducts(). Listen to that if you want to react per product:

document.addEventListener('whim-product-unavailable', function(e) {
  // e.detail.status: 'missing' (not in the program) | 'error' (price never loaded)
  if (e.detail.status === 'missing') showBuyOnlyBadge(e.detail.name);
});

How failures are detected

FailureDetectione.detail
Script blocked (ad blocker, CSP)Snippet: script error event, settled immediatelyreason: 'blocked'
Network failure (offline, DNS, reset)Snippet: script error event, settled immediatelyreason: 'network'
Stalled / silent failureSnippet: watchdog timer (default 5s, see below)reason: 'timeout'
Script downloaded and ran, but the SDK never initializedSnippet: script load event with window.whim still the stub and no init error recorded, settled immediatelyreason: 'crashed'
Browser lacks Custom Elements (legacy Edge, pre-2018 engines)Bundle: checked before anything is built, settled immediatelyphase: 'preflight', reason: 'unsupported-browser'
The bundle ran and failed at a step (bad config, store, cart, API, lazy loader, elements, queue replay, ready)Bundle: init's own try/catch, settled immediatelyphase naming the step

The watchdog measures from the moment the bootstrap snippet runs. If it fires while the SDK <script> tag is present in the DOM but still downloading (for example, your own delayed loader injected it moments earlier, or a hidden tab flushed several overdue timers at once), it grants one extension of sdkTimeoutMs instead of reporting a false timeout. The same single extension applies if the watchdog fires while the tab is hidden. Worst case is therefore 2× sdkTimeoutMs, and pending calls still always settle. If you inject the SDK from your own delayed loader, size sdkTimeoutMs to cover the delay plus a realistic download.

About crashed. A classic script executes before its load event fires, so by the time load arrives the bundle has either initialized (it replaced window.whim) or failed a step (it recorded whim.error itself). If neither happened, the file ran without ever reaching our init: a captive portal or proxy answering the script URL with HTML and a 200, a browser too old to parse the bundle (a SyntaxError still fires load), a broken deploy, or a tag that matched sdkScriptMatch but isn't the Whim bundle. It's the one load-failure reason that points at the Whim bundle or the path to it rather than at the visitor's environment, so it is the only one worth raising with Whim if you see it recur.

Settlement is non-terminal: a bundle that arrives after the watchdog still initializes fully and fires whim.ui.ready(). If your page reacts to whim-error (for example by switching to a buy-only state), make that reaction reversible from whim.ui.ready().

The blocked / network distinction is a best-effort classification — browsers intentionally hide why a script failed, so the snippet reads the signals around it (online state, resource timing).

Keep the snippet current: the bootstrap snippet is pasted inline, so it stays frozen on your page until you re-paste it. Behavioural changes are versioned (whim._v, currently 3) and reported on failure beacons; when we ship a snippet change we will let you know to refresh your copy.

Configuration

Set window.whimConfig before the bootstrap snippet:

window.whimConfig = {
  sdkTimeoutMs: 10000,         // watchdog timeout, default 5000
  disableErrorReporting: true, // opt out of failure beacons to Whim
  merchantId: 'YOUR_MERCHANT_ID', // scopes error detection to /v1/YOUR_MERCHANT_ID.js
                               // instead of any /v1/*.js on the page — use this if
                               // you have other /v1/ scripts (analytics, warranties)
  sdkScriptMatch: 'my-proxy',  // substring matcher if you proxy the script
                               // under a first-party URL (overrides merchantId)
  errorEndpoint: '/api/whim/errors', // override where failure beacons are sent
                               // (defaults to Whim's endpoint)
  apiBaseUrl: 'https://API_ORIGIN', // pin the origin the SDK calls for prices and
                               // catalog data (provided during onboarding); set it
                               // if you proxy the script under a first-party URL so
                               // the SDK doesn't infer the origin from your script tag
  debug: true,                 // log every state change and event to the console
};
Serving behind ad blockers: if a meaningful share of your traffic uses ad blockers, the most effective fix is serving the SDK from your own domain (first-party proxy). Contact us and we'll set it up with you.

Graceful degradation

The defenses above keep the SDK from breaking your page. If you want the page to look like it never heard of Whim when the script fails, treat the Whim layer as a progressive enhancement: ship your normal product page, keep the Whim markup hidden in CSS, and reveal it only once the SDK is ready. whim.ui.ready() never fires on a blocked script, a watchdog timeout, or an init crash — so any failure simply leaves your standard page in place, with no Whim components visible.

Put your existing buy UI and the Whim layer in sibling blocks:

<!-- Your normal PDP — the ONLY thing shoppers see if Whim never loads -->
<div data-whim-standard>
  <div class="price">$449.00</div>
  <button class="add-to-cart">Add to Cart</button>
</div>

<!-- The Whim layer — hidden until the SDK opens the gate -->
<div data-whim-enhanced>
  <whim-selector product="sku-123">
    <whim-price buy-price="$449.00" show="full" suffix="/mo"></whim-price>
    <whim-option mode="buy">Buy</whim-option>
    <whim-option mode="whim">Rent monthly</whim-option>
    <whim-checkout-button><button>Rent with Whim</button></whim-checkout-button>
  </whim-selector>
</div>

Hide the Whim layer by default in your own CSS. Because this ships with your page, it holds even if the script is blocked or JavaScript is disabled — only the SDK can open the gate:

[data-whim-enhanced] { display: none; }
html.whim-ready [data-whim-enhanced] { display: block; }
html.whim-ready [data-whim-standard] { display: none; }

Open the gate from ui.ready() — the one switch that only flips on a healthy SDK:

whim.ui.ready(function () {
  document.documentElement.classList.add('whim-ready');
});
Which approach? Use this gate when the page must fall back to a plain PDP with no Whim UI on failure. If instead you want Whim woven into the page with a readable degraded state, skip the gate and rely on the per-component fallbacks — e.g. whim-price's buy-price renders your static value until the live price arrives.

Cart

The cart persists items in localStorage per merchant. All cart methods resolve through whim.map() if you've registered product mappings.

Tip: The cart is for multi-product Whim flows where shoppers collect items across PDPs before checking out. If you only need single-product checkout, use whim.getCheckoutUrl() instead.
Products
Wireless Headphones
Black, Noise Cancelling
$29/mo
Portable Speaker
Waterproof, 20W
$15/mo
Smart Watch
Silver, GPS
$35/mo
Rental
Console

whim.cart.add(productName, opts?)

Adds a product to the cart. If the product is already in the cart, its quantity is incremented by opts.quantity (default 1). Resolves to true when the cart actually changed and false when the call was dropped (debounced double-click, unknown product, or out of stock). One exception: on a lazy catalog, a name the SDK hasn't seen yet is assumed to be a not-yet-loaded product — the add is accepted and the line completes once the price arrives. Emits cart-change on success. Use the resolved boolean to gate one-shot UI like a toast.

whim.cart.add('wireless-headphones-black');

// Add multiple at once
whim.cart.add('wireless-headphones-black', { quantity: 3 });

// With options
whim.cart.add('wireless-headphones-black', {
  quantity: 2,
  merchantData: { source: 'pdp', listingId: 'abc123' },
  addons: { case: 'hard-shell' },
});

// Branch on whether the cart actually changed
if (await whim.cart.add('wireless-headphones-black')) {
  // Item added — show success UI
}

Returns Promise<boolean>

Rapid repeated calls for the same product within 500ms are coalesced into a single add. This prevents accidental double-clicks on a buy button from adding the item twice. Calls suppressed this way resolve to false and do not emit events.


whim.cart.setQuantity(productName, quantity)

Sets the absolute quantity for a product. Passing 0 (or less) removes the line. If the product isn't in the cart yet and quantity is positive, it's added at that quantity — same validation as cart.add. Emits cart-change.

whim.cart.setQuantity('wireless-headphones-black', 3);
whim.cart.setQuantity('wireless-headphones-black', 0); // removes

whim.cart.remove(productName)

Removes a product from the cart. Emits cart-change.

whim.cart.remove('wireless-headphones-black');

whim.cart.has(productName)

Resolves to true if the product is in the cart.

if (await whim.cart.has('wireless-headphones-black')) {
  // already in cart
}

Returns Promise<boolean>


whim.cart.get()

Resolves with all cart items with resolved prices.

var items = await whim.cart.get();
// [{
//   productName: 'wireless-headphones-black',
//   merchantProductId: 'sku-123456',  // present if added via mapped ID
//   price: { price: 29, introDiscount: { price: 15, months: 3 }, ... },
//   merchantData: { source: 'pdp' },
//   addons: { case: 'hard-shell' }
// }]

Returns Promise<WhimCartItem[]>


whim.cart.getCheckoutUrl()

Resolves with a single checkout URL for all items in the cart, or null if the cart is empty.

var url = await whim.cart.getCheckoutUrl();
// 'https://rent.your-store.com/your-merchant-id/checkout?products=product-a,product-b(addons(case=hard-shell))'

Returns Promise<string | null>


whim.cart.count()

Resolves with the total number of units in the cart, summed across quantities. A cart with two distinct products at quantities 2 and 3 resolves to 5. Use this for cart badges.

Returns Promise<number>


whim.cart.distinctCount()

Resolves with the number of distinct line items in the cart, regardless of quantity. A cart with two distinct products at quantities 2 and 3 resolves to 2.

Returns Promise<number>


whim.cart.clear()

Removes all items from the cart. Emits cart-change.


whim.cart.getProductNames()

Resolves with an array of Whim product names currently in the cart.

Returns Promise<string[]>


whim-cart-change event

Dispatched on document after every cart change. e.detail is a WhimCartChangeEvent with the current cart state. It describes changes, not initial state: the cart restored from storage on page load does not fire it, so seed your UI from whim.cart.count() in ui.ready and keep it current from the event.

document.addEventListener('whim-cart-change', function(e) {
  // e.detail = { count, distinctCount, items, subtotal }
  updateCartBadge(e.detail.count);
});

Types

WhimPrice

{
  price: number;               // ongoing monthly price
  priceFormatted: string;      // e.g. '$60'
  currency: string;            // e.g. 'USD'
  introDiscount: {             // null if no discount
    price: number;             // discounted price
    priceFormatted: string;    // e.g. '$30'
    percent: number;           // e.g. 50
    months: number;            // how many months the discount applies
  } | null;
}

WhimProductConfig

{
  productName: string;
  isOutOfStock: boolean;      // true if currently unavailable for rental
  price: {
    amount: number;           // monthly price in cents
    currencyCode: string;
    recurringInterval: string;
    recurringCount: number;
  };
  discountPercent: number | null;  // intro discount percentage
  discountMonths: number | null;   // intro discount duration
}

WhimProductStatus

{
  name: string;              // the name you passed to checkProducts()
  productName: string;       // that name after map() resolution
  status: 'success' | 'missing' | 'error';
  isKnownUnavailable: boolean; // true ONLY for a definitive negative
  isOutOfStock: boolean;     // meaningful when status is 'success'
  price: WhimPrice | null;   // null unless status is 'success'
  error: Error | null;       // set when status is 'error'
}

WhimCartItem

{
  productName: string;
  merchantProductId?: string;  // your ID, if added via map()
  quantity: number;            // always >= 1
  price: WhimPrice;
  lineSubtotal: number;        // price.price * quantity
  merchantData?: Record<string, string>;
  addons?: Record<string, string>;
}

WhimCartAddOpts

{
  quantity?: number;                            // default 1
  merchantData?: Record<string, string>;
  addons?: Record<string, string>;
}

WhimCartChangeEvent

{
  count: number;          // total units, summed across quantities
  distinctCount: number;  // number of distinct line items
  items: WhimCartItem[];
  subtotal: {             // summed across items
    amount: number;       // sum of lineSubtotal across all items
    formatted: string;    // e.g. '$79'
    currency: string;     // e.g. 'USD'
  };
}

Components

Web Components for common UI patterns: buy/whim toggles, price display, checkout buttons, and variant switching. Registered automatically when the SDK loads. They react to state changes without additional JS.

Components render in light DOM, so your existing CSS applies. They handle mode switching, price rendering, checkout redirects, and accessibility (role, aria-checked, keyboard support). Each one uses the same JS API under the hood.

You can use components on their own, use the JS API on its own, or combine both. For example, you might use <whim-price> to display the Whim price but handle mode switching and checkout with your own JS. Components and the API share the same state, so they stay in sync automatically.

Styling by mode with data-whim-mode

The following components mirror the active SDK mode onto themselves as a data-whim-mode attribute: <whim-selector>, <whim-price>, <whim-when>, <whim-checkout-button>, and <whim-cart-add>. The attribute value is either "buy" or "whim", and updates synchronously on every mode change. Use it to drive mode-specific styling directly from CSS, without subscribing to events or writing any JS:

/* Emphasize the hero price when the customer is in rent mode */
whim-price[data-whim-mode="whim"] { color: #ea4f1f; }

/* Tint the toggle row background in rent mode */
whim-selector[data-whim-mode="whim"] { background: #fff8f5; }

/* Different button look when wrapped by <whim-cart-add> in rent mode */
whim-cart-add[data-whim-mode="whim"] button { border-color: #ea4f1f; }

Transitions and animations defined in CSS work out of the box.

<whim-selector>

Sets the active product for the page. Wrap your buy/whim toggle in this element.

<whim-selector product="wireless-headphones-black">
  <whim-option mode="buy">Buy - $249</whim-option>
  <whim-option mode="whim">Rent - <whim-price suffix="/mo" /></whim-option>
</whim-selector>

Attributes

AttributeRequiredDescription
productYesThe Whim product name (or your mapped ID if you've called whim.map())

The selector calls whim.setProduct() when it connects, so price data and checkout URLs are available immediately after this element mounts.

The current SDK mode is mirrored onto the element as data-whim-mode="buy" or data-whim-mode="whim" so you can style the toggle row from CSS. See Styling by mode for examples.

If rent isn't offerable for the product, the element gets a data-out-of-stock attribute. You can use this to hide or restyle the rental option:

whim-selector[data-out-of-stock] { display: none; }

Three situations set it, and data-unavailable-reason tells them apart: "out-of-stock" (the product is in the program but has no rental inventory), "missing" (the product isn't in the rental program), and "load-failed" (the price could not be loaded after retries, so availability is unknown). Style all three off data-out-of-stock and, if you want, give load-failed its own copy so a pricing outage doesn't read as out of stock. The SDK retries a load-failed selection when the tab becomes visible again or the browser comes back online. If a rentable price arrives, both attributes are cleared; if the product turns out to be out of stock, data-out-of-stock stays and the reason becomes "out-of-stock".

whim-selector[data-out-of-stock] .rent-unavailable-label::after { content: "Out of Stock"; }
whim-selector[data-unavailable-reason="load-failed"] .rent-unavailable-label::after { content: "Pricing unavailable"; }

<whim-option>

A buy/whim toggle option. Must be inside a <whim-selector>. Clicking sets the SDK mode and highlights the selected option.

<whim-option mode="buy">Buy</whim-option>
<whim-option mode="whim">Rent</whim-option>

Attributes

AttributeRequiredDescription
modeYes"buy" or "whim"
default-styleNoApply the built-in option styles (border, padding, selection state). Omit to fully control the appearance with your own CSS.
theme-borderNoBorder color (default: #e0e0e0). Requires default-style.
theme-border-selectedNoSelected border color (default: #1a1a1a). Requires default-style.
theme-border-radiusNoBorder radius (default: 10px). Requires default-style.
theme-paddingNoPadding (default: 16px). Requires default-style.

The selected option gets the .whim-selected class and aria-checked="true". Keyboard accessible (Enter and Space).

Styling

By default, <whim-option> ships unstyled so it inherits cleanly from your existing layout. Add the default-style attribute to opt into the built-in look:

<whim-option mode="buy" default-style>Buy</whim-option>
<whim-option mode="whim" default-style>Rent</whim-option>

With default-style, you can theme the option through CSS custom properties:

whim-option {
  --whim-border: #ccc;
  --whim-border-selected: #0066ff;
  --whim-border-radius: 8px;
  --whim-padding: 12px 16px;
}

Or through theme-* attributes directly on the element:

<whim-option mode="whim" default-style theme-border="#ddd" theme-border-selected="blue">
  Rent
</whim-option>
Tip: Components render in light DOM, so all your existing CSS applies. The .whim-selected class on the active option lets you style the selected state however you want.

<whim-price>

Renders Whim pricing for the current product. In buy mode, shows whatever you put in buy-price (or nothing). Re-renders automatically on mode and product changes.

<whim-price buy-price="$249" suffix="/mo" />

<!-- No discount:    "$60/mo" -->
<!-- With discount:  "$30/mo first month, then $60/mo" -->

If the product has an intro discount, the component automatically renders the full pricing copy (e.g. "$30/mo first month, then $60/mo") so the ongoing price is always visible.

Attributes

AttributeRequiredDescription
buy-priceNoText to show when mode is "buy". If omitted, shows nothing in buy mode.
showNo"full" to render just the ongoing monthly price (no discount copy). "first" to render only the discounted price when an intro discount applies, or the ongoing price otherwise. Useful if you want to compose your own layout.
prefixNoText before the price (e.g. "From ")
suffixNoText after the price (e.g. "/mo")
out-of-stock-labelNoText to show when the product is out of stock in whim mode. Defaults to "Out of Stock".
Tip: When a product has an intro discount, <whim-price> automatically renders the full discount copy (e.g. "$30/mo first 3 months, then $60/mo"). Use show="full" if you only want the ongoing monthly price.

The current SDK mode is mirrored onto the element as data-whim-mode="buy" or data-whim-mode="whim" so you can recolor or restyle the price by mode from CSS. See Styling by mode for examples.

<whim-teaser-price>

Teaser for listing pages and product grids. Renders a one-liner like "or rent for $276/mo" next to the buy price. The program verb (rent, try, subscribe) comes from your merchant config and defaults to "try". Shows the intro discount price when available.

<p class="buy-price">$3,054.00</p>
<whim-teaser-price product="leica-q2" />
<!-- renders: "or rent for $276/mo" -->

Attributes

AttributeRequiredDescription
productYesThe Whim product name (or your mapped ID)
currency-symbolNoOnly relevant while a price is still loading: shows this symbol (e.g. $) as static text before the loading placeholder, so just the digits shimmer. Off by default.

While a price is loading, the element shows an inline shimmer placeholder in place of the price and carries a data-whim-loading attribute; the surrounding copy stays visible so the price fades in without layout shift. When the product is out of stock, the element renders nothing and gets a data-out-of-stock attribute.

Styling

The element is display: inline by default, so it inherits styles from its parent. Style it like any other element:

whim-teaser-price {
  color: #e85d3a;
  font-size: 14px;
  display: block;
}

Listing page example

Drop one into each product card on a category page:

<div class="product-card">
  <img src="/leica-q2.jpg" alt="Leica Q2" />
  <h3>Leica Q2</h3>
  <p class="price">$3,054.00 - $3,943.00</p>
  <whim-teaser-price product="leica-q2" />
</div>
Tip: Unlike <whim-price> (which uses the active product from <whim-selector>), each <whim-teaser-price> resolves its own product independently via the product attribute. This makes it ideal for pages showing many products at once.

<whim-checkout-button>

Wraps your existing checkout button or link. In whim mode, it redirects to the Whim checkout URL. In buy mode, your existing checkout still works as-is.

With a link

The component swaps the href when mode is "whim" and restores it when mode is "buy":

<whim-checkout-button>
  <a href="/cart/add?sku=sku-123456">Add to Cart</a>
</whim-checkout-button>

With a button

The component intercepts clicks in whim mode and redirects to checkout. In buy mode, the click passes through to your handler:

<whim-checkout-button>
  <button onclick="addToCart()">Add to Cart</button>
</whim-checkout-button>

The component detects whether the child is an <a> or a <button> and behaves accordingly.

Attributes

AttributeRequiredDescription
buy-labelNoText shown on the inner button or link when mode is "buy". The original text is captured on first render and restored when this attribute is omitted for the active mode.
whim-labelNoText shown on the inner button or link when mode is "whim".
<whim-checkout-button buy-label="Add to Cart" whim-label="Start Rental">
  <button>Add to Cart</button>
</whim-checkout-button>
Tip: In buy mode, the checkout button does nothing. Your existing checkout behavior runs as-is, no conditional logic needed.

The current SDK mode is mirrored onto the wrapper as data-whim-mode="buy" or data-whim-mode="whim" so you can restyle the inner button or link by mode from CSS. See Styling by mode for examples.

When the product is out of stock in whim mode, clicks are blocked and the inner trigger gets aria-disabled="true" — a <button> is also given disabled, while an <a> has its href removed and is taken out of the tab order. The wrapper also gets a data-out-of-stock attribute for CSS styling.

<whim-cart-add>

Wraps your existing add-to-cart button. In whim mode, it adds the product to the Whim cart and dispatches a whim-added-to-cart event. In buy mode, the click passes through to your handler.

<whim-cart-add
  product="wireless-headphones-black"
  buy-label="Add to Cart"
  whim-label="Add to Rental Cart"
>
  <button onclick="addToCart()">Add to Cart</button>
</whim-cart-add>

Attributes

AttributeRequiredDescription
productYesThe Whim product name (or your mapped ID) to add when clicked in whim mode.
quantityNoHow many units to add per click (default: 1). If the product is already in the cart, this is added to the existing quantity.
buy-labelNoText shown on the inner button when mode is "buy". The original text is captured on first render and restored when this attribute is omitted for the active mode.
whim-labelNoText shown on the inner button when mode is "whim".

The whim-added-to-cart event

When a click in whim mode results in a successful add, the component dispatches a bubbling whim-added-to-cart CustomEvent. The event only fires when the cart was actually mutated, so a toast hooked to it never shows a false confirmation:

document.addEventListener('whim-added-to-cart', function (e) {
  // e.detail = { product, quantity }
  showToast('Added ' + e.detail.product + ' to your rental cart');
});
Tip: Rapid repeated clicks on the same product within a short window are coalesced into a single add. The event fires once per real add, so you can wire a toast directly to it without debouncing yourself.

The current SDK mode is mirrored onto the wrapper as data-whim-mode="buy" or data-whim-mode="whim" so you can restyle the inner add-to-cart button by mode from CSS. See Styling by mode for examples.

Out-of-stock products are blocked from being added to the cart. The click is silently swallowed.

<whim-when>

Shows or hides its children based on the current SDK mode. Use it to swap product page sections (gear grade tables, warranty options, rental perks) without writing any JavaScript.

<whim-when mode="buy">
  <!-- Visible only in buy mode -->
  <p>Free shipping on orders over $75</p>
</whim-when>

<whim-when mode="whim">
  <!-- Visible only in whim mode -->
  <p>Cancel anytime. No long-term commitment.</p>
</whim-when>

Attributes

AttributeRequiredDescription
modeYes"buy", "whim", or a comma-separated list (e.g. "buy,whim") to match multiple modes.

When the mode doesn't match, the element is hidden with display: none and takes no layout space.

Before the SDK loads, a <whim-when> is an unknown element and renders all of its children, so both mode regions are visible until the SDK defines it. To avoid that flash, hide the non-default region in your own CSS until the element is upgraded. :defined is supported in every browser that supports Custom Elements:

whim-when[mode="whim"]:not(:defined) { display: none; }

The current SDK mode is also mirrored onto the element as data-whim-mode="buy" or data-whim-mode="whim", regardless of whether this <whim-when> currently matches, so you can target descendants from CSS based on the active mode (e.g. animate a card on entry). See Styling by mode for examples.

<whim-variant> / <whim-variant-option>

Handles variant switching (color, condition, size, etc.) by wrapping your existing variant buttons. When a customer clicks a variant option, the SDK switches to the corresponding product.

<whim-variant name="color">
  <whim-variant-option product="wireless-headphones-black">
    <button>Black</button>
  </whim-variant-option>
  <whim-variant-option product="wireless-headphones-silver">
    <button>Silver</button>
  </whim-variant-option>
  <whim-variant-option product="wireless-headphones-navy">
    <button>Navy</button>
  </whim-variant-option>
</whim-variant>

<whim-variant> attributes

AttributeRequiredDescription
nameYesThe variant attribute name (e.g. "color", "size")

<whim-variant-option> attributes

AttributeRequiredDescription
productYes*The Whim product name for this variant
valueNoReserved for future configurable product support

*Use product in v1. Each variant option maps to a distinct product name in your catalog.

How it works

Each <whim-variant-option> wraps a single child element (your existing button or swatch). The SDK attaches a click listener that calls whim.setProduct() with the specified product name. This triggers ready and price-change events, so <whim-price> and <whim-checkout-button> update automatically.

Tip: Variant switching calls setProduct() internally, which re-resolves pricing for the new product. Don't cache prices between variants yourself.

Full example

A complete product page with buy/whim toggle, variant switching, price display, and checkout:

<whim-selector product="wireless-headphones-black">
  <!-- Buy/rent toggle -->
  <whim-option mode="buy">
    Buy - $249
  </whim-option>
  <whim-option mode="whim">
    Rent - <whim-price suffix="/mo" />
  </whim-option>

  <!-- Color variants -->
  <whim-variant name="color">
    <whim-variant-option product="wireless-headphones-black">
      <button>Black</button>
    </whim-variant-option>
    <whim-variant-option product="wireless-headphones-silver">
      <button>Silver</button>
    </whim-variant-option>
  </whim-variant>

  <!-- Checkout -->
  <whim-checkout-button>
    <a href="/cart/add?sku=sku-123456">Add to Cart</a>
  </whim-checkout-button>
</whim-selector>

The SDK manages all the state: mode switching, price updates, checkout URLs. The HTML and CSS are yours.


Product explorer

Enter your merchant ID to pull your product catalog from the Whim API. This is the same data the SDK uses at runtime.

Enter your merchant ID and click Load Products.

Sandbox

A mock product page with buy/whim toggle, pricing, and checkout URL. Load your products above to populate it with real data.

The buy price