Skip to content

SDK Reference

This page documents the entire public surface of the Headless SDK. Download xcotton-types.ts for complete TypeScript definitions, including global Window augmentations for window.xcotton and window.xcottonConfig. For step-by-step integration instructions, see the Quick start.

Global object / namespaceWhen it becomes available
window.xcottonConfigDeclared by your page before loading sdk.js
window.xcottonImmediately after sdk.js executes
window.xcotton.ppAfter init() resolves, when PP is enabled for the store
window.xcotton.spAfter the first cart sync with a cartId completes (triggered by init({ cartId }) or syncCart()), when SP is enabled

The startup configuration object read by the SDK. Must be defined on window before sdk.js runs.

interface XcottonConfig {
shop: string
country: string
currency: string
language: string
publicAccessToken: string
spWidgetSelector?: string
ppWidgetSelector?: string
}
window.xcottonConfig = {
shop: 'example.myshopify.com',
country: 'US',
currency: 'USD',
language: 'en',
publicAccessToken: 'your-storefront-public-token',
spWidgetSelector: '.xc-sp-container',
ppWidgetSelector: '#xcotton-pp-panel',
}
FieldRequiredDescription
shopYesShopify store domain (for example, example.myshopify.com)
countryYesActive market country code (such as US). Must match the cart’s buyer country and Shopify Markets
currencyYesActive settlement currency code for the current market (such as USD). Must match the cart currency
languageYesUI language code (such as en). Affects panel copy only, not the settlement currency
publicAccessTokenYesShopify Storefront API public access token
spWidgetSelectorNoCSS selector for SP containers. A first match owns its panel until node removal. All panels share state and quotes
ppWidgetSelectorNoCSS selector for the PP panel container

currency is also the SDK’s currency reference: the cart, quotes, and written lines must all use it. When switching markets, prepare a cart for the new market and declare the new country / currency before loading the SDK on a new page. Changing window.xcottonConfig at runtime does not update the current session.

Initializes the SDK and registers your onCartChange callback and optional adapter.

interface XcottonApi {
init(opts?: XcottonInitOptions): Promise<void>
syncCart(input?: string | Cart): Promise<Cart>
isXcottonError(value: unknown): value is XcottonError
sp?: SpApi
pp?: PpApi
}
interface XcottonInitOptions {
cartId?: string
adapter?: XcottonCartAdapter
onCartChange?: (cart: Cart) => unknown
}
const sdk = window.xcotton
if (!sdk) throw new Error('[XC] sdk.js has not loaded yet')
await sdk.init({
cartId: 'gid://shopify/Cart/your-cart-id',
onCartChange() {
void reloadCartView().catch(console.error)
},
})
OptionDescription
cartIdOptional. Pass an existing cart ID at startup to run the initial synchronization with init
adapterOptional. Overrides part or all of the built-in Storefront API cart client. See XcottonCartAdapter
onCartChangeOptional. Invoked after a confirmed SDK cart write and when each syncCart settles, receiving a confirmed Cart. It does not run without a confirmed snapshot. See Keep the cart in sync
  • One-shot entry point: init runs exactly once and does not handle refresh, binding, cart switching, or reconfiguration. Calling it again while initialization is in progress, after success, after an environment initialization failure, or after a failed first sync rejects: SDK_INIT_IN_PROGRESS while in progress, otherwise SDK_ALREADY_INITIALIZED with context.phase set to ready, environment_failed, sync_failed, or busy. It never reuses the original Promise and never counts as an idempotent success.
  • Binding, refresh, and cart switching: init is not a second cart entry point. After a cart-less initialization succeeds, call syncCart(cartId) to bind the first cart. Refresh and cart switches also go through syncCart. A late-arriving cartId is not bound by calling init again.
  • Cart-optional startup: can be called before a cart exists (PP product pricing does not require a cart).
  • Two-stage recovery: if the environment fails to initialize (including PP being enabled but failing to start), the SDK keeps no half-initialized surface. Correct the cause and reload the page.syncCart cannot bypass that failure. If the environment is ready but the first cart read or business sync fails, recover with syncCart(cartId) from the real cart state without replaying previous writes, and do not call init again.
  • First sync busy: when the first sync conflicts with another cart operation in flight, init rejects with OPERATION_IN_PROGRESS. A later init call returns SDK_ALREADY_INITIALIZED with context.phase set to busy. Wait for the active operation to settle, then bind with syncCart(cartId). Do not call init again.
  • Error handling: init rejects with an XcottonError (SDK_INIT_FAILED for the environment stage, SDK_INIT_SYNC_FAILED for the first sync stage, OPERATION_IN_PROGRESS when the first sync is busy). The message starts with [XC] and names the failing module, stage, and cause. See Error handling.

Returns whether a value is an XcottonError thrown by the SDK. Failures from public SDK methods, including before initialization, use this shape. Site functions and script-load errors do not. The guard does not rely on instanceof, so it also works across bundle boundaries and realms.

const sdk = window.xcotton
if (!sdk) throw new Error('[XC] sdk.js has not loaded yet')
try {
await sdk.syncCart('gid://shopify/Cart/your-cart-id')
} catch (error) {
if (sdk.isXcottonError(error)) {
console.warn(error.code, error.context, error.recovery.action)
} else {
console.error(error)
}
}
  • Parameter: any value.
  • Returns: value is XcottonError. When true, you can safely read code, message, context, recovery, and cause.
  • Do not test with instanceof XcottonError: remote modules and your storefront may run in different bundles or realms.

The single cart synchronization entry point: use it for the first bind, for switching carts, and for refreshing the same cart. The SDK decides which case applies, so your storefront keeps no second binding identity and no synchronization queue.

const sdk = window.xcotton
if (!sdk) throw new Error('[XC] sdk.js has not loaded yet')
await sdk.syncCart('gid://shopify/Cart/your-cart-id')
  • Pass a Storefront cart GID (such as gid://shopify/Cart/...) or a complete Cart that satisfies the SDK field contract. Omit it to synchronize the bound cart. Without a bound cart, the call rejects. A supplied Cart is validated for identity, currency, and complete pagination before adoption.
  • The Promise returns an independent Cart after required business synchronization completes. A failed V4 request or protection quantity update rejects, so checkout must stay blocked. A failed operation may still notify confirmed cart facts. Callback counts do not represent operation counts.
  • On the first bind and whenever cartId changes, the SDK reconciles the fee line according to the active integration mode. See When a cart is first synchronized.
  • window.xcotton.sp is attached only after the first syncCart with a cart ID finishes.
  • Concurrency and merging: public syncCart calls run in order. Adjacent concurrent calls with the same input merge into one sync and share its result. A call that overlaps another internal operation already holding cart permission rejects with OPERATION_IN_PROGRESS. A known external change to the same cart invalidates old facts and work. A rejected request for another cart neither changes the binding nor indicates that the current cart changed. Wait for the active operation to finish before deciding whether to synchronize again.
  • Currency guard: the synchronized cart total must use the currency configured at initialization. Otherwise the call rejects instead of quoting or writing with the wrong currency.
  • Never call syncCart inside onCartChange, or you will create an infinite loop.

Optional custom cart adapter for storefronts that route cart reads and writes through their own API or BFF. You can implement a subset of methods. Any omitted method uses the built-in Storefront API implementation.

interface XcottonCartAdapter {
getCart?(cartId: string): Promise<Cart | null>
addLines?(cartId: string, lines: CartLineInput[]): Promise<Cart>
updateLines?(cartId: string, lines: CartLineUpdateInput[]): Promise<Cart>
removeLines?(cartId: string, lineIds: string[]): Promise<Cart>
updateAttributes?(cartId: string, attributes: AttributeInput[]): Promise<Cart>
}
interface CartLineInput {
merchandiseId: string
quantity?: number | null
attributes?: AttributeInput[] | null
}
interface CartLineUpdateInput {
id: string
quantity?: number | null
attributes?: AttributeInput[] | null
}
interface AttributeInput {
key: string
value: string
}
MethodImplementation requirements
getCartReturn null only when the cart is confirmed not to exist. Reject on network, auth, or rate-limit errors
addLinesmerchandiseId is a full ProductVariant GID.quantity defaults to 1 when omitted
updateLinesid is the current CartLine.id. Supply quantity, attributes, or both (quantity: 0 removes the line)
removeLinesPreferred method for removing cart lines
updateAttributesAlways receives the complete cart attribute array ([] clears all attributes)

Every write method must resolve with the complete updated Cart. For line attributes, omitting or passing null keeps existing attributes, [] clears them, and a non-empty array replaces the entire list. For complete contract and validation rules, see Use your own cart API.

Currency guard: the built-in and custom adapters follow identical rules. The SDK checks that cart.cost.totalAmount.currencyCode matches the currency configured at initialization, and that every money field uses that same currency. A mismatch rejects before quoting or writing. Custom adapters must not convert or replace currencies.

Rate-limit metadata: when your API is throttled, reject with an Error that keeps the following fields so the SDK can enter its cooldown and reject cart requests during it:

const error = new Error('Cart API throttled')
Object.assign(error, { status: 429, code: 'THROTTLED', retryAfterMs: 2000 })
throw error

The SDK recognizes throttling from status: 429, code: 'THROTTLED', or 429 / THROTTLED in message. Without retryAfterMs it applies a 30-second cooldown. If none of the three signals is present, the error is handled as a regular adapter failure and no cooldown starts. Do not swallow a throttling error and return null, and do not retry on your own.

The built-in adapter, custom adapters, and onCartChange all share a single Cart type, a focused subset of the Storefront API Cart schema. See xcotton-types.ts for the full definition.

Field pathDescription
idStorefront cart GID
lines.nodesAll cart lines (every pagination page must be fetched)
lines.pageInfoPagination metadata.hasNextPage must be false when returned to the SDK
lines.nodes[i].idLine ID (CartLine.id). Distinct per line even for the same variant. May change after attribute updates
lines.nodes[i].__typename'CartLine' or 'ComponentizableCartLine' (bundles are priced by the parent line)
lines.nodes[i].cost.amountPerQuantityUnit price
lines.nodes[i].cost.subtotalAmountPre-discount line subtotal
lines.nodes[i].cost.totalAmountPayable line total (preserve zero amounts, and do not substitute fallback prices)
lines.nodes[i].merchandiseProductVariant: GID, SKU, title, and requiresShipping
lines.nodes[i].merchandise.productParent product: id, title, handle, vendor, productType, isGiftCard
attributesCart-level attributes (value may be null when read, must be a string when written)
cost.totalAmountCart total as MoneyV2 (amount is a decimal string, currencyCode is the currency)
discountCodesApplied discount codes and their applicable status

UI-only fields such as product images and checkoutUrl are not part of Cart. Fetch them through your own storefront query when needed.

window.xcotton.sp becomes available after the first syncCart with a cart ID resolves. It remains undefined when SP is disabled for the store.

interface SpApi {
addSpFee(): Promise<{ added: true; extId: number }>
removeSpFee(): Promise<{ removed: boolean }>
declineSpFee(): Promise<{ declined: true }>
resolveSP(): Promise<SpResolveResult>
getQuote(): Promise<SpQuote>
getMode(): 'cart' | 'checkout' | 'checkoutPlus'
showModal(): Promise<void>
}
const sp = window.xcotton?.sp
if (!sp) throw new Error('[XC] SP is not ready')
MethodPurposeWrites cartTypical mode
addSpFee()Writes the fee line immediately. Idempotent when the fee is correctYescheckout+ with protection, custom toggle
removeSpFee()Closes intent and removes the fee line. No line present is not an errorYesCustom toggle, custom “without protection”
declineSpFee()Explicitly declines this checkout: clears the fee and writes the refusal markerYescheckout+ without protection
resolveSP()Aligns the fee line with the customer’s switch intent and returns the actionYescart, checkout
getQuote()Reads the current SP quote. Rejects when protection is unavailableNoAll modes
getMode()Returns the effective modeNoAll modes
showModal()Opens the SP coverage details modal (requires a mounted panel)NoAll modes

All methods are available in all three modes (resolveSP() rejects in checkout+). The “typical mode” column describes the intended use.

Explicitly requests the protection fee immediately, including the checkout+ “checkout with protection” entry. The SDK aligns the fee line with the current quote and succeeds idempotently when the correct fee already exists.

const sp = window.xcotton!.sp!
const { added, extId } = await sp.addSpFee()
// added is always true; extId is the confirmed SP variant ID (not a cart line ID) and is never 0
  • The promise rejects when the cart is not insurable (empty cart, subtotal outside the coverage range) or the write fails. It no longer resolves added: false. Handle the error code / recovery instead of assuming success.
  • The result does not include a full Cart.onCartChange reports the confirmed cart facts.

Closes the protection intent and removes the fee line immediately.

const sp = window.xcotton!.sp!
const { removed } = await sp.removeSpFee()
  • removed: true means the fee line was confirmed removed.removed: false means the cart had no fee line. Both are normal completion.
  • Even without a fee line the SDK still closes the intent and writes the required markers. A failed marker write rejects the promise.
  • Unlike declineSpFee(), removeSpFee() is a state operation (“turn protection off”), not the customer’s explicit “checkout without protection” choice.

Explicitly declines this checkout: clears the fee line and writes the refusal marker.

const sp = window.xcotton!.sp!
const { declined } = await sp.declineSpFee()
// declined is always true; failures reject the promise
ModeIntent handling
cartCloses and persists the switch intent so the next sync does not re-add the fee
checkoutKeeps the customer’s switch intent. This decline does not persist it as closed
checkout+Never reads, writes, or renews the switch cache. The two buttons express the choice
  • Once the fee removal and markers succeed, an unrelated re-quote failure does not turn the decline into a failure.
  • Do not chain resolveSP() after a decline: checkout keeps the original intent, so the next resolve would add protection again.

Aligns the fee line with the customer’s intent and returns the action result. The storefront decides whether to navigate.

type SpResolveResult =
| { action: 'added'; extId: number }
| { action: 'removed' }
| { action: 'skipped'; protection: 'included'; extId: number }
| { action: 'skipped'; protection: 'excluded' }
const sp = window.xcotton!.sp!
const result = await sp.resolveSP()
// added, removed, and skipped (the fee line already matches) all continue to checkout. A rejection never reaches here.
if (result.action === 'added' || result.action === 'removed' || result.action === 'skipped') {
window.location.href = '/checkout'
}
  • added: the fee line was written or adjusted.extId is the confirmed SP variant ID.
  • removed: the fee line was removed.
  • skipped / included: the fee line already matches the intent (required markers may still be written).
  • skipped / excluded: the customer does not want protection and the cart has no fee line.
  • When the intent is on but the cart is not insurable the promise rejects with a specific business error (for example SP_NOT_AVAILABLE). It is never reported as skipped. Read, quote, write, and confirmation failures also reject.
  • In checkout+ there is no switch intent, so the call rejects with INVALID_ARGUMENT. Call addSpFee() or declineSpFee() instead.

Synchronously returns the integration mode configured by the account manager (AM). 'checkoutPlus' is the checkout+ mode described in these docs.

Reads the current SP quote without changing cart attributes or cart lines, or taking the purchase flow’s write permission. It rejects if the cart session or known facts become invalid during the read, so it cannot return an old cart’s quote. Returns SpQuote:

{
"extId": 45678901234,
"totalPrice": "1.99",
"totalPriceInt": 199,
"currencyCode": "USD",
"currencySymbol": "$"
}
  • A resolved quote means the protection is currently available and extId is valid. An unavailable cart rejects with a specific business error (for example SP_NOT_AVAILABLE, with context.reason). extId: 0 and inRange no longer encode failure.
  • A valid zero-amount quote requires a valid extId and a numeric totalPriceInt of 0. Missing, non-finite, negative, or fractional amounts reject instead of becoming 0. An uninsurable cart is not a zero-amount quote.
  • A quote snapshot is not permanently valid. The SDK re-confirms against the current cart when writing the fee.

Opens the SP coverage details modal through the already-mounted panel.

  • At least one SP panel must be mounted and connected. Otherwise the promise rejects with SP_PANEL_REQUIRED (recovery.action: MOUNT_SP_PANEL).
  • The promise resolves once the modal has rendered. It does not wait for the customer to close it and does not mean protection was purchased.
  • Opening does not change intent or write the cart. Multiple panels share one modal, and it closes when the last panel unmounts.
  • The modal button uses the internal intent entry: cart expresses intent and writes the cart, checkout only expresses intent, and checkout+ does not show the intent button.
interface SpQuote {
extId: number
totalPrice: string
totalPriceInt: number
currencyCode: string
currencySymbol: string
}
FieldDescription
extIdSP variant ID. A resolved quote always has a valid value, 0 is no longer a success value
totalPriceDisplay string in the major currency unit. Below 10000 in the fixed ×100 protocol scale, it keeps 2 decimals. At or above 10000 it rounds to whole units and loses the fraction
totalPriceIntExact amount as an integer in “major unit × 100” (independent of the currency’s decimal places). Use it for all calculations and comparisons
currencyCodeCurrency code (for example 'USD', 'CAD'). The currency identity is defined by this field
currencySymbolDisplay-only symbol (for example '$'). CAD and USD both use $
BranchMeaning
{ action: 'added', extId }The fee line was written or adjusted.extId is the SP variant ID
{ action: 'removed' }The fee line was removed
{ action: 'skipped', protection: 'included', extId }An existing fee line already matches the intent
{ action: 'skipped', protection: 'excluded' }The customer wants no protection and no fee line exists

window.xcotton.pp becomes available as soon as init() resolves, with no cart required. It remains undefined when PP is disabled for the store.

The product-page convenience path is pp.addToCart. The methods below remain as atomic primitives for a custom add-to-cart UI or a storefront-drawn cart button, and compose to the same flow.

interface PpApi {
loadPlans(
variantId: string | number,
variantDetail?: PPVariantDetail,
): Promise<PPPlansResult>
loadPlans(variantId: null, variantDetail?: PPVariantDetail): Promise<null>
canBePopup(context?: PPProductContext): Promise<boolean>
getSelectedPlan(context?: PPProductContext): Promise<PPSelection | null>
setSelectedPlan(
selection: PPSelection | null,
context?: PPProductContext,
): Promise<void>
showModal(context?: PPProductContext): Promise<PPSelection | null>
addProtection(params: {
mainLineId: string
selection: PPSelection
}): Promise<{ added: true }>
removeProtection(params: {
mainLineId: string
}): Promise<PPRemoveProtectionResult>
getCartQuotes(): Promise<PPCartQuotesResult>
getRelations(): Promise<PPRelation[]>
offerProtection(params: {
mainLineId: string
}): Promise<PPOfferProtectionResult>
addToCart(options: PPAddToCartOptions): Promise<PPAddToCartResult>
}
MethodPurposeRequires a synchronized cart
loadPlans()Fetches quotes for the active variant and renders the PP panelNo
canBePopup()Reads whether the automatic modal condition is metNo
getSelectedPlan()Reads the current selection without opening a modalNo
setSelectedPlan()Sets or clears the current selection and updates the panelNo
showModal()Opens the plan selection modal on demandNo
addProtection()Writes protection for an explicit plan on a specific main lineYes
removeProtection()Cancels protection for a main product lineYes
getCartQuotes()Returns cart lines that can still add protection, with their quotesYes
getRelations()Returns mappings between protected main lines and protection linesYes
offerProtection()Cart add-protection orchestration: validate, open the modal, writeYes
addToCart()PDP convenience add-to-cart: selection, main product, sync, protectNo (the callback may create a cart)

Product-level quoting, selection, and modal methods never create or bind a cart. Cart protection methods require a bound cart.addToCart() may bind a cart created by its main-product callback.

Product-level quoting, selection, and modal methods never create or bind a cart. Cart protection methods require a bound cart.addToCart may bind a cart created by its main-product callback.

pp.addToCart({ context?, addMainProduct })

Section titled “pp.addToCart({ context?, addMainProduct })”

Convenience add-to-cart for product pages. The SDK locks the current product context, resolves the selection, calls your main product write once, then synchronizes the cart and writes protection. It performs the same business as the manual chain (getSelectedPlan() → write main product → syncCart() → addProtection()). Choose either path to fit your storefront.

When context is omitted, the SDK uses the product context that is still active when the call starts. Do not call this method from pages without a loadPlans context, such as a home page or a listing-page quick add. Write the main product and call syncCart() instead, and leave the protection entry to the cart. A missing context rejects with INVALID_ARGUMENT (path=context) and points to the plain add-to-cart path.

interface PPAddToCartOptions {
context?: PPProductContext
addMainProduct: (
attributes: AttributeInput[],
context: PPProductContext,
) => Promise<{ cartId: string; mainLineId: string }>
}
type PPAddToCartResult = { cartId: string; mainLineId: string } & {
protection: 'added' | 'skipped'
}
declare function addMainProduct(
variantId: string,
quantity: number,
attributes: AttributeInput[],
): Promise<{ cart: { id: string }; mainLine: { id: string } }>
const sdk = window.xcotton
if (!sdk?.pp) throw new Error('[XC] Product Protection is not enabled')
const result = await sdk.pp.addToCart({
addMainProduct: async (attributes, context) => {
const { cart, mainLine } = await addMainProduct(
context.mainVariantId,
1,
attributes,
)
return { cartId: cart.id, mainLineId: mainLine.id }
},
})
FieldDescription
contextOptional. Omit it to use the global product context established by loadPlans when the call starts. Pass a handle to validate its identity and generation. Missing or stale context rejects
addMainProductCallback that writes only the main product. Receives the line attributes (AttributeInput[], empty when no plan is selected) and the locked context. Called exactly once
cartIdCart ID in the result, taken from the callback return value (the callback may create the cart)
mainLineIdMain product line ID (CartLine.id) in the result. It can be a new line or an existing line Shopify merged into
protectionadded main product and protection are both confirmed.skipped no protection was selected and the main product plus sync completed

Order of operations:

  1. Lock the product context. Use an existing selection as-is. When none is selected and the back office allows the modal, open it first (cancelling still adds the main product), then pass selection.mainLineAttributes to the callback.
  2. Call addMainProduct(attributes, context) once.
  3. Synchronize the cart with the returned cartId and re-read the cart to confirm mainLineId matches this context’s variant.
  4. If there is a selection, write the protection line against the returned mainLineId.

Callback and failure contract:

  • The callback writes only the main product. Do not call the SDK from inside it (init, syncCart, pp.addToCart included), do not read the current page variant, and do not reset the PDP quote.
  • The SDK never replays the callback and never rolls back a main product that succeeded. The callback runs at most once.
  • Success resolves only added / skipped. Every failure rejects with an XcottonError. When the main product was confirmed but sync or protection failed, the error is PP_WRITE_CONFIRMATION_FAILED with stage, mainProduct, protection, and writeOutcome in its context. The main product stays in the cart, so do not invite the customer to add it again. Follow the recovery guidance and refresh the cart or retry only protection.
  • If operation permission expires before the callback starts, the SDK does not invoke it. Both mainProduct and writeOutcome are not_sent. If the callback ran but its result cannot be confirmed, the SDK uses available error metadata or reports unknown when evidence is insufficient. This does not prove that the main product was or was not written.
  • pp.addToCart and pp.offerProtection share the purchase-flow lock. Other selection or purchase entries reject with PP_SELECTION_IN_PROGRESS while it runs.

The purchase flow holds cart operation permission from selection through the main-product callback, synchronization, and protection confirmation. SP writes and external syncCart calls reject when they conflict. Do not call these SDK entries inside addMainProduct. Conflicts and timeouts never cause the SDK to replay the callback.

loadPlans(variantId) quotes the variant, makes it the global current PP context, and resolves to { context, options }. context is a handle managed by the SDK:

  • Omit context and the call captures the current global context when it starts. After awaiting it never drifts to a newer product.
  • Pass context explicitly and the call belongs to that product only. An unknown handle (not returned by the SDK) rejects with INVALID_ARGUMENT. An expired handle (after switching variants, leaving the product page, or A → B → A) rejects with PP_CONTEXT_CHANGED and never falls back to the global context.
  • Never construct a handle by hand or cache one across products.
const variantId = 'gid://shopify/ProductVariant/1234567890'
const { context, options } = await window.xcotton!.pp!.loadPlans(variantId)
await window.xcotton!.pp!.canBePopup(context)
await window.xcotton!.pp!.getSelectedPlan(context)
// A single-product page may omit context.
await window.xcotton!.pp!.canBePopup()

All objects returned by PP methods are defensive snapshots. Mutating them never affects internal SDK state.

Prices the product variant currently selected on the product page, renders the PP panel, and resolves to { context, options }:

  • context: an SDK-managed product context handle you can pass to the selection methods. It can also be omitted to use the global current context.

  • options: PPSelection[]. Each entry carries the plan and mainLineAttributes and can be submitted to setSelectedPlan directly, or read by a custom add-to-cart UI.

  • variantId: main product variant ID (string or number). The SDK resolves the product and price for the market configured at initialization.

  • Passing null signals leaving the product context: the SDK cancels any pending quote, closes any open plan modal, clears the PP panel and the current selection, and resolves to null.

  • A successful quote with no available plan resolves to { context, options: [] }. The context stays valid (canBePopup() is false, getSelectedPlan() is null, and showModal() rejects with PP_NO_OPTIONS).

  • A new call cancels any in-flight quote. A call superseded by a newer context rejects with PP_CONTEXT_CHANGED instead of resolving to an empty plan list.

  • Re-quoting the same variant keeps a still-valid selection. After the customer explicitly deselects, a re-quote does not reselect the hasSelectedDefault plan.

  • variantDetail (optional): pass when your storefront already holds the variant details for the active market to save one Storefront API lookup.

interface PPVariantDetail {
product: { id: string }
title: string
sku?: string | null
price: { amount: string; currencyCode: string }
}
FieldDescription
product.idGID of the product that owns the variant. Plans are resolved by it
titleVariant title
skuOptional variant SKU
price.amountVariant price in the active market as a Storefront decimal string (such as '29.99'), not cents
price.currencyCodePrice currency. Must match the currency configured at initialization

The SDK converts amount to an integer using the existing quote protocol’s fixed ×100 scale and validates the currency. A failed request, a backend rejection, or a response without a plan identity rejects with a specific error and never resolves to an empty plan list.

Reads whether the current product meets the back-office “open the modal on add to cart” condition. It never opens a modal, changes the selection, or writes the cart.

  • Returns false when the setting is off or no plan is available.true only means the condition held at query time.
  • Waits for the latest quote of the same variant before answering.
  • A quote or configuration failure rejects with a specific error instead of pretending to be false.
  • Switching variants or leaving the product page while waiting rejects with PP_CONTEXT_CHANGED.

Read-only: returns the selection established for the current variant, or null when nothing is selected or the customer cleared it.

  • Never selects a plan, opens a modal, or writes the cart.
  • Waits for the latest quote of the same variant and never returns a plan from a previous price.
  • An invalidated context (variant switch, page leave, A → B → A) rejects with PP_CONTEXT_CHANGED.null only means “confirmed no selection”.

Sets or clears the current product selection and updates the PP panel. It never opens a modal, adds to cart, or writes the cart.

  • Pass an option from options: the SDK verifies the candidate belongs to the target product and still matches the current quote before committing it. Do not assemble plans or line attributes yourself.
  • Pass null: clears the selection (same as a customer deselect). A later re-quote for the same variant does not reselect the default plan.
  • Setting the same valid candidate again, or clearing again, resolves successfully. Idempotency never bypasses validation.
  • Failures are specific: a foreign or stale plan rejects with INVALID_ARGUMENT (context.path points at a specific selection field, such as selection.mainVariantId, selection.plan.variantId, or a plan content field). An invalidated context rejects with PP_CONTEXT_CHANGED.
  • While a purchase flow or the selection modal is running, it rejects with PP_SELECTION_IN_PROGRESS and does not rewrite the selection being confirmed.
const variantId = 'gid://shopify/ProductVariant/1234567890'
const index = 0
const { context, options } = await window.xcotton!.pp!.loadPlans(variantId)
// When the customer picks one, submit the SDK-provided option as-is.
await window.xcotton!.pp!.setSelectedPlan(options[index]!, context)
const selection = await window.xcotton!.pp!.getSelectedPlan(context)
// When the customer deselects, clear it; the next add to cart still follows the back-office modal setting.
await window.xcotton!.pp!.setSelectedPlan(null, context)

A chosen plan resolves to an independent PPSelection:

{
"mainVariantId": "gid://shopify/ProductVariant/1234567890",
"plan": { "variantId": 45678901234, "name": "1 Year", "priceText": "$4.99" },
"mainLineAttributes": [{ "key": "_PlanID", "value": "…" }]
}

Captures the product context when called, awaits its latest quote, and unconditionally opens the plan selection modal, bypassing the automatic modal setting.

  • Confirming updates the SDK selection and the PDP panel and resolves to an independent PPSelection. Nothing is added to the cart.
  • Cancelling, closing, or pressing Escape resolves to null and clears the PDP selection. The cancellation is not recorded as a refusal: while nothing is selected, the next add to cart still follows the back-office modal setting.
  • With no available plan it rejects with PP_NO_OPTIONS instead of resolving to null.
  • Switching variants or leaving the product page while waiting rejects with PP_CONTEXT_CHANGED. Once the modal is open, any re-quote closes it and rejects with PP_CONTEXT_CHANGED, so the customer can never confirm a stale price. A later context’s selection is left untouched.
  • Selection exclusivity starts with waiting for the quote and ends on confirm, cancel, or failure: calling showModal() or setSelectedPlan() while it runs rejects immediately with PP_SELECTION_IN_PROGRESS (no queueing, no stacked modals).

A plan confirmed in the modal stays in the panel state, so the next pp.addToCart resolves the same selection. Your storefront neither stores the choice nor rewrites the main product attributes.

pp.addProtection({ mainLineId, selection })

Section titled “pp.addProtection({ mainLineId, selection })”

Writes protection for an explicit plan on the specified main product line. Resolves to { added: true }.

  • mainLineId: the main product line’s CartLine.id (never a variant ID).
  • selection: required, from valid loadPlans options or getCartQuotes candidates. Omitting it rejects with INVALID_ARGUMENT. The variant must match the target main line, and the plan is validated again before writing. Do not construct plans yourself or submit stale options.
  • Opens no modal and never borrows the PDP selection or a hidden candidate. Repeating the same plan is an idempotent success, and quantity is recomputed from the real cart across the shared group of the same variant and Plan ID. A line protected with a different plan rejects PP_ALREADY_PROTECTED; call removeProtection() first to switch.
  • A missing main line or a protection line ID rejects CART_LINE_NOT_FOUND. Invalid plan/main-line identity and write/confirmation failures each have specific errors.
  • Calling it while a purchase flow runs rejects PP_SELECTION_IN_PROGRESS. The SDK’s own composed flows are not rejected by their own lock.

Cancels protection for a main product line. Resolves to { status: 'removed' }, or { status: 'already_unprotected' } when the line has neither protection nor a link marker. mainLineId is the main line’s CartLine.id (not the protection line). The main product stays. The SDK clears only that line’s _PlanID, keeps your other attributes, and recomputes protection quantity across the remaining group.

After confirming cancellation, the SDK restores the line’s available offers as needed. A failed offer refresh does not undo removed.getCartQuotes() rejects until you call syncCart() after the service recovers.

Cart add-protection orchestration. Resolves { status: 'added' } or { status: 'cancelled' }.

  • Checks the cart add-protection switch, the main line, and the current valid quote in that order. A disabled switch or no candidate for that line rejects PP_NO_OPTIONS, never a customer cancellation.
  • Even a single plan opens the modal. Only after the customer confirms does it reuse addProtection. Cancelling resolves { status: 'cancelled' } with zero writes.
  • A line with real protection rejects PP_ALREADY_PROTECTED (based only on actual protection lines in the current cart, where a residual _PlanID is not protection). Ambiguous relations or quantity anomalies reject CART_SNAPSHOT_INCONSISTENT instead.
  • The selection comes from that main line’s cart quote and never switches or clears the PDP context or selection. If the main-line facts or quote become stale while the modal is open, the old selection rejects without writing. Synchronize again before starting another offer.
  • Shares the purchase-flow lock with pp.addToCart: other selection or purchase entries reject PP_SELECTION_IN_PROGRESS for the whole flow, including the write-confirmation stage.

Renamed from getCartOffers. Returns the PP cart quotes computed during the latest cart sync. These are main product lines in the cart that can still have protection added. Read-only in-memory read. It sends no network request.

{
"enabled": true,
"quotes": [
{
"mainLineId": "gid://shopify/CartLine/abc123",
"quantity": 1,
"options": [
{
"mainVariantId": "gid://shopify/ProductVariant/1234567890",
"plan": { "variantId": 45678901234, "name": "1 Year", "priceText": "$4.99" },
"mainLineAttributes": [{ "key": "_PlanID", "value": "…" }]
}
]
}
]
}
  • enabled only reports that the back-office in-cart add-protection switch is confirmed on. When it is off the result is { enabled: false, quotes: [] }. When it is on but there are no candidates the result is { enabled: true, quotes: [] }. The two empty results are not interchangeable.
  • An uninitialized SDK rejects with SDK_NOT_INITIALIZED, and no bound cart rejects with CART_NOT_BOUND. Not synchronized yet, sync in progress, and a stale snapshot each reject with PP_CART_QUOTES_NOT_READY, with context.reason set to not_synced, sync_in_progress, or stale. A failed latest sync rejects with PP_CART_QUOTES_SYNC_FAILED. An empty array never stands in for a failure.
  • Each entry’s options match the product-page quote shape and are complete PPSelection objects that can be submitted directly to addProtection. After a storefront cart write, await syncCart() before reading. A previous generation’s result does not represent the current cart.
  • When the read rejects, clear the previous candidates before showing the error. Do not leave an “Add protection” button that can only fail again. Read again after the service recovers and syncCart() completes.

Returns all protected main product lines in the current cart along with their linked protection lines. Unprotected lines are omitted. Because multiple main product lines of the same variant and plan share a single aggregated protection line, multiple entries can point to the same protectionLineId.

[
{
"mainLineId": "gid://shopify/CartLine/abc123",
"protectionLineId": "gid://shopify/CartLine/def456"
}
]

To render a “Remove protection” action alongside a main product line, match the line by mainLineId and pass that mainLineId to removeProtection({ mainLineId }).protectionLineId is for display only. When no cart is bound the call rejects with CART_NOT_BOUND. A read failure rejects with CART_SYNC_FAILED or the adapter’s specific error. Ambiguous protection groups reject with CART_SNAPSHOT_INCONSISTENT. A confirmed absence of relations resolves to []. Never use an empty array for a failure.

interface PPProductContext {
readonly mainVariantId: string
}
interface PPPlansResult {
context: PPProductContext
options: PPSelection[]
}
interface PPSelection {
mainVariantId: string
plan: PPPlanItem
mainLineAttributes: AttributeInput[]
}
interface PPPlanItem {
variantId: string | number
name?: string
priceInt?: number
priceText?: string
properties?: Record<string, string>
[key: string]: unknown
}
interface PPCartQuote {
mainLineId: string
quantity: number
options: PPSelection[]
}
type PPCartQuotesResult =
| { enabled: false; quotes: [] }
| { enabled: true; quotes: PPCartQuote[] }
interface PPRelation {
mainLineId: string
protectionLineId: string
}
type PPRemoveProtectionResult =
| { status: 'removed' }
| { status: 'already_unprotected' }
type PPOfferProtectionResult = { status: 'added' } | { status: 'cancelled' }
interface PPAddToCartOptions {
context?: PPProductContext
addMainProduct: (
attributes: AttributeInput[],
context: PPProductContext,
) => Promise<{ cartId: string; mainLineId: string }>
}
type PPAddToCartResult = { cartId: string; mainLineId: string } & {
protection: 'added' | 'skipped'
}
Type / fieldDescription
PPProductContext.mainVariantIdProductVariant GID behind a context handle. The handle is issued by the SDK and must not be constructed by hand
PPPlansResult.optionsFull plan snapshot of the current quote. Every entry can be submitted to setSelectedPlan
PPSelection.mainVariantIdProductVariant GID of the main product this selection belongs to. Check it against the target variant fixed at click time before writing the main product
PPSelection.planThe selected protection plan
PPSelection.mainLineAttributesMain line attributes as Storefront AttributeInput[]. The site passes them through during composed purchase, while direct addProtection fills in the association marker
PPPlanItem.variantIdVariant ID of the protection plan
PPPlanItem.nameDisplay name of the plan (such as '1 Year'). Not unique across products. Do not use it to link lines
PPPlanItem.priceIntPlan price as an integer in the fixed ×100 protocol scale
PPPlanItem.priceTextFormatted price string (such as '$4.99')
PPCartQuote.mainLineIdCartLine.id of an unprotected main product line eligible for protection
PPCartQuote.quantityQuantity of that main product line
PPCartQuote.optionsSelectable PPSelection entries for that line. Submit one directly to addProtection
PPRelation.mainLineIdCartLine.id of a protected main product line
PPRelation.protectionLineIdCartLine.id of its linked protection line (display only, and pass mainLineId to cancel protection)
PPOfferProtectionResultadded protection written and confirmed.cancelled customer cancelled with zero writes
PPAddToCartResult.protectionadded main product and protection both completed.skipped no protection selected this time

Failures from public SDK methods use a single XcottonError shape (name === 'XcottonError'). Before initialization the code is SDK_NOT_INITIALIZED. Site functions, a failed sdk.js load, or a missing window.xcotton raise ordinary exceptions outside this contract:

interface XcottonError {
name: 'XcottonError'
code: XcottonErrorCode
operation: string
message: string
context: { code: XcottonErrorCode } & Record<string, unknown>
recovery: { action: XcottonRecoveryAction; message: string }
cause?: unknown
}
  • code is the value your integration should branch on. context.code matches it, and narrowing by code exposes the fields specific to that failure.
  • message starts with [XC] and includes the specific cause. Use it for logs and display only. Never branch on message text.
  • Use the returned code, context, and available cause to identify the failure. Do not infer a more specific reason or write result than the upstream evidence provides.cause may be absent.
  • recovery.action suggests what to do next (such as SYNC_CART, WAIT_AND_RETRY, or CORRECT_ARGUMENT), and recovery.message is an English instruction you can show to developers.
  • For write failures, read context.writeOutcome: not_sent, not_applied (explicitly rejected, not applied), partial, or unknown (the write result cannot be confirmed). Never retry a write automatically after partial or unknown. Verify the cart first.
  • Narrow with window.xcotton.isXcottonError(error) instead of instanceof or message text.
const sdk = window.xcotton
if (!sdk) throw new Error('[XC] sdk.js has not loaded yet')
try {
await sdk.syncCart('gid://shopify/Cart/your-cart-id')
} catch (error) {
if (!sdk.isXcottonError(error)) throw error
switch (error.code) {
case 'SDK_NOT_INITIALIZED':
await sdk.init()
break
case 'ADAPTER_RATE_LIMITED':
// Wait for error.context.retryAfterMs before retrying
break
default:
console.error(error.code, error.message, error.recovery.action)
}
}
Error code or messageCauseFix
[XC] SDK initialization failed (module=…, stage=…, shop=…): …Initialization failed.module and stage identify the failing phase, and the text after the colon gives the reasonUse module, stage, causeCode, and the reason to locate the failure. Check shop, publicAccessToken, market values, and back-office configuration. Reload the page after the fix. If it persists, contact your account manager (AM)
[XC] SDK not initialized. Call xcotton.init() before using SDK operations.A cart or protection method ran before init() resolvedAwait init() first
[XC] syncCart requires a cartId before the first synchronization (…)syncCart was called without an argument and no cart is boundCall syncCart(cartId) for the first bind
[XC] Cart operation requires a bound cart; obtain a cart and call syncCart(cartId) first.A cart operation (such as addProtection) ran before the first synchronizationGet the cart and call syncCart(cartId). Do not call init again
[XC] cart currency does not match the configured market (…)The cart total currency differs from the configured currencyPass the market currency and make sure the cart belongs to that market
[XC] Cart session changed (…)The bound cart switched while an operation was in flightRe-run the operation against the newly bound cart
[XC] Cart requests are rate limited during … (retryAfterMs=…)Shopify or your API throttled cart requests. The SDK rejects calls during the cooldown (the adapter’s retry delay, or 30 seconds by default)Retry after the cooldown expires
[XC] pp.addToCart rejected an invalid argument (path=addMainProduct, …)pp.addToCart was called without a main product callbackPass a callback that writes only the main product and returns { cartId, mainLineId }
[XC] pp.addToCart rejected an invalid argument (path=context, …)pp.addToCart ran without an active product-page context, for example from a listing-page quick addCall pp.loadPlans for the target variant first, or add the main product and call syncCart
PP_WRITE_CONFIRMATION_FAILEDPurchase failed during the main-product, sync, or protection stage. The error alone does not prove that the main product was writtenFollow context.stage, mainProduct, writeOutcome, and recovery. Do not re-add a confirmed main product, and verify the cart when its result is unknown
PP_ALREADY_PROTECTEDThe cart line already has a real protection line (pp.offerProtection, or pp.addProtection with a different plan)Refresh the cart and hide the add-protection offer; call removeProtection first to switch plans
PP_SELECTION_IN_PROGRESSAnother PP selection or purchase flow is still runningWait for the active flow to settle before calling again
[XC] addProtection rejected an invalid argument (path=mainLineId, …)addProtection was called without mainLineIdPass the target main product line’s CartLine.id
[XC] addProtection rejected an invalid argument (path=selection, …)selection was missing or does not belong to that main lineUse the original option from loadPlans / getCartQuotes. Do not re-add a main product that already succeeded
[XC] removeProtection rejected an invalid argument (path=mainLineId, …)removeProtection was called without mainLineId, or a protection line was passedPass the target main product line’s CartLine.id
[XC] sp.resolveSP rejected an invalid argument (path=spMode, …): checkoutPlus has no switch intent; call addSpFee() or declineSpFee() insteadcheckout+ (checkoutPlus) has no switch intent, so resolveSP() is invalidCall addSpFee() or declineSpFee() from the two checkout buttons. Never infer the choice from the placeholder intent
[XC] line.id: duplicate or missing cart lineA line targeted for update or removal is missing from the current cart or duplicated in one requestVerify that your adapter returns the complete cart
[XC] updateLines: quantity or attributes requiredA line update omitted both quantity and attributesCheck your adapter’s updateLines implementation
[XC] Invalid MoneyV2 amountA monetary amount is not a valid decimal stringPreserve Storefront amount strings unchanged
[XC] …currencyCode: inconsistent currency (full path such as cart.cost.totalAmount.currencyCode)An adapter money field uses a different currency from the cart totalUse one currency everywhere. Do not convert amounts

Note: [XC] sdk.js has not loaded yet and [XC] SP is not ready are thrown by the defensive guards in this documentation’s example code, not by the SDK itself.