Skip to content

Use your own cart API

By default, the SDK reads and writes the cart directly through the Shopify Storefront API. No adapter is needed. Implement XcottonCartAdapter only when cart operations must pass through your own API or BFF (for example, to attach server-side credentials, enforce custom validation, or run side effects on cart writes), and pass it to init.

An adapter has five methods that mirror the Storefront Cart API:

MethodStorefront query / mutationReturn value
getCart(cartId)cartCart or null
addLines(cartId, lines)cartLinesAddThe updated Cart
updateLines(cartId, lines)cartLinesUpdateThe updated Cart
removeLines(cartId, lineIds)cartLinesRemoveThe updated Cart
updateAttributes(cartId, attributes)cartAttributesUpdateThe updated Cart

The SDK automatically passes cartId from the currently bound cart, so your adapter never needs to track the active cart ID on its own.

  • The store is running SDK v0.3. If you are migrating a v0.2 adapter, read Upgrade from v0.2 to v0.3 first.
  • Your backend endpoints can query the cart, add/update/remove lines, and replace cart-level attributes.
  • Write endpoints return the complete Cart structure expected by the SDK.getCart may also return null (see Cart). Type-check your adapter against XcottonCartAdapter from xcotton-types.ts.
  1. Implement the five adapter methods, forwarding the SDK’s arguments to your backend and returning the full updated Cart.
  2. Register the adapter once per page load via init({ adapter, onCartChange }). You can initialize before a cart exists, since PP product page pricing does not require a cart.
  3. Call syncCart(cart.id) after every successful storefront cart write, including the first bind and cart switches. Await completion. The SDK decides whether this is a first bind, a cart switch, or a same-cart refresh.
  4. Inside onCartChange, only refresh your UI. Never call syncCart from onCartChange, and never re-emit SDK writes as storefront cart events (see Keep the cart in sync).

Any method you omit falls back to the SDK’s built-in Storefront API implementation. When mixing custom and default methods:

  • All methods must operate on the same Storefront cart, use the same line IDs, and hold valid credentials.
  • If your BFF rewrites or synthesizes custom line IDs rather than returning native Storefront CartLine.id values, the built-in methods won’t recognize them. You must implement every line-based method yourself.
  • If a custom method throws or rejects, the SDK will not fall back to the built-in Storefront method.
OperationRule
Add a linePass merchandiseId as a full ProductVariant GID.quantity is optional and defaults to 1
Update a lineAddress the line by its current CartLine.id. Supply quantity, attributes, or both. quantity: 0 removes the line: the SDK passes it to updateLines unchanged, so your implementation must treat it as a deletion
Remove a linePrefer removeLines
Locating linesNever look up a line by variant ID. One variant can span multiple lines, and attribute updates may change line IDs. Never turn a failed update into an add
Line attributesOmitting or passing null preserves existing attributes.[] clears them. A non-empty array replaces the entire list
Cart attributesupdateAttributes always receives the complete attribute list ([] clears all attributes). The SDK pre-merges its own keys, so write the array as-is without re-reading and merging
Attribute valuesValues must be strings. Duplicate keys and invalid quantities are rejected by the SDK before calling your adapter
ScenarioContract requirement
getCart cannot find the cartReturn null only when you have confirmed the cart does not exist. Return an empty cart as a complete Cart with lines.nodes: [], not null. The SDK rejects operations that need a real cart with CART_NOT_FOUND
Network error, auth failure, rate limitReject the Promise. Never return null on transient errors, or the SDK will treat the cart as deleted
Write succeededReturn the complete post-mutation Cart. Returning null or undefined violates the contract: the SDK will re-read the cart to recover state, but still rejects the call with ADAPTER_RESPONSE_INVALID
Write failedReject the Promise. Because a failed write may have partially applied on the server, disable automatic retries in your HTTP client. The SDK will not replay or roll back failed writes
API throttledReject an Error with at least status: 429 or code: 'THROTTLED' (a message containing 429 / THROTTLED is also recognized). With retryAfterMs, the SDK uses that cooldown. Otherwise it applies the default 30-second cooldown

The SDK validates every returned Cart. If a read result fails validation, the SDK re-reads once through the adapter and recovers when that read passes. If a write result fails validation, the SDK rejects the operation:

  • cart.id must match the currently bound cart ID.
  • The cart total currency (cost.totalAmount.currencyCode) must match the window.xcottonConfig.currency configured at initialization. The built-in and custom adapters follow the same rule, and a mismatch rejects before quoting or writing.
  • All cart line pages must be fetched (lines.pageInfo.hasNextPage must be false), with no duplicate line IDs.
  • Monetary fields must preserve Storefront MoneyV2 formatting: amount is a decimal string, and every currencyCode must match the cart total’s currency. Do not multiply amounts by 100 or substitute non-zero prices for zero-cost lines.
  • Preserve every required field in the Cart contract. Pricing also reads shipping eligibility (requiresShipping), gift card flag (isGiftCard), SKU, title, vendor, productType, discountCodes, and line cost. Product identity (product.id, product.handle), line quantity, and attributes are required too. Legacy v0.2 Ajax Cart fields are not needed.

Write outcomes are reported as four states: not sent, explicitly rejected without effect, partially or fully confirmed, and unknown. The SDK puts the state into context.writeOutcome when it can confirm it. If a method returns null / undefined, or a post-write pagination read fails, the SDK performs a single read-only recovery read and still rejects the operation. It never replays the mutation.

Custom adapters own pagination and must return a complete Cart with lines.pageInfo.hasNextPage === false. The SDK validates only that final snapshot. Cross-checking cart-level facts (total, attributes, discount codes) across pages is a capability of the default adapter while it fetches pages, and the SDK never reads the intermediate pages of a custom adapter. When a read snapshot is incomplete, pagination is invalid, or line IDs are duplicated, the SDK re-reads once through the adapter. This lowers the mixed-snapshot risk but is not a cross-page transaction. Synchronization and writes on the same cart are coordinated by the SDK: starting another cart write while one is in flight rejects with OPERATION_IN_PROGRESS, so your adapter must not rely on its own queue to serialize them.

In the example below, myApi represents your storefront’s cart client. The example imports it from lib/cart.ts, so adjust that path for your project. The second file replaces lib/xcotton.ts from the Quick start. The change imports cartAdapter and passes adapter: cartAdapter to init.

lib/cart-adapter.ts
import type { XcottonCartAdapter } from './xcotton-types'
import { myApi } from './cart'
// The SDK passes the bound cartId; every write returns the full Cart.
export const cartAdapter: XcottonCartAdapter = {
getCart: (cartId) => myApi.getCart(cartId),
addLines: (cartId, lines) => myApi.addLines(cartId, lines),
updateLines: (cartId, lines) => myApi.updateLines(cartId, lines),
removeLines: (cartId, lineIds) => myApi.removeLines(cartId, lineIds),
updateAttributes: (cartId, attributes) =>
myApi.updateAttributes(cartId, attributes),
}
lib/xcotton.ts
import type { XcottonApi, XcottonCartAdapter } from './xcotton-types'
import { cartAdapter } from './cart-adapter'
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({
adapter: cartAdapter,
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)
}
ActionExpected result
Add the same variant as two separate lines, then update/remove eachEach operation affects only the targeted line
Test a cart with more lines than a single GraphQL pagelines.nodes contains all lines and hasNextPage is false
Change a main product line’s quantityThe linked protection line’s quantity updates to match
Remove protection for one main line (removeProtection)The main line stays, its _PlanID is cleared while other attributes remain, and other lines in the shared group keep their protection
Empty the cart in cart mode, then syncNo leftover fee line remains
Simulate a dropped write responseNo duplicate items are added, failure isn’t shown as success, and the user can continue after a re-read
Start another cart write while an SDK sync or write is in flightThe later request rejects with OPERATION_IN_PROGRESS and the adapter receives a single mutation
Change cart attributes or the total while the default adapter pages through a cartThe SDK discards the mixed result and re-reads a bounded number of times without replaying writes. A custom adapter must keep its own pages consistent
Trigger an SDK cart writeBoth the cart page and drawer update together, and callback errors are caught
Error messageCause
[XC] line.id: duplicate or missing cart lineA line ID targeted for update/removal is missing from the returned cart, or appears twice in one request
[XC] updateLines: quantity or attributes requiredA line update omitted both quantity and attributes
Contains response does not match bound cartThe returned cart.id does not match the currently bound cart ID
Contains inconsistent currencyA monetary field’s currencyCode differs from the cart total’s currency
Contains cart currency does not match the configured marketThe cart total currency differs from window.xcottonConfig.currency, so the currency guard rejected it
[XC] Cart requests are rate limitedThe adapter returned a throttling signal (429 / THROTTLED). The SDK entered its cooldown, or this request is being rejected during that cooldown
Contains duplicate line or duplicate attributeA line ID or attribute key is duplicated in the returned payload

For complete type signatures, see the SDK Reference.