Skip to main content
Astro does not need a dedicated checkout binding. Use the root @commercengine/checkout helpers (initCheckout, getCheckout, subscribeToCheckout) directly in layout scripts or client modules.

NPM Package

@commercengine/checkout

Root checkout helpers for Astro. Import from @commercengine/checkout (not a framework-specific subpath).

Installation

npm install @commercengine/checkout

Setup

Call initCheckout once in your root layout script. Listen for astro:page-load so checkout re-initializes after ClientRouter page swaps:
src/layouts/Layout.astro
---
// src/layouts/Layout.astro
---

<html lang="en">
  <head><slot name="head" /></head>
  <body>
    <slot />

    <script type="module">
      import { initCheckout } from "@commercengine/checkout";

      function init() {
        initCheckout({
          storeId: "store_xxx",
          apiKey: "ak_xxx",
          environment: "production",
          onComplete: (order) => {
            window.location.href = `/thank-you?order=${order.orderNumber}`;
          },
        });
      }

      init();
      document.addEventListener("astro:page-load", init);
    </script>
  </body>
</html>
Feature flags, login methods, drawer direction, and payment provider are managed in Checkout Studio (Config API), not as SDK init options. Use theme only for per-session overrides.

Imperative API

Since there is no framework-specific hook, use getCheckout() and subscribeToCheckout() for imperative control:
src/components/CartButton.astro
<button id="cart-btn" disabled>Cart (0)</button>

<script>
  import { getCheckout, subscribeToCheckout } from "@commercengine/checkout";

  const btn = document.getElementById("cart-btn")!;

  subscribeToCheckout(
    (state) => state.isReady,
    (isReady) => {
      (btn as HTMLButtonElement).disabled = !isReady;
    }
  );

  subscribeToCheckout(
    (state) => state.cartCount,
    (count) => {
      btn.textContent = `Cart (${count})`;
    }
  );

  btn.addEventListener("click", () => getCheckout().openCart());
</script>

Add to cart

src/components/AddToCartButton.astro
<button class="add-to-cart" data-product-id="product_abc" data-variant-id="variant_xyz">
  Add to Cart
</button>

<script>
  import { getCheckout } from "@commercengine/checkout";

  document.querySelectorAll(".add-to-cart").forEach((btn) => {
    btn.addEventListener("click", () => {
      const productId = btn.getAttribute("data-product-id")!;
      const variantId = btn.getAttribute("data-variant-id");
      getCheckout().addToCart(productId, variantId, 1);
    });
  });
</script>

Framework islands

If you use React, Vue, Svelte, or Solid islands inside Astro, you can use the corresponding framework binding’s hook/composable/store inside the island. The checkout instance is shared — initCheckout only needs to be called once in the layout.
src/components/CartButton.tsx
// React island example
import { useCheckout } from "@commercengine/checkout/react";

export function CartButton() {
  const { openCart, cartCount, isReady } = useCheckout();
  return (
    <button onClick={openCart} disabled={!isReady}>
      Cart ({cartCount})
    </button>
  );
}
src/pages/index.astro
---
import { CartButton } from "../components/CartButton";
---

<CartButton client:load />

Storefront SDK integration

If your Astro app also uses the @commercengine/storefront SDK, you must use authMode: "provided" with two-way token sync. Create a client module that owns both the storefront session and checkout init:
src/lib/storefront-client.ts
import { getCheckout, initCheckout } from "@commercengine/checkout";
import { BrowserTokenStorage, createStorefront, Environment } from "@commercengine/storefront";

const tokenStorage = new BrowserTokenStorage("myapp_");

const storefront = createStorefront({
  storeId: import.meta.env.PUBLIC_STORE_ID ?? "",
  apiKey: import.meta.env.PUBLIC_API_KEY ?? "",
  environment:
    import.meta.env.PUBLIC_CE_ENV === "staging" ? Environment.Staging : Environment.Production,
  session: {
    tokenStorage,
    onTokensUpdated: (accessToken, refreshToken) => {
      getCheckout().updateTokens(accessToken, refreshToken);
    },
  },
});

const sessionSdk = storefront.session();
let initPromise: Promise<void> | null = null;

export function initHostedCheckout() {
  if (initPromise) return initPromise;

  initPromise = (async () => {
    const accessToken = await sessionSdk.ensureAccessToken();
    const refreshToken = await tokenStorage.getRefreshToken();

    initCheckout({
      storeId: import.meta.env.PUBLIC_STORE_ID ?? "",
      apiKey: import.meta.env.PUBLIC_API_KEY ?? "",
      environment: import.meta.env.PUBLIC_CE_ENV === "staging" ? "staging" : "production",
      authMode: "provided",
      accessToken: accessToken ?? undefined,
      refreshToken: refreshToken ?? undefined,
      onTokensUpdated: ({ accessToken, refreshToken }) => {
        void sessionSdk.setTokens(accessToken, refreshToken);
      },
    });
  })().catch((error) => {
    initPromise = null;
    throw error;
  });

  return initPromise;
}
Then call it from the layout:
src/layouts/Layout.astro
<script>
  import { initHostedCheckout } from "@/lib/storefront-client";

  function bootstrap() {
    void initHostedCheckout();
  }

  bootstrap();
  document.addEventListener("astro:page-load", bootstrap);
</script>
If your Astro app uses the first-party @commercengine/storefront/astro wrapper instead of the base SPA SDK, see the Astro SDK Integration page for the equivalent pattern using storefront.bootstrap() and clientStorefront().

Preserving the iframe across ClientRouter navigations

Astro’s ClientRouter replaces document.body on each navigation, which remounts any body-mounted checkout iframe. If remounting is acceptable, no extra work is needed. If you need to keep the iframe alive across page transitions, install a custom swap that only replaces your page content region instead of the full body:
1

Wrap page content in a data attribute

src/layouts/Layout.astro
<body>
  <div data-page-content>
    <slot />
  </div>

  <!-- checkout iframe lives outside data-page-content -->
  <script type="module">
    // ... initCheckout / initHostedCheckout here
  </script>
</body>
2

Add a custom swap handler

src/layouts/Layout.astro
<script>
  import { swapFunctions } from "astro:transitions/client";

  document.addEventListener("astro:before-swap", (event) => {
    event.swap = () => {
      swapFunctions.deselectScripts(event.newDocument);
      swapFunctions.swapRootAttributes(event.newDocument);
      swapFunctions.swapHeadElements(event.newDocument);
      const restoreFocus = swapFunctions.saveFocus();

      const nextContent = event.newDocument.querySelector("[data-page-content]");
      const currentContent = document.querySelector("[data-page-content]");

      if (nextContent && currentContent) {
        swapFunctions.swapBodyElement(nextContent, currentContent);
      } else {
        swapFunctions.swapBodyElement(event.newDocument.body, document.body);
      }

      restoreFocus();
    };
  });
</script>
This swaps only the [data-page-content] region, leaving the checkout iframe (and any other body-level elements) untouched.

API reference

Since Astro uses the root helpers, the imperative API is slightly different from the framework-specific hooks:
FunctionDescription
initCheckout(options)Create the checkout iframe. Call once at startup.
getCheckout()Returns the checkout instance for imperative control.
subscribeToCheckout(selector, callback)Subscribe to state changes with a selector function.
destroyCheckout()Remove the iframe and clean up all listeners.
getCheckout() methods:
MethodDescription
openCart()Open the cart drawer
openCheckout()Open checkout directly (skips cart)
close()Close the overlay
addToCart(productId, variantId, quantity?)Add an item to the cart
updateTokens(accessToken, refreshToken?)Sync auth tokens from your app
getCart()Returns { count, total, currency }
getTokens()Returns latest { accessToken, refreshToken } or null
destroy()Remove the iframe and clean up
subscribeToCheckout state properties:
PropertyTypeDescription
isReadybooleantrue when the checkout iframe is loaded
isOpenbooleantrue when the overlay is visible
cartCountnumberNumber of items in the cart
cartTotalnumberCart subtotal
cartCurrencystringCurrency code
isLoggedInbooleanWhether a user is logged in
userUserInfo | nullCurrent user info