Skip to content

Quick start

By the end of this guide, your cart page and cart drawer will render the Shipping Protection (SP) panel. When a customer toggles the switch, the fee line is automatically added to or removed from the cart. In cart mode, your storefront must also call resolveSP() before checkout and block navigation on failure. See Integration modes. The checkout and checkout+ modes differ in how you handle the checkout button. See Integration modes for when each mode writes the fee line.

  • Complete the checklist in Before you integrate.
  • Confirm with your account manager (AM) that SP is enabled. If your store uses checkout or checkout+ integration mode, complete this guide first, then read Integration modes.
  • Download xcotton-types.ts into your project’s lib/ directory. All examples below import types from that path.

This minimal, framework-free page runs the entire SP integration in three steps. The sections below show how each piece maps to your framework.

<!-- Step 1: declare the config, then load the SDK -->
<script>
window.xcottonConfig = {
shop: 'example.myshopify.com',
country: 'US',
currency: 'USD',
language: 'en',
publicAccessToken: 'your-storefront-public-token',
spWidgetSelector: '.xc-sp-container',
}
</script>
<script
src="https://sslstaticus.xcottons.com/shopify-plugins/xmh-idc/headless/sdk.js"
crossorigin="anonymous"
></script>
<!-- Step 2: the SDK renders the SP panel into this container -->
<div class="xc-sp-container"></div>
<script type="module">
// Step 3: initialize and sync the page's current cart
await window.xcotton.init({
cartId: 'gid://shopify/Cart/your-cart-id',
onCartChange: () => reloadCartView(),
})
// Then, after every cart write of your own, sync once:
// the SDK decides first bind, cart switch, and same-cart refresh
// await window.xcotton.syncCart(cart.id)
</script>

reloadCartView() is your storefront’s own function that re-fetches the cart and updates the UI.

This minimal page is the direct single-entry example: init({ cartId }) also performs the first sync for that cart. The framework examples below use a shared helper: xcottonInit() only assembles the SDK without a cart, and syncXcotton(cartId) handles the first bind, same-cart refreshes, and cart switches. Both forms call init once.

Declare window.xcottonConfig before loading sdk.js. The order is strict. In SSR or full-stack frameworks, both scripts must execute before hydration.

window.xcottonConfig fields:

FieldRequiredDescriptionExample
shopYesShopify store domainexample.myshopify.com
countryYesActive market country code. Must match Shopify MarketsUS
currencyYesActive settlement currency for the current market. Must match the cart currencyUSD
languageYesUI language code. It sets panel copy and the language for Storefront queries and quotesen
publicAccessTokenYesStorefront API public access token—
spWidgetSelectorNoCSS selector for the SP panel container.xc-sp-container
ppWidgetSelectorNoCSS selector for the PP panel container#xcotton-pp-panel

The script URL is always https://sslstaticus.xcottons.com/shopify-plugins/xmh-idc/headless/sdk.js with no version segment. The active SDK version is controlled by your store’s back-office configuration, so you never need to change the URL when upgrading.

app/layout.tsx
import Script from 'next/script'
const config = {
shop: 'example.myshopify.com',
country: 'US',
currency: 'USD',
language: 'en',
publicAccessToken: 'your-storefront-public-token',
spWidgetSelector: '.xc-sp-container',
ppWidgetSelector: '#xcotton-pp-panel',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>
<Script id="xcotton-config" strategy="beforeInteractive">
{`window.xcottonConfig = ${JSON.stringify(config)}`}
</Script>
<Script
src="https://sslstaticus.xcottons.com/shopify-plugins/xmh-idc/headless/sdk.js"
strategy="beforeInteractive"
crossOrigin="anonymous"
/>
{children}
</body>
</html>
)
}

Place an empty container element matching spWidgetSelector on the cart page and inside the cart drawer. Leave it empty. The SDK detects the container and mounts the panel automatically.

Multiple matching containers can coexist, including a temporarily hidden drawer. The SDK creates a panel in each container, sharing the same switch state and quote. Toggling either panel updates the others.

components/cart-protection.tsx
export function CartProtection() {
return <div className="xc-sp-container" />
}

When the cart drawer closes and reopens, or when client-side navigation returns to the cart page, the SDK automatically restores the panel into the remounted container without firing extra cart or quote requests.

The PP panel container lives on the product page and is rendered by the startup component in Step 3.

Cart lifecycle management follows two rules:

  1. At page startup in the shared component example: call init() once without arguments (the helper wraps this as xcottonInit()) to assemble the SDK only. Do not pass cartId.
  2. After every storefront cart write: whenever your code adds items, updates quantities, removes lines, or clears the cart, call syncCart(cart.id) once (the shared helper wraps this as syncXcotton(cart.id)). It handles the first bind, cart switches, and same-cart refreshes.

Your storefront keeps no binding state. The SDK decides whether this is a first bind, a same-cart refresh, or a cart switch.

First, create a shared helper module so all components interact with the SDK through a single entry point:

lib/xcotton.ts
import type { XcottonApi } from './xcotton-types'
import { reloadCartView } from './cart'
const cartChangeListeners = new Set<() => void>()
export function onXcottonCartChange(listener: () => void): () => void {
cartChangeListeners.add(listener)
return () => {
cartChangeListeners.delete(listener)
}
}
// Initialize once per page: every component shares one Promise and callback reference.
const handleCartChange = () => {
void reloadCartView().catch(console.error)
// Confirmed cart facts can accompany a business-sync failure; components must read offers and handle errors.
for (const listener of cartChangeListeners) listener()
}
let initPromise: Promise<void> | undefined
export function xcottonInit(): Promise<void> {
const sdk = window.xcotton
if (!sdk) return Promise.reject(new Error('[XC] sdk.js has not loaded yet'))
if (initPromise) return initPromise
// One-shot: repeating this call does not reinitialize; bind a late cartId with syncXcotton(cartId).
initPromise = sdk.init({ onCartChange: handleCartChange })
return initPromise
}
// Hand storefront writes to the SDK: it decides first bind, cart switch, and same-cart refresh.
// Initialize the session first; the storefront keeps no binding state or queue.
export async function syncXcotton(cartId: string): Promise<void> {
const sdk: XcottonApi | undefined = window.xcotton
if (!sdk) throw new Error('[XC] sdk.js has not loaded yet')
await xcottonInit()
await sdk.syncCart(cartId)
}
  • xcottonInit() runs once at page startup and accepts no cartId. It only assembles the SDK. The first bind goes through syncXcotton(cartId). The helper caches the single Promise so every component shares the result. init is a one-shot entry point: calling it while initialization is in progress, after success, or after failure rejects explicitly (SDK_INIT_IN_PROGRESS / SDK_ALREADY_INITIALIZED). It never reuses the in-flight Promise as a success, and it cannot replace the adapter or onCartChange registered by the first call. If the environment fails to initialize (including PP being enabled but failing to start), the SDK does not keep a half-initialized surface: fix the cause and reload the page.
  • syncXcotton(cartId) first waits for xcottonInit() without a cart ID, then calls sdk.syncCart(cartId). It handles the first bind, same-cart refreshes, and cart switches. Call it after every storefront cart write. You do not need to detect cart switches, and a late-arriving cartId goes here too instead of calling init again. The SDK runs public syncCart calls in order. Adjacent concurrent calls with the same input merge into one sync and share its result. When a call overlaps an internal cart write that does not go through the public surface, such as a panel switch write, it can reject with OPERATION_IN_PROGRESS. Follow recovery and retry after the active operation settles. A successful syncCart returns an independent Cart after the required synchronization. Use that Promise to decide whether the business step completed.
  • Inside onCartChange, only call reloadCartView(). Never call syncXcotton or syncCart inside onCartChange, or you will trigger an infinite loop (see Keep the cart in sync).

Next, mount the startup component at your application root. It does three things: assembles the SDK once with xcottonInit() without a cart ID. Hands the current cart ID to syncXcotton(cartId), which calls syncCart, when a cart already exists or after the storefront writes the cart. And passes the selected product variant ID to PP pricing (null outside product pages).

components/xcotton-startup.tsx
'use client'
import { useEffect } from 'react'
import { xcottonInit, syncXcotton } from '@/lib/xcotton'
import { onCartUpdatedByStorefront } from '@/lib/cart'
export function XcottonStartup({
variant,
cartId,
}: {
variant: string | number | null
cartId?: string
}) {
useEffect(() => {
return onCartUpdatedByStorefront((cart) =>
syncXcotton(cart.id).catch(console.error),
)
}, [])
useEffect(() => {
void (cartId ? syncXcotton(cartId) : xcottonInit()).catch(console.error)
}, [cartId])
useEffect(() => {
void xcottonInit()
.then(() =>
variant === null
? window.xcotton?.pp?.loadPlans(null)
: window.xcotton?.pp?.loadPlans(variant),
)
.catch(console.error)
return () => {
void xcottonInit()
.then(() => window.xcotton?.pp?.loadPlans(null))
.catch(console.error)
}
}, [variant])
return <div id="xcotton-pp-panel" hidden={!variant} />
}

The startup component keeps observing cartId. Update the prop when an existing cart is restored asynchronously or the active cart changes. The update calls syncXcotton(cartId), and rendering the same ID again adds no requests. Storefront writes still use the separate subscription to syncXcotton(cartId). Do not forward SDK notifications into that subscription.

The example imports two storefront-specific functions from lib/cart.ts that you implement for your app:

FunctionPurpose
reloadCartView()Re-queries the cart and updates your UI. Must be read-only and never write to the cart.
onCartUpdatedByStorefront(handler)Invokes handler(cart) after your own storefront mutates the cart, returning an unsubscribe function. SDK writes must never fire this event.

The variant prop is the currently selected variant ID on the product page (string or number) used for PP pricing. See Add product protection. Pass null if your store doesn’t use PP.

To open the SP coverage details modal from your own button outside the panel, call sp.showModal(). The returned Promise resolves as soon as the modal opens, without waiting for the customer to close it. The coverage button inside the modal expresses intent for the active mode: cart mode writes the fee line, checkout mode records intent only, and checkout+ does not show that button.

components/protection-details-button.tsx
'use client'
import { xcottonInit } from '@/lib/xcotton'
async function onShowSpDetailsClick() {
await xcottonInit()
const sp = window.xcotton?.sp
if (!sp) throw new Error('[XC] SP is not ready')
await sp.showModal()
}
export function ProtectionDetailsButton() {
return (
<button
type="button"
onClick={() => void onShowSpDetailsClick().catch(console.error)}
>
Protection details
</button>
)
}

These checks use cart mode. In checkout and checkout+, the fee line is written only at checkout. See Integration modes.

Open the cart page and run the following in the browser console:

window.xcotton.sp?.getMode()
// 'cart'
await window.xcotton.sp?.getQuote()
// { extId: 45678901234, totalPrice: '1.99', totalPriceInt: 199, currencyCode: 'USD', currencySymbol: '$' }

Then verify each interaction:

ActionExpected result
Open the cart pageThe SP panel renders inside the container
Toggle the switchThe fee line is added or removed, and your cart UI refreshes
Add a product from the storefront and call syncCart(cart.id)The panel quote updates
Empty the cart or push the subtotal outside the eligible range, then call syncCart(cart.id)The fee line is removed automatically
Close and reopen the drawer, or navigate back to the cart in a SPAThe panel reappears with no new cart or quote requests in the Network tab
Toggle the switch with both the cart page and drawer visibleBoth views display the same updated cart
SymptomCauseFix
Error [XC] sdk.js has not loaded yetThrown by the helper module because sdk.js hasn’t executed yetEnsure both scripts run before hydration and the script URL is reachable
init() rejects with [XC] SDK initialization failedSDK environment initialization failed. The parentheses name the module and stageVerify shop and publicAccessToken. If it persists, send the full error to your account manager (AM). Fix the cause and reload the page. Do not call init again
init() rejects with code=SDK_INIT_SYNC_FAILEDThe environment is ready, but the first cart sync failedRecover with syncCart(cartId) using the cartId in the error. Do not call init again
init() rejects with code=OPERATION_IN_PROGRESSAn in-flight operation held the first syncWait for the active operation to settle, then recover with syncCart(cartId). Do not call init again
init() rejects with code=SDK_ALREADY_INITIALIZEDA component called init more than onceInitialize once per page. Route late-arriving cart IDs through syncXcotton(cartId) (or call syncCart(cartId) directly)
syncCart throws an error starting with [XC] SDK not initializedsyncCart was called before init() finishedRoute cart sync calls through syncXcotton after awaiting xcottonInit()
syncCart() throws [XC] syncCart requires a cartId before the first synchronizationNo cart ID was passed and no cart is bound yetPass the current cart ID for the first sync
syncCart throws [XC] cart currency does not match the configured market (…)The cart currency differs from window.xcottonConfig.currencyPass the active market’s currency and make sure the cart belongs to that market
window.xcotton.sp is undefinedThe first sync with a cart ID hasn’t finished, or SP is disabledAwait syncCart first. If still undefined, contact your account manager (AM)
sp is defined, but the panel doesn’t appearNo DOM element matches spWidgetSelector, the container is inside a shadow root, or the current cart does not meet the display conditions (for example, an empty cart or a total outside the eligible range)Verify the selector with document.querySelectorAll(). Place the container in the light DOM and check that the cart meets the SP display conditions
Cart and quote requests fire in an endless loopsyncCart is being called inside onCartChangeOnly refresh the UI inside onCartChange
Panel price doesn’t match the checkout totalcountry or currency doesn’t match Shopify MarketsPass the active market’s values. See Before you integrate

For the complete list of error messages, see the SDK Reference.