@commercengine/js
v0.6.4
Published
Commerce Engine Checkout - Embeddable checkout SDK
Maintainers
Readme
@commercengine/js
Embed Commerce Engine checkout in any website.
Installation
CDN (Recommended)
<script src="https://cdn.commercengine.com/v1.js" async></script>npm
npm install @commercengine/jsQuick Start
<script src="https://cdn.commercengine.com/v1.js" async></script>
<script>
window.Commercengine.onLoad = async () => {
const checkout = await Commercengine.init({
storeId: "store_xxx",
apiKey: "ak_xxx",
environment: "production", // or "staging"
});
document.getElementById("cart-btn").onclick = () => checkout.openCart();
};
</script>Configuration
CheckoutConfig
interface CheckoutConfig {
// === Credentials (required) ===
storeId: string; // Your Commerce Engine Store ID
apiKey: string; // Your Commerce Engine API Key
environment?: "production" | "staging"; // Default: "production"
// === Development ===
url?: string; // Direct checkout URL (for local dev)
// Overrides environment-based URL
// === Theme ===
theme?: "light" | "dark" | "system"; // Per-session override. Default: "system"
// Primary config via Checkout Studio (Config API)
// === Appearance ===
appearance?: {
zIndex?: number; // Overlay z-index. Default: 99999
};
// === Authentication ===
authMode?: "managed" | "provided"; // Default: "managed"
accessToken?: string; // Initial access token (if user logged in)
refreshToken?: string; // Initial refresh token
// === Quick Buy (add item on init) ===
quickBuy?: {
productId: string; // Product ID (required)
variantId: string | null; // Variant ID (required, null for non-variant)
quantity?: number; // Default: 1
};
sessionMode?: "continue-existing" | "force-new"; // Default: "continue-existing"
autoDetectQuickBuy?: boolean; // Auto-detect from parent URL. Default: false
// Drawer direction, features, and login methods are configured via
// Checkout Studio (Config API) and are not SDK options.
// === Callbacks ===
onReady?: () => void;
onOpen?: () => void;
onClose?: () => void;
onComplete?: (order: OrderData) => void;
onCartUpdate?: (cart: CartData) => void;
onTokensUpdated?: (data: AuthTokensUpdatedData) => void;
onLogin?: (data: AuthLoginData) => void;
onLogout?: (data: AuthLogoutData) => void;
onSessionError?: () => void;
onError?: (error: ErrorData) => void;
}Example: Full Configuration
const checkout = await Commercengine.init({
storeId: "store_xxx",
apiKey: "ak_xxx",
environment: "production",
theme: "dark",
appearance: {
zIndex: 100000,
},
accessToken: userSession?.accessToken,
refreshToken: userSession?.refreshToken,
onReady: () => {
console.log("Checkout loaded");
},
onComplete: (order) => {
window.location.href = `/thank-you?order=${order.orderNumber}`;
},
onCartUpdate: (cart) => {
updateCartBadge(cart.count);
},
onTokensUpdated: ({ accessToken, refreshToken }) => {
// Tokens changed - sync to your auth system (fires on every token change)
saveUserSession(accessToken, refreshToken);
},
onLogin: ({ user, loginMethod }) => {
// User logged in via checkout - update UI
console.log("User logged in via", loginMethod, ":", user?.email || user?.phone);
},
onLogout: () => {
// User logged out - update UI for anonymous state
},
onSessionError: () => {
// Session corrupted - SDK cleared all tokens, user needs to re-authenticate
clearUserSession();
},
});Methods
openCart()
Open the cart drawer.
checkout.openCart();openCheckout()
Open checkout directly, bypassing cart. Use for "Buy Now" flows.
checkout.openCheckout();close()
Close the checkout overlay.
checkout.close();updateTokens(accessToken, refreshToken?)
Sync authentication state from parent site. Call when the user logs in/out on
your site, or when an async session bootstrap completes after init().
// When user logs in on your site
checkout.updateTokens(session.accessToken, session.refreshToken);
// When user logs out
checkout.updateTokens("", "");Pre-ready safe. Calls made before onReady fires are buffered (latest
pair wins) and delivered to the iframe as soon as it announces it can receive
messages — typically well before full hydration. This unlocks the
parallel-init pattern, where iframe
boot and async token fetch run concurrently instead of serially.
addToCart(productId, variantId, quantity?)
Add an item to cart and open the cart drawer. This is the canonical implementation of "quick buy" functionality.
// Add a product (non-variant)
checkout.addToCart("prod_123", null, 1);
// Add a product variant
checkout.addToCart("prod_123", "var_456", 2);
// Quantity defaults to 1
checkout.addToCart("prod_123", "var_456");Parameters:
productId(string, required) - Product IDvariantId(string | null, required) - Variant ID, ornullfor non-variant productsquantity(number, optional) - Quantity to add, defaults to 1
Behavior:
- Adds item to cart and automatically opens the cart drawer
- If item already in cart: adds to existing quantity
- If item not in cart: adds new item
- If no cart exists: creates cart with the item
getCart()
Get current cart state.
const cart = checkout.getCart();
console.log(cart.count, cart.total, cart.currency);getTokens()
Get the latest token payload seen by the SDK from auth:tokens-updated.
This avoids race conditions when .on("auth:tokens-updated") is attached after init().
Returns null if no token update event has been received yet.
const checkout = await Commercengine.init({ ... });
const tokens = checkout.getTokens();
// tokens.accessToken, tokens.refreshTokendestroy()
Remove checkout iframe and cleanup event listeners.
checkout.destroy();Properties
| Property | Type | Description |
|----------|------|-------------|
| ready | boolean | Whether checkout is initialized and ready |
| open | boolean | Whether checkout overlay is currently visible |
Events
Subscribe via callbacks (in config) or the event emitter.
Event Types
| Event | Callback | Data | Description |
|-------|----------|------|-------------|
| ready | onReady | - | Checkout iframe loaded |
| open | onOpen | - | Drawer opened |
| close | onClose | - | All drawers closed |
| complete | onComplete | OrderData | Order placed successfully |
| cart:updated | onCartUpdate | CartData | Cart state changed |
| auth:tokens-updated | onTokensUpdated | AuthTokensUpdatedData | Tokens changed (any reason) |
| auth:login | onLogin | AuthLoginData | User logged in (semantic) |
| auth:logout | onLogout | AuthLogoutData | User logged out (semantic) |
| auth:session-error | onSessionError | - | Session corrupted, tokens cleared |
| error | onError | ErrorData | Configuration error |
Event Data Types
interface OrderData {
id: string; // Order ID
orderNumber: string; // Human-readable order number
}
interface CartData {
count: number; // Number of items
total: number; // Subtotal amount
currency: string; // Currency code (e.g., "USD", "INR")
}
interface AuthTokensUpdatedData {
accessToken: string;
refreshToken?: string;
}
interface AuthLoginData {
user?: UserInfo;
loginMethod?: LoginMethod;
}
interface AuthLogoutData {
user?: UserInfo; // Anonymous user info
}
interface ErrorData {
message: string;
}
type LoginMethod = "whatsapp" | "phone" | "email";
interface Channel {
id: string;
name: string;
type: string;
}
interface UserInfo {
id: string;
email: string | null;
phone: string | null;
username: string;
firstName: string | null;
lastName: string | null;
storeId: string;
isLoggedIn: boolean;
isAnonymous: boolean;
customerId: string | null;
customerGroupId: string | null;
anonymousId: string;
channel: Channel;
tokenExpiry: Date;
tokenIssuedAt: Date;
}Event Emitter API
// Subscribe
checkout.on("cart:updated", (cart) => {
console.log("Cart:", cart.count, cart.total);
});
// Subscribe once
checkout.once("complete", (order) => {
console.log("Order:", order.orderNumber);
});
// Unsubscribe
const handler = (cart) => console.log(cart);
checkout.on("cart:updated", handler);
checkout.off("cart:updated", handler);Authentication Flow
Scenario 1: User Not Logged In
User will be prompted to login within checkout (WhatsApp, Phone, or Email OTP).
const checkout = await Commercengine.init({
storeId: "store_xxx",
apiKey: "ak_xxx",
onTokensUpdated: ({ accessToken, refreshToken }) => {
// Sync tokens to your auth system (fires on login, logout, refresh)
saveSession({ accessToken, refreshToken });
},
onLogin: ({ user, loginMethod }) => {
// Update UI with user info
console.log("Logged in via", loginMethod, ":", user?.email);
},
});Scenario 2: User Already Logged In
Pass tokens to skip login screen.
const checkout = await Commercengine.init({
storeId: "store_xxx",
apiKey: "ak_xxx",
accessToken: currentSession.accessToken,
refreshToken: currentSession.refreshToken,
});Scenario 3: Sync Auth Changes
When user logs in/out on your site, sync to checkout.
// User logs in on your site
myAuth.onLogin((session) => {
checkout.updateTokens(session.accessToken, session.refreshToken);
});
// User logs out on your site
myAuth.onLogout(() => {
checkout.updateTokens("", "");
});Features
Features (loyalty, coupons, collect-in-store, free shipping progress, product recommendations) are configured via Checkout Studio (Config API). They are not SDK options.
| Feature | Default | Description |
|---------|---------|-------------|
| loyalty | true | Loyalty points redemption at checkout |
| coupons | true | Coupon and promo code input |
| collectInStore | false | Option to collect order in-store |
| freeShippingProgress | true | Progress bar showing amount needed for free shipping |
| productRecommendations | true | "You may also like" product carousel in cart |
TypeScript
All types are exported:
import {
Commercengine,
Checkout,
type CheckoutConfig,
type CheckoutEventType,
type CheckoutFeatures,
type DrawerDirection,
type DrawerDirectionConfig,
type Environment,
type CartData,
type OrderData,
type AuthTokensUpdatedData,
type AuthLoginData,
type AuthLogoutData,
type ErrorData,
type UserInfo,
type Channel,
type LoginMethod,
} from "@commercengine/js";URL Parameters (Advanced)
For direct iframe integration without the SDK, checkout uses a subdomain-based URL scheme. The store ID and environment are encoded in the hostname — they are not URL parameters.
URL format: https://{storeId}.checkout.commercengine.com?api_key=ak_xxx
Staging: https://{storeId}.staging.checkout.commercengine.com?api_key=ak_xxx
| Parameter | Description | Example |
|-----------|-------------|---------|
| api_key | API Key (required) | ak_xxx |
| mode | Deployment mode | iframe |
| parent_origin | Parent origin for postMessage | https://brand.com |
| theme | Theme preference (per-session override) | light, dark |
| token | Access token | JWT string |
| refresh_token | Refresh token | JWT string |
| auth_mode | Auth management | provided, managed |
| product_id | Quick buy product | Product ID |
| variant_id | Quick buy variant | Variant ID |
| qty | Quick buy quantity | 1 |
| session_mode | Session behavior | continue-existing, force-new |
Features (loyalty, coupons, etc.) and drawer direction are configured via Checkout Studio (Config API) and are not accepted as URL parameters.
Auth Modes
managed(default): Checkout manages token lifecycle with auto-refresh. Best for lightweight integrations (marketing pages, buy-now buttons)provided(recommended for storefront apps): Parent manages tokens via storefront SDK, checkout uses in-memory only. Tokens are synced viaupdateTokens()andonTokensUpdated
Session Modes
continue-existing(default): Add quick-buy item to existing cartforce-new: Delete existing cart and start fresh with only the quick-buy item
Note: When quick buy params are provided (via config or auto-detected), the cart drawer automatically opens once checkout is ready.
Auto-Detect Quick Buy
For ad-driven traffic, enable autoDetectQuickBuy to automatically parse quick buy params from the parent page URL:
// Ad link: https://brand.com?product_id=prod_123&variant_id=var_456&qty=2
const checkout = await Commercengine.init({
storeId: "store_xxx",
apiKey: "ak_xxx",
autoDetectQuickBuy: true, // Reads product_id, variant_id, qty from parent URL
});This allows embedded checkout to handle ad clicks natively without custom URL parsing.
URL Cleanup: When quick buy params are detected, they are automatically removed from the browser URL (using replaceState) to prevent duplicate adds on page refresh.
Performance
Two tuning levers for time-to-interactive on the embedded checkout. Both are optional and additive.
Preconnect to the checkout origin
The first time a user visits a page that embeds checkout, the browser pays a
cold DNS + TLS handshake to a brand-new origin ({storeId}.checkout.commercengine.com).
Warming that connection in <head> before the iframe is created shaves the
handshake cost off the first render.
Two helpers are exported for building the origin URL without coupling to the internal subdomain convention:
import { getCheckoutOrigin, getCheckoutIframeUrl } from "@commercengine/js";
const origin = getCheckoutOrigin({
storeId: "store_xxx",
environment: "production", // or "staging"
});
// → "https://store_xxx.checkout.commercengine.com"
const iframeUrl = getCheckoutIframeUrl({
storeId: "store_xxx",
apiKey: "ak_xxx",
environment: "production",
});
// → "https://store_xxx.checkout.commercengine.com?api_key=ak_xxx&mode=iframe"CDN consumers can access the same helpers via the global:
<script src="https://cdn.commercengine.com/v1.js" async></script>
<script>
const origin = Commercengine.getCheckoutOrigin({
storeId: "store_xxx",
environment: "production",
});
// …inject <link rel="preconnect" href={origin} crossorigin> into <head>
</script>Drop the origin into your root layout (Next.js / Remix / etc.):
<link rel="preconnect" href={origin} crossOrigin="anonymous" />
<link rel="dns-prefetch" href={origin} />For more aggressive warmup (downloads the iframe document on idle bandwidth),
use getCheckoutIframeUrl(...) with <link rel="prefetch" as="document">.
Tradeoff: costs bandwidth even if the user never opens cart.
Parallel init (cold visitor speedup)
When the parent app uses authMode: "provided" and does not have a cached
token in storage, the typical flow is serial:
// Serial: anonymous-token fetch blocks iframe creation
const { accessToken, refreshToken } = await fetchAnonymousToken();
const checkout = await Commercengine.init({
storeId, apiKey, authMode: "provided", accessToken, refreshToken,
});updateTokens() being pre-ready safe
lets you reorder this so the iframe boot and token fetch overlap:
// Parallel: init starts loading the iframe immediately;
// updateTokens flushes once the iframe is ready to receive.
const checkout = await Commercengine.init({
storeId, apiKey, authMode: "provided",
// No accessToken — iframe will wait for tokens via postMessage
});
const { accessToken, refreshToken } = await fetchAnonymousToken();
checkout.updateTokens(accessToken, refreshToken);@commercengine/js buffers the call, and as soon as the iframe announces it's
listening (an internal handshake event), the tokens flush via postMessage. The
iframe then hydrates and emits ready normally.
When this helps: first-time visitors with no token in storage. The anonymous-token round-trip (~100-300ms typical) overlaps with iframe boot instead of blocking it.
When this doesn't help: returning visitors with a cached token — pass it
upfront via accessToken as usual; nothing to parallelize.
