npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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

npm / pnpm (bundler projects)

npm install @copecart/sdk
import { 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 is CopeCart.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:

  1. create cart,
  2. add selected product/plan,
  3. set buyer identity,
  4. reprice,
  5. 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/sdk
import { 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 below

Each 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 prices

Cart

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 amount

cope.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 snapshot

Redirect 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.checkoutUrl

cope.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.origin matches checkout.embedCheckoutUrl origin
  • event.source is the mounted iframe window
  • event.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 again

Cleanup

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 setBuyerIdentity or checkout), you only provide { type: string }. The granted_at and ip fields 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 to active state
  • If the checkout expires (30 minutes) without payment, the cart returns to active automatically

Terminal states (ordered, expired, abandoned, merged):

  • getCart() throws CopeCartExpiredError
  • localStorage is automatically cleared
  • You must call createCart() to start a new cart

Cart secret persistence:

  • Stored in localStorage under key cope_cart as { 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.