@copecart/sdk
v0.2.0
Published
Browser SDK for COPE Cart V1 API
Downloads
253
Readme
@copecart/sdk
Browser SDK for the COPE Cart V1 API. Zero dependencies, ~2 KB gzipped. Add products to cart, calculate prices, and either redirect to hosted checkout or mount it in an iframe — all from your own website.
Table of Contents
- Installation
- Quick Start
- Getting Started
- Configuration
- API Reference
- Types
- Error Handling
- Retry & Idempotency
- Cart Lifecycle
- Browser Support
Installation
npm / pnpm (bundler projects)
npm install @copecart/sdkimport { CopeCart } from '@copecart/sdk'CDN (no build step)
<script src="https://unpkg.com/@copecart/sdk@latest/dist/index.global.js"></script>
<script>
const cope = new CopeCart.CopeCart({ publishableKey: 'cope_pk_...' })
</script>When loaded via CDN, the SDK is available as
window.CopeCart. The main class isCopeCart.CopeCart.
Quick Start
import { CopeCart, CopeApiError } from '@copecart/sdk'
// 1. Initialize
const cope = new CopeCart({
publishableKey: 'cope_pk_your_key_here',
})
// 2. Look up a product (public, no cart required)
const product = await cope.getProduct('prod_VdSQGSJu')
// 3. Create a cart
const cart = await cope.createCart({ currency: 'EUR' })
// 4. Add a product with a specific payment plan
await cope.addLine(cart.id, {
product_id: 'prod_VdSQGSJu',
plan_id: product.payment_plans[0].id,
})
// 5. Set buyer info (required for tax calculation)
await cope.setBuyerIdentity(cart.id, {
email: '[email protected]',
country: 'DE',
postal_code: '10115',
})
// 6. Calculate totals (tax, shipping, discounts)
const repriced = await cope.reprice(cart.id)
console.log(repriced.totals!) // { subtotal_minor_units, tax_minor_units, total_minor_units, ... }
// 7. Create checkout session
const checkout = await cope.checkout(cart.id, {
success_url: 'https://your-site.com/thank-you',
cancel_url: 'https://your-site.com/cart',
consents: [{ type: 'buyer_tos' }],
})
// 8. Redirect to payment page
cope.redirectToCheckout(checkout)After payment, the buyer is redirected to success_url with ?order_id=<uuid> appended.
Prefilled Checkout for External Services
COPE-1260 adds a single helper for server-side TypeScript adapters that already know the buyer, selected product, selected plan, and desired checkout handoff. It calls one backend idempotent handoff endpoint:
- create cart,
- add selected product/plan,
- set buyer identity,
- reprice,
- create hosted checkout.
import { CopeCart } from '@copecart/sdk'
const cope = new CopeCart({
publishableKey: process.env.COPE_PUBLISHABLE_KEY!,
baseUrl: 'https://app.cope.com/gateway/cart_api',
checkoutBaseUrl: 'https://app.cope.com',
})
const handoff = await cope.createPrefilledCheckout({
// Stable caller reference: reused as deterministic idempotency input.
externalReference: 'tr-event-session-0001',
currency: 'EUR',
locale: 'en-US',
intendedPaymentMethod: 'terminal',
metadata: {
source: 'event_sales',
event: {
id: 'tr-2026-london',
seller_id: 'seller-42',
contract_token_id: 'contract-token-abc',
badge_customer_ref: 'badge-123',
product_group: 'vip',
selected_product: 'prod_VdSQGSJu',
},
},
line: {
productId: 'prod_VdSQGSJu',
planId: 123,
quantity: 1,
},
buyerIdentity: {
email: '[email protected]',
first_name: 'Ada',
last_name: 'Lovelace',
country: 'DE',
postal_code: '10115',
buyer_type: 'b2c',
billing_address: {
line1: 'Hauptstrasse 1',
city: 'Berlin',
postal_code: '10115',
country: 'DE',
},
},
checkout: {
success_url: 'https://example.com/thank-you',
cancel_url: 'https://example.com/cancelled',
consents: [{ type: 'buyer_tos' }],
},
})
console.log(handoff.checkoutUrl)
console.log(handoff.checkoutToken)The returned checkoutUrl is suitable for seller-device redirect. A calling service can render checkoutUrl as a QR code for customer-device checkout. checkoutToken is exposed for integrations that need to build their own handoff envelope, but the URL should be preferred when possible. The prefilled response intentionally does not include cart_secret.
externalReference is required and should be stable for the external sales session or contract redemption attempt. The SDK sends it as external_reference, derives the backend Idempotency-Key, and stores the reference as cart metadata. Replaying the same payload returns the stored handoff response; replaying the same key with a different body returns an idempotency mismatch instead of creating or mutating another cart/session. Keep references unique within the first 120 sanitized characters because that prefix is used in the stable idempotency key.
Pass exactly one of line or lines. line is the single-line shorthand; lines is for one or more line inputs. Supplying both fields, even with an empty lines array, is rejected before the API call.
Only use non-sensitive reconciliation references in metadata. Buyer PII belongs in buyerIdentity, where the checkout and tax flows already expect it. external_reference and intended_payment_method are reserved metadata keys; use the top-level externalReference and intendedPaymentMethod fields instead. This helper does not implement contract QR validation, terminal reader control, or SAP integration.
Getting Started
Step 1: Get Your Publishable Key
Each business receives a unique publishable key (cope_pk_...). Obtain it from Business Settings in the COPE admin panel.
The publishable key is safe to use in frontend code. It identifies your business but cannot perform privileged operations.
Step 2: Install the SDK
Option A — Bundler (recommended):
npm install @copecart/sdkimport { CopeCart } from '@copecart/sdk'
const cope = new CopeCart({
publishableKey: 'cope_pk_your_live_key',
})Option B — CDN (script tag):
<script src="https://unpkg.com/@copecart/sdk@latest/dist/index.global.js"></script>
<script>
const cope = new CopeCart.CopeCart({
publishableKey: 'cope_pk_your_live_key',
})
</script>Step 3: Build the Checkout Flow
Typical integration:
import { CopeCart, CopeApiError, CopeCartExpiredError } from '@copecart/sdk'
const cope = new CopeCart({ publishableKey: 'cope_pk_live_key' })
// Display product info on your page
const product = await cope.getProduct('prod_YourProductUUID')
renderProductCard(product) // your UI function
// Track cart ID yourself — the SDK persists the secret but not a public cart ID accessor
let cartId: string | null = null
// When user clicks "Add to Cart"
async function handleAddToCart(productUuid: string, planId: number) {
try {
// Create cart if not exists
if (!cartId) {
const cart = await cope.createCart({ currency: 'EUR' })
cartId = cart.id
}
await cope.addLine(cartId, { product_id: productUuid, plan_id: planId })
} catch (e) {
if (e instanceof CopeApiError && e.code === 'duplicate_line') {
showToast('Product already in cart')
}
}
}
// When user clicks "Checkout"
async function handleCheckout(cartId: string) {
// 1. Set buyer identity (collect from your form)
await cope.setBuyerIdentity(cartId, {
email: buyerEmail,
country: buyerCountry,
postal_code: buyerPostalCode,
})
// 2. Calculate final prices
const repriced = await cope.reprice(cartId)
showOrderSummary(repriced.totals!) // show tax breakdown to buyer
// 3. Create checkout session
const checkout = await cope.checkout(cartId, {
success_url: 'https://your-site.com/order-confirmation',
cancel_url: 'https://your-site.com/cart',
consents: [{ type: 'buyer_tos' }],
})
// 4. Redirect to Stripe
cope.redirectToCheckout(checkout)
}Step 4: Handle the Thank-You Page
After successful payment, the buyer is redirected to your success_url with the order ID appended:
https://your-site.com/order-confirmation?order_id=ord_abc123// thank-you.ts
const params = new URLSearchParams(window.location.search)
const orderId = params.get('order_id')
if (orderId) {
showConfirmation(`Order ${orderId} placed successfully!`)
}Step 5: SPA Cleanup
In single-page applications, destroy the SDK instance on route change to abort in-flight requests and prevent memory leaks:
// React example
useEffect(() => {
const cope = new CopeCart({ publishableKey: 'cope_pk_...' })
// ... use cope
return () => cope.destroy()
}, [])Configuration
new CopeCart(options)
| Option | Type | Required | Default | Description |
|--------|------|----------|---------|-------------|
| publishableKey | string | Yes | — | Business API key. Must start with cope_pk_. |
| baseUrl | string | No | https://app.cope.com/gateway/cart_api | COPE gateway URL. Override only for custom deployments. |
| checkoutBaseUrl | string | No | baseUrl origin | Base URL for checkout page redirect. Used to validate checkout URL origin. |
API Reference
Products
cope.getProduct(productUuid): Promise<SdkProduct>
Fetch product details by UUID. Public endpoint — no cart or authentication required.
const product = await cope.getProduct('prod_VdSQGSJu')
product.uuid // 'prod_VdSQGSJu'
product.name // 'My Course'
product.headline // 'Learn everything' or null
product.product_type // 'digital_product'
product.currency // 'USD'
product.payment_methods // ['card']
product.images // [{ id: 1, image_url: 'https://...' }]
product.payment_plans // see SdkPaymentPlan type belowEach payment plan includes pricing info:
product.payment_plans[0].id // 42
product.payment_plans[0].plan_type // 'one_time' | 'subscription' | 'installment'
product.payment_plans[0].first_payment_amount_cents // '49.99' (decimal string, major units despite field name)
product.payment_plans[0].interval // null | 'day' | 'week' | 'month' | 'year'
product.payment_plans[0].vanity_prices // multi-currency pricesCart
cope.createCart(payload): Promise<Cart>
Create a new cart. The cart_secret is automatically persisted to localStorage for subsequent authenticated requests.
const cart = await cope.createCart({
currency: 'EUR', // required — ISO 4217 (EUR, USD, CHF)
locale: 'de-DE', // optional — BCP 47 locale
attribution: { // optional — marketing attribution
affiliate_id: 'aff_123',
promo_code: 'SAVE20',
utm_source: 'google',
utm_medium: 'cpc',
utm_campaign: 'spring_sale',
utm_content: 'banner_1',
utm_term: 'online course',
landing_url: window.location.href,
referrer: document.referrer,
captured_at: new Date().toISOString(), // optional — when attribution was captured
},
})cope.getCart(cartId): Promise<Cart>
Retrieve an existing cart. The cart_secret is automatically loaded from localStorage.
Throws CopeCartExpiredError if the cart is in a terminal state (ordered, expired, abandoned, merged) and clears localStorage.
try {
const cart = await cope.getCart(cartId)
} catch (e) {
if (e instanceof CopeCartExpiredError) {
// Cart was completed or expired — create a new one
const newCart = await cope.createCart({ currency: 'EUR' })
}
}Line Items
cope.addLine(cartId, payload): Promise<Cart>
Add a product to the cart. Throws duplicate_line if the same product is already in the cart.
const cart = await cope.addLine(cartId, {
product_id: 'prod_VdSQGSJu', // required — product UUID
plan_id: 42, // optional — payment plan ID from getProduct; omit for default plan
quantity: 1, // optional — defaults to 1
})cope.updateLine(cartId, lineId, payload): Promise<Cart>
Change quantity or switch payment plan for an existing line item.
// Change quantity
await cope.updateLine(cartId, lineId, { quantity: 3 })
// Switch payment plan
await cope.updateLine(cartId, lineId, { plan_id: 55 })cope.removeLine(cartId, lineId): Promise<Cart>
Remove a line item from the cart.
const cart = await cope.removeLine(cartId, lineId)Typical quantity control pattern:
async function changeQuantity(cartId: string, lineId: string, newQty: number) {
if (newQty <= 0) {
return cope.removeLine(cartId, lineId)
}
return cope.updateLine(cartId, lineId, { quantity: newQty })
}Buyer Identity
cope.setBuyerIdentity(cartId, payload): Promise<Cart>
Set or update buyer information. email, country, and postal_code are required before calling reprice() or checkout().
await cope.setBuyerIdentity(cartId, {
email: '[email protected]', // required for checkout
country: 'DE', // required for tax calculation (ISO 3166-1 alpha-2)
postal_code: '10115', // required for tax calculation
buyer_type: 'b2c', // optional — 'b2c' or 'b2b'
company: 'Acme GmbH', // optional — for B2B
shipping_address: { // optional — for physical goods (all fields optional)
line1: 'Friedrichstr. 123',
line2: 'Apt 4B', // optional
city: 'Berlin',
region: 'BE',
postal_code: '10115',
country: 'DE',
},
billing_address: { ... }, // optional — same structure as shipping (all fields optional)
consents: [{ type: 'marketing' }], // optional — additional consents
})Pricing
cope.reprice(cartId): Promise<Cart>
Recalculate totals (subtotal, tax, shipping, discounts). Call after any change to lines, buyer identity, or promo codes.
Requires buyer identity with country and postal_code set.
const cart = await cope.reprice(cartId)
// totals is null until reprice() is called — use non-null assertion after reprice
const totals = cart.totals!
totals.subtotal_minor_units // 2999 (€29.99)
totals.tax_minor_units // 570 (€5.70)
totals.shipping_minor_units // 0 (€0.00)
totals.discount_minor_units // 0 (€0.00)
totals.total_minor_units // 3569 (€35.69)
totals.currency // 'EUR'
totals.charge_currency // 'EUR' (currency used for Stripe charge)
totals.finality // 'estimate' | 'final'All monetary values are in minor units (cents). Divide by 100 for display:
total_minor_units / 100.
cope.applyPromoCode(cartId, code): Promise<Cart>
Apply a promotional code. Call reprice() after to see updated discount.
await cope.applyPromoCode(cartId, 'SAVE20')
const repriced = await cope.reprice(cartId)
console.log(repriced.totals!.discount_minor_units) // discount amountcope.removePromoCode(cartId): Promise<Cart>
Remove the currently applied promo code.
Checkout
cope.checkout(cartId, payload): Promise<CheckoutResult>
Create a hosted checkout session. Returns a redirect URL and an iframe URL.
const checkout = await cope.checkout(cartId, {
success_url: 'https://your-site.com/thank-you', // optional — redirect after payment
cancel_url: 'https://your-site.com/cart', // optional — redirect on cancel
embed_origin: window.location.origin, // optional — required before mounting in an iframe
consents: [{ type: 'buyer_tos', version: '1.0' }], // required — buyer ToS (version optional)
})
checkout.checkoutId // 'co-001'
checkout.checkoutUrl // 'https://app.cope.com/checkout/token123' — redirect route
checkout.embedCheckoutUrl // 'https://app.cope.com/checkout/embed/token123' — iframe route
checkout.status // 'open'
checkout.expiresAt // '2026-03-16T10:30:00Z' (30-minute expiry)
checkout.totals // final CartTotals snapshotRedirect URLs: If success_url is provided, the buyer is redirected there after payment with ?order_id=<uuid> appended. If omitted, the default COPE thank-you page is shown.
HTTPS required for redirect URLs in production. http://localhost is allowed in development.
Embedded checkout: Pass embed_origin: window.location.origin when creating checkout. COPE validates that exact origin against the business' registered embed domains. When the create response includes embed.parent_origin, the SDK exposes it as checkout.embedOrigin. The SDK refuses to mount when checkout.embedOrigin is missing or does not exactly match window.location.origin. The SDK mounts the frontend checkout page at /checkout/embed/:token; the checkout page owns the internal API fetch and embed policy validation.
cope.redirectToCheckout(checkout): void
Navigate the browser to the hosted checkout page.
cope.redirectToCheckout(checkout)
// Browser navigates to checkout.checkoutUrlcope.mountCheckout(checkout, options): MountedCheckout
Mount the hosted checkout iframe into your page. Use the CheckoutResult returned by checkout().
const checkout = await cope.checkout(cartId, {
embed_origin: window.location.origin,
consents: [{ type: 'buyer_tos' }],
})
const mounted = cope.mountCheckout('#checkout-frame', checkout, {
fallback: 'redirect',
onReady: () => showCheckout(),
onResize: ({ height }) => console.log(`Checkout height: ${height}px`),
onSuccess: () => handleCheckoutSuccess(),
onCancel: () => handleCheckoutCancel(),
onError: ({ code, retryable }) => showCheckoutError(code, retryable),
onTerminal: ({ status }) => handleCheckoutTerminalStatus(status),
})
// Later, for SPA route changes or modal close:
mounted.destroy()mountCheckout() creates an iframe with allow="payment *" and referrerPolicy="no-referrer". It does not add a sandbox attribute because no sandbox token set has been proven compatible with all required wallet, redirect, and payment provider flows yet. The embed is constrained by the registered embed_origin contract plus strict postMessage origin/source checks instead.
After mounting, the SDK sends { type: 'copecart.checkout.mount', version: 1, token } to the iframe with targetOrigin set to checkout.embedCheckoutUrl origin. It retries briefly until the iframe reports ready, then stops. It never posts with wildcard target origin. If the iframe never reports ready, fallback: 'redirect' navigates to checkout.checkoutUrl; fallback: 'error' calls onError({ code: 'load_failed', retryable: true }).
Messages from the iframe are accepted only when all of these checks pass:
event.originmatchescheckout.embedCheckoutUrloriginevent.sourceis the mounted iframe windowevent.data.source === 'cope.checkout'event.data.version === 1
Callbacks receive sanitized events only:
type CheckoutEmbedEvent =
| { type: 'ready'; messageId?: string; timestamp?: string }
| { type: 'error'; payload: { code: 'not_found' | 'load_failed' | 'embed_not_allowed' | 'confirm_failed'; retryable: boolean }; messageId?: string; timestamp?: string }
| { type: 'terminal'; payload: { status: 'expired' | 'completed' | 'unavailable' | 'cancelled' | 'processing' }; messageId?: string; timestamp?: string }
| { type: 'resize'; payload: { height: number }; messageId?: string; timestamp?: string }Resize messages update the mounted iframe height automatically. The optional
messageId and timestamp metadata can be used by advanced integrations for
deduplication or stale-message detection.
onSuccess and onCancel are convenience callbacks derived from terminal events. The SDK never forwards checkout tokens, cart secrets, payment client secrets, or buyer PII through embed callbacks.
cope.cancelCheckout(checkoutId): Promise<void>
Cancel an open checkout and return the cart to active state. Throws checkout_not_cancellable if the checkout is already completed or expired.
await cope.cancelCheckout(checkout.checkoutId)
// Cart is back to 'active' — can modify and checkout againCleanup
cope.destroy(): void
Abort all in-flight requests, remove mounted checkout iframes, clear localStorage, and mark the instance as destroyed. All subsequent API calls throw CopeError('SDK destroyed').
cope.destroy()Types
All types are exported from the package and available for import:
import type {
// Core data
Cart,
CartLine,
CartTotals,
BuyerIdentity,
Address,
Consent,
PriceSnapshot,
CartState, // 'active' | 'checkout_in_progress' | 'ordered' | 'expired' | 'abandoned' | 'merged'
TotalsFinality, // 'estimate' | 'final'
// Products
SdkProduct,
SdkPaymentPlan,
SdkProductImage,
SdkVanityPrice,
// Checkout
CheckoutResult,
CheckoutStatus, // 'open' | 'completed' | 'expired' | 'cancelled'
CheckoutCreateResponse,
CheckoutCreateEmbedPolicy,
CheckoutEmbedEvent,
CheckoutEmbedErrorCode,
CheckoutEmbedErrorPayload,
CheckoutEmbedTerminalPayload,
CheckoutMountMessage,
MountCheckoutOptions,
MountedCheckout,
// Payloads (input types)
CopeCartOptions,
CreateCartPayload,
CartAttribution,
AddLinePayload,
UpdateLinePayload,
UpdateBuyerIdentityPayload,
CheckoutPayload,
// Errors
ApiErrorCode,
} from '@copecart/sdk'Key Type Details
Cart — the main cart object:
interface Cart {
id: string
state: CartState
version: number
currency: string // display currency (EUR, USD, CHF)
charge_currency: string // currency used for Stripe charge
cart_secret?: string // only present on createCart response
lines: CartLine[]
buyer_identity: BuyerIdentity | null
totals: CartTotals | null // null until reprice() is called
locale: string
promo_code: string | null
}CartLine — a line item in the cart:
interface CartLine {
id: string // use for updateLine/removeLine
cart_id: string
product_id: number // internal integer FK
plan_id: number | null
quantity: number
price_snapshot: PriceSnapshot | null // populated after reprice()
product_uuid: string // the UUID you used in addLine
stale?: boolean // true if price needs recalculation
requires_shipping?: boolean
trial_days?: number
payment_methods: string[]
plan_type: string | null // 'one_time' | 'subscription' | 'installment'
}BuyerIdentity — buyer information attached to the cart:
interface BuyerIdentity {
email: string
country: string // ISO 3166-1 alpha-2
postal_code: string | null
company: string | null
buyer_type: 'b2b' | 'b2c' | null
billing_address: Address | null
shipping_address: Address | null
consents: Consent[] // server-enriched consents (see below)
}Consent — consent record (response from server, enriched with metadata):
interface Consent {
type: string // e.g., 'buyer_tos', 'marketing'
granted_at: string // ISO 8601 timestamp (set by server)
ip: string // buyer IP address (set by server)
}Note: When sending consents (in
setBuyerIdentityorcheckout), you only provide{ type: string }. Thegranted_atandipfields are populated by the server in the response.
Address — billing or shipping address:
interface Address {
line1: string
line2: string | null
city: string
region?: string | null
postal_code: string
country: string // ISO 3166-1 alpha-2
}PriceSnapshot — price captured at the time of reprice:
interface PriceSnapshot {
unit_price_minor_units: number // per-unit price in cents
currency: string // display currency
charge_currency: string // Stripe charge currency
product_name: string // product name at capture time
plan_name: string | null // payment plan name
captured_at: string // ISO 8601 timestamp
}CartTotals — pricing breakdown (all values in minor units / cents):
interface CartTotals {
subtotal_minor_units: number
tax_minor_units: number
shipping_minor_units: number
discount_minor_units: number
total_minor_units: number
currency: string
charge_currency: string
finality: 'estimate' | 'final'
}SdkPaymentPlan — product payment options:
interface SdkPaymentPlan {
id: number // use as plan_id in addLine
plan_type: 'one_time' | 'subscription' | 'installment'
position: number // display order
currency: string
display_name: string | null
first_payment_amount_cents: string // '49.99' (decimal string, major units despite field name)
next_payments_amount_cents: string | null // for subscription/installment (major units)
next_payments_count: number | null // number of remaining installments
shipping_price_cents: string // '0.00' (major units despite field name)
charge_shipping_once: boolean
shipping_depend_on_quantity: boolean
interval: 'day' | 'week' | 'month' | 'year' | null
interval_count: number | null
second_payment_in: number | null // days until second payment
vanity_prices: SdkVanityPrice[] // prices in other currencies
}SdkVanityPrice — price in an alternative display currency:
interface SdkVanityPrice {
id: number
currency: string // e.g., 'EUR' when base is 'USD'
first_payment_amount_cents: string // '45.99' (major units despite field name)
next_payments_amount_cents: string | null
}Error Handling
The SDK throws four error classes. CopeError is the base class; three specialized subclasses handle specific scenarios:
import { CopeError, CopeApiError, CopeNetworkError, CopeCartExpiredError } from '@copecart/sdk'
try {
await cope.addLine(cartId, { product_id: 'invalid', plan_id: 1 })
} catch (e) {
if (e instanceof CopeApiError) {
// Server returned an error response
e.code // 'not_saleable', 'duplicate_line', etc. (ApiErrorCode)
e.message // Human-readable message from server
e.status // HTTP status code (401, 404, 422, 429, 500, etc.)
e.field // Field name if validation error (e.g., 'product_id')
e.requestId // X-Request-Id header — include in support requests
e.errors // Full error array: [{ code, message, field? }]
}
if (e instanceof CopeNetworkError) {
// Connection failed (timeout, offline, DNS resolution)
e.message // 'Failed to fetch', 'Request timed out', etc.
}
if (e instanceof CopeCartExpiredError) {
// Cart is in a terminal state — must create a new cart
e.state // 'ordered' | 'expired' | 'abandoned' | 'merged'
}
if (e instanceof CopeError) {
// Base error — thrown for SDK configuration issues:
// - Invalid publishableKey (missing or wrong prefix)
// - HTTP page (non-localhost)
// - Calling methods after destroy()
// - Checkout URL origin mismatch
e.message
}
}Error Codes Reference
| Code | HTTP Status | When |
|------|-------------|------|
| unauthorized | 401 | Invalid/missing publishable key or cart secret |
| not_found | 404 | Product or cart not found |
| cart_not_active | 422 | Cart is not in active state |
| duplicate_line | 422 | Same product already in cart |
| not_saleable | 422 | Product is not available for purchase |
| empty_cart | 422 | Attempted checkout with no line items |
| incomplete_buyer_identity | 422 | Missing email, country, or postal code |
| incomplete_shipping_address | 422 | Physical product requires complete shipping address |
| missing_consent | 422 | Checkout requires buyer_tos consent |
| below_minimum_charge | 422 | Order total below Stripe's minimum charge amount |
| invalid_redirect_url | 422 | success_url or cancel_url is malformed |
| invalid_promo_code | 422 | Promo code does not exist or is expired |
| missing_code | 422 | Promo code request missing the code field |
| missing_tax_location | 422 | Buyer country/postal code needed for reprice |
| checkout_expired | 410 | Checkout session expired (30-minute limit) |
| checkout_not_open | 422 | Checkout is not in open state |
| checkout_not_cancellable | 422 | Cannot cancel a completed/expired checkout |
| cart_id_mismatch | 422 | Cart ID in URL doesn't match cart secret |
| validation_error | 422 | Generic field validation failure |
| rate_limited | 429 | Too many requests — retry after delay |
| tax_service_unavailable | 503 | Tax calculation service temporarily down |
| payment_provider_error | 422 | Stripe returned an error |
Retry & Idempotency
Automatic Retries
The SDK retries failed requests once with exponential backoff + jitter:
| Request Type | Retried on 5xx/429? | Retried on network error? |
|-------------|---------------------|--------------------------|
| GET (getCart, getProduct) | Yes | Yes |
| POST (createCart, addLine, reprice, applyPromoCode) | Yes | Yes |
| POST to /checkout or /cancel | No | No |
| PATCH (updateLine, setBuyerIdentity) | No | No |
| DELETE (removeLine, removePromoCode) | No | No |
- Backoff: 500ms base, doubles per attempt, max 5s, with random factor between 0.5x and 1.0x
- Request timeout: 8 seconds per attempt
- 4xx errors (except 429) are never retried
Idempotency Keys
All POST requests include an Idempotency-Key header (UUID v4). The same key is reused across retries, ensuring the server processes the request exactly once even if the client retries.
Cart Lifecycle
┌─────────┐
│ (new) │
└────┬─────┘
│ createCart()
▼
┌──────────────┐
┌───▶│ active │◀──── cancelCheckout()
│ └──────┬───────┘
│ │ checkout()
│ ▼
│ ┌────────────────────┐
│ │ checkout_in_progress│
│ └───────┬────────────┘
│ │
│ ┌─────┴──────┐
│ ▼ ▼
│ ┌─────────┐ ┌─────────┐
│ │ ordered │ │ expired │ (terminal states)
│ └─────────┘ └─────────┘
│
│ ┌───────────┐ ┌────────┐
└──│ abandoned │ │ merged │ (terminal states)
└───────────┘ └────────┘checkout_in_progress state:
- Not a terminal state —
getCart()returns normally - The buyer is on the Stripe payment page
- You can call
cancelCheckout()to return the cart toactivestate - If the checkout expires (30 minutes) without payment, the cart returns to
activeautomatically
Terminal states (ordered, expired, abandoned, merged):
getCart()throwsCopeCartExpiredErrorlocalStorageis automatically cleared- You must call
createCart()to start a new cart
Cart secret persistence:
- Stored in
localStorageunder keycope_cartas{ cartId, secret } - Automatically saved on
createCart()response - Automatically loaded on SDK initialization
- Automatically cleared on terminal cart state or
destroy() - Only one cart can be active at a time (new cart overwrites previous)
Browser Support
| Requirement | Minimum | |-------------|---------| | Chrome | Last 2 versions | | Firefox | Last 2 versions | | Safari | Last 2 versions | | Edge | Last 2 versions |
Required browser APIs: fetch, localStorage, crypto.randomUUID, AbortSignal.any
Bundle size: ~2 KB gzipped, zero runtime dependencies.
Dual ESM/CJS: The SDK ships as both ES module and CommonJS. Use import for ESM or require() for CJS. The IIFE CDN build is also available for <script> tags.
