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).
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+.
- Getters and cart data methods return promises. Use
awaitor.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 ondocument(whim-price-change,whim-cart-change, …). Listen withdocument.addEventListener; it works before the SDK loads. whim.isReadyis a boolean. Startsfalsewhen the snippet runs, becomestrueonce 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.
Set the product
<whim-selector> tells the SDK which product to load. Everything else goes inside it.
Add the buy/whim toggle
<whim-option> lets shoppers switch between buy and whim mode. The SDK handles the state, selection styling, and accessibility.
Display the price
<whim-price> shows the Whim price in whim mode. Set buy-price to show your buy price when in buy mode.
Handle checkout
<whim-checkout-button> wraps your existing CTA. Redirects to Whim checkout in whim mode.
<!-- 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.
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.
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.
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();
}
+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:
name— the name you passed, so results key back to your own data;productNameis that name aftermap()resolution.status—'success'(answered;pricepopulated),'missing'(answered; not in your catalog), or'error'(could not be determined).isKnownUnavailable—trueonly for a definitive negative: absent from the catalog, or present and out of stock.isOutOfStock,price,error— the details behind the verdict.
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.
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.
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);
});
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();
.d.ts augments DocumentEventMap, so e.detail is typed for every whim-* event.
Event types
| Event | Payload | When |
|---|---|---|
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-change | WhimPrice | After setProduct resolves pricing |
product-change | WhimProductConfig | Every 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-change | Record<string, string> | After a <whim-variant-option> selection updates the active variant attributes |
addon-change | Record<string, string> | After setAddon or removeAddon |
catalog-update | WhimProductConfig[] | 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. |
error | WhimSdkError | Only 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. |
refresh | none | After ui.refresh() |
cart-change | WhimCartChangeEvent | After 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:
- No call ever hangs. Every pending Promise from pre-load calls settles with
null— the same shape getters already return when no product is active.await whim.getPrice()always comes back. - Your page is untouched. Web components that can't initialize leave your original markup rendering exactly as it was.
- Failure is non-terminal. If the script eventually loads (e.g. a slow connection), the SDK initializes normally: queued calls replay,
ui.readycallbacks fire, and components upgrade. DOM listeners you registered during the outage are unaffected and fire as the SDK emits. Only the calls awaited during the outage receivednull.
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
| Failure | Detection | e.detail |
|---|---|---|
| Script blocked (ad blocker, CSP) | Snippet: script error event, settled immediately | reason: 'blocked' |
| Network failure (offline, DNS, reset) | Snippet: script error event, settled immediately | reason: 'network' |
| Stalled / silent failure | Snippet: watchdog timer (default 5s, see below) | reason: 'timeout' |
| Script downloaded and ran, but the SDK never initialized | Snippet: script load event with window.whim still the stub and no init error recorded, settled immediately | reason: 'crashed' |
| Browser lacks Custom Elements (legacy Edge, pre-2018 engines) | Bundle: checked before anything is built, settled immediately | phase: '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 immediately | phase 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).
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
};
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');
});
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.
whim.getCheckoutUrl() instead.
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
| Attribute | Required | Description |
|---|---|---|
product | Yes | The 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
| Attribute | Required | Description |
|---|---|---|
mode | Yes | "buy" or "whim" |
default-style | No | Apply the built-in option styles (border, padding, selection state). Omit to fully control the appearance with your own CSS. |
theme-border | No | Border color (default: #e0e0e0). Requires default-style. |
theme-border-selected | No | Selected border color (default: #1a1a1a). Requires default-style. |
theme-border-radius | No | Border radius (default: 10px). Requires default-style. |
theme-padding | No | Padding (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>
.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
| Attribute | Required | Description |
|---|---|---|
buy-price | No | Text to show when mode is "buy". If omitted, shows nothing in buy mode. |
show | No | "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. |
prefix | No | Text before the price (e.g. "From ") |
suffix | No | Text after the price (e.g. "/mo") |
out-of-stock-label | No | Text to show when the product is out of stock in whim mode. Defaults to "Out of Stock". |
<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
| Attribute | Required | Description |
|---|---|---|
product | Yes | The Whim product name (or your mapped ID) |
currency-symbol | No | Only 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>
<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
| Attribute | Required | Description |
|---|---|---|
buy-label | No | Text 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-label | No | Text 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>
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
| Attribute | Required | Description |
|---|---|---|
product | Yes | The Whim product name (or your mapped ID) to add when clicked in whim mode. |
quantity | No | How many units to add per click (default: 1). If the product is already in the cart, this is added to the existing quantity. |
buy-label | No | Text 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-label | No | Text 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');
});
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
| Attribute | Required | Description |
|---|---|---|
mode | Yes | "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
| Attribute | Required | Description |
|---|---|---|
name | Yes | The variant attribute name (e.g. "color", "size") |
<whim-variant-option> attributes
| Attribute | Required | Description |
|---|---|---|
product | Yes* | The Whim product name for this variant |
value | No | Reserved 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.
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.
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