> ## Documentation Index
> Fetch the complete documentation index at: https://docs.totebot.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Cart bridge

> Add the chat widget and share the shopper's cart with the agent.

Medusa is headless, so Totebot can't auto-discover the shopper's cart the way it can on Shopify or WooCommerce. You add a small **bridge** to your storefront that tells the widget which cart the shopper is using. Once it's in place, the agent and the in-chat product cards can read and update the same cart the shopper sees.

There are three steps: load the widget, expose the shopper's cart, and react to the agent's cart changes. Steps 2 and 3 are implemented together — see the [complete examples](#4-complete-examples) for your stack.

> [!WARNING]
>
> **Allow your storefront's origin (CORS) — required.** The widget reads the shopper's cart directly from your Medusa server (the `baseUrl` you set below), so your storefront's origin must be in your Medusa server's `STORE_CORS`. If it isn't, cart reads fail with a browser CORS error and the in-chat cart stays empty. Most storefronts are already listed (your own site needs it to load the cart at all); add yours if it isn't — for example `STORE_CORS=https://yourstore.com,https://www.yourstore.com` — then restart Medusa.

## 1. Load the widget

Inject the widget script dynamically once the page has loaded:

```html theme={null}
<script>
  (function () {
    function loadTotebot() {
      var s = document.createElement("script");
      s.src = "https://app.totebot.ai/embed.min.js";
      s.id = "YOUR_AGENT_ID";
      s.setAttribute("domain", "totebot.ai");
      document.body.appendChild(s);
    }
    if (document.readyState === "complete") loadTotebot();
    else window.addEventListener("load", loadTotebot);
  })();
</script>
```

## 2. Expose the cart

Tell the widget which cart the shopper is using by setting `window.__totebotMedusa` and dispatching `totebot:medusa-cart`. `cartId` is not a value you hard-code — read it from wherever your storefront keeps the shopper's current cart (a cookie, your cart store, `localStorage`, etc.). Leave it `undefined` when the shopper has no cart yet; the agent creates one on first add and hands it back via the `totebot:medusa-cart-adopt` event (step 3).

Push on load, and **again whenever your cart changes** so the agent always sees the latest cart. The lines marked `YOU:` are where you plug in your own cart logic — Totebot can't know how your storefront stores its cart.

```html theme={null}
<script>
  function pushTotebotCart() {
    window.__totebotMedusa = {
      // YOU: return your storefront's current cart id, or undefined if none exists yet.
      cartId: getCurrentCartId() || undefined,
      baseUrl: "https://your-medusa-server.com",
      publishableKey: "pk_...",
      // The URL that actually renders your checkout. On the Medusa Next.js
      // starter that is /<region>/checkout?step=address (see note below).
      checkoutUrl: "https://your-store.com/checkout",
      // Optional. Custom storefronts only. On the Medusa Next.js starter the
      // region is auto-detected from the /<countryCode>/ URL, so you can omit this.
      regionId: "reg_..."
    };
    window.dispatchEvent(new Event("totebot:medusa-cart"));
  }

  pushTotebotCart();              // run once on load
  // YOU: call pushTotebotCart() again whenever your storefront's cart changes
  // (after your own add/update/remove), so the agent always sees the latest cart.
</script>
```

This snippet shows the shape. For a full implementation wired to your stack, see the [complete examples](#4-complete-examples).

## 3. Live refresh & cart bootstrap

When the agent (or an in-chat add) changes the cart, the write happens on Totebot's backend (out-of-band from the shopper's browser), so your storefront's mini-cart would otherwise stay stale. Totebot pushes two browser events to close that gap — handle both in your storefront (see the complete examples):

* **`totebot:medusa-cart-refresh`** — fired after any cart write. Re-read your cart and re-render your mini-cart. This is the Medusa analog of WooCommerce's `wc_fragment_refresh`. *Example: the shopper's mini-cart says "2 items"; they ask the agent to add a tee; the icon updates to "3 items" without a reload.*

  **The rule:** if your storefront reads the cart through a **server-side cache** that isn't auto-invalidated on out-of-band writes, you must **bust that cache** in this handler — otherwise repeat writes to the same cart stay stale. If your storefront **fetches the cart fresh** each time (most client-side/SPA setups), there's no cache to bust — just re-fetch and re-render.

  Concretely: the **Next.js starter** reads the cart with a `force-cache` tagged fetch, so `router.refresh()` alone is **not** enough — it also needs a `revalidateTag` server action (`totebotRevalidateCart`, shown in the Next.js example). Remix → `revalidator.revalidate()`; Nuxt/SvelteKit → re-run the cart load; a plain SPA → just re-fetch.

* **`totebot:medusa-cart-adopt`** (carries the new `cartId`) — fired when the agent created a cart while the shopper had none. Persist the new cart id as the shopper's cart (cookie / your cart store) so your own mini-cart and checkout point at the agent's cart instead of an empty one.

## 4. Complete examples

Each example implements steps 2 and 3 in one place — pushing the cart out *and* reacting to the agent's events — because a real bridge does both. They are **illustrative**: the helpers in commented blocks (`getCartId`, `fetchCart`, `renderMiniCart`, …) stand in for *your* storefront's real cart code. Swap them for yours; the Totebot-facing parts (`window.__totebotMedusa`, the event names, the dispatch) stay exactly as written.

**Next.js starter**

On the official Medusa Next.js starter the cart id lives in an httpOnly cookie, so you read it server-side in the layout and pass it to a small client bridge component. Adoption uses a server action because only the server can write that cookie.

`src/app/layout.tsx`:

```tsx theme={null}
import Script from "next/script"
import { retrieveCart } from "@lib/data/cart"
import TotebotMedusaBridge from "@modules/totebot/totebot-medusa-bridge"

export default async function RootLayout({ children }: { children: React.ReactNode }) {
  const cart = await retrieveCart().catch(() => null)
  // Changes whenever cart contents change, so the bridge re-signals the chat even
  // when the cart id is unchanged (e.g. a second add via the storefront's button).
  const cartVersion = cart
    ? `${cart.updated_at ?? ""}:${(cart.items ?? []).reduce((sum, item) => sum + (item.quantity ?? 0), 0)}`
    : null
  // The starter's checkout hangs without a step param, so send shoppers to the
  // address step (see "Checkout URL" note below).
  const region = process.env.NEXT_PUBLIC_DEFAULT_REGION || "us"
  return (
    <html lang="en">
      <body>
        {children}
        <TotebotMedusaBridge
          cartId={cart?.id ?? null}
          cartVersion={cartVersion}
          baseUrl={process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL!}
          publishableKey={process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY!}
          checkoutUrl={`${process.env.NEXT_PUBLIC_BASE_URL}/${region}/checkout?step=address`}
        />
        <Script
          src="https://app.totebot.ai/embed.min.js"
          id="YOUR_AGENT_ID"
          strategy="afterInteractive"
          domain="totebot.ai"
        />
      </body>
    </html>
  )
}
```

`src/modules/totebot/totebot-adopt-cart.ts` (adopts the agent's cart and busts the starter's cart cache):

```ts theme={null}
"use server"
import { cookies as nextCookies } from "next/headers"
import { revalidateTag } from "next/cache"
import { getCacheTag } from "@lib/data/cookies"

// The starter reads the cart with `cache: "force-cache"` tagged `carts-<id>` and
// busts it via revalidateTag in its own actions. The agent writes the cart
// out-of-band, so without this the mini-cart stays stale for repeat writes to the
// same cart — router.refresh() alone does not bust a tagged fetch.
export async function totebotRevalidateCart() {
  const cartCacheTag = await getCacheTag("carts")
  if (cartCacheTag) revalidateTag(cartCacheTag)
  const fulfillmentCacheTag = await getCacheTag("fulfillment")
  if (fulfillmentCacheTag) revalidateTag(fulfillmentCacheTag)
}

export async function totebotAdoptCart(cartId: string) {
  if (!cartId) return
  const cookies = await nextCookies()
  cookies.set("_medusa_cart_id", cartId, {
    maxAge: 60 * 60 * 24 * 7,
    httpOnly: true,
    sameSite: "strict",
    secure: process.env.NODE_ENV === "production",
  })
  await totebotRevalidateCart()
}
```

`src/modules/totebot/totebot-medusa-bridge.tsx`:

```tsx theme={null}
"use client"
import { useEffect } from "react"
import { useRouter } from "next/navigation"
import { totebotAdoptCart, totebotRevalidateCart } from "./totebot-adopt-cart"

export default function TotebotMedusaBridge({
  cartId, cartVersion, baseUrl, publishableKey, checkoutUrl,
}: { cartId: string | null; cartVersion?: string | null; baseUrl: string; publishableKey: string; checkoutUrl: string }) {
  const router = useRouter()

  // Depend on cartVersion (not just cartId) so storefront-side changes to an
  // existing cart re-signal the chat, not only the first add that created it.
  useEffect(() => {
    window.__totebotMedusa = { cartId: cartId ?? undefined, baseUrl, publishableKey, checkoutUrl }
    window.dispatchEvent(new Event("totebot:medusa-cart"))
  }, [cartId, cartVersion, baseUrl, publishableKey, checkoutUrl])

  useEffect(() => {
    // Agent changed the cart → bust the starter's tagged cart cache, then
    // re-render so the server-read mini-cart updates (incl. repeat writes to
    // the same cart; router.refresh() alone won't bust a force-cached fetch).
    const onRefresh = async () => {
      await totebotRevalidateCart()
      router.refresh()
    }
    // Agent created a cart while the shopper had none → adopt it, then re-render.
    const onAdopt = async (e: Event) => {
      const id = (e as CustomEvent<{ cartId?: string }>).detail?.cartId
      if (!id) return
      await totebotAdoptCart(id)
      router.refresh()
    }
    window.addEventListener("totebot:medusa-cart-refresh", onRefresh)
    window.addEventListener("totebot:medusa-cart-adopt", onAdopt)
    return () => {
      window.removeEventListener("totebot:medusa-cart-refresh", onRefresh)
      window.removeEventListener("totebot:medusa-cart-adopt", onAdopt)
    }
  }, [router])

  return null
}
```

**React SPA (non-Next.js)**

For Vite, CRA, Remix-SPA, and similar: keep the widget loader in your `index.html` (the dynamic-injection snippet from step 1). Put the cart bridge in a hook so it has access to your client state. This example uses TanStack Query; if you use plain state, replace `queryClient.invalidateQueries(...)` with your own refetch/setter.

```tsx theme={null}
// useTotebotMedusaBridge.ts
import { useEffect } from "react";
import { useQueryClient } from "@tanstack/react-query";

const CART_KEY = "medusa_spa_cart_id"; // wherever you persist the shopper's cart id

// Pass your cart query's data so the effect re-runs whenever the cart changes.
export function useTotebotMedusaBridge(cart: unknown) {
  const queryClient = useQueryClient();

  // Push the current cart to the widget on mount and on every cart change.
  useEffect(() => {
    window.__totebotMedusa = {
      cartId: localStorage.getItem(CART_KEY) || undefined,
      baseUrl: import.meta.env.VITE_MEDUSA_BACKEND_URL,
      publishableKey: import.meta.env.VITE_MEDUSA_PUBLISHABLE_KEY,
      regionId: import.meta.env.VITE_MEDUSA_REGION_ID,
    };
    window.dispatchEvent(new Event("totebot:medusa-cart"));
  }, [cart]);

  // React to the agent's cart events.
  useEffect(() => {
    const onRefresh = () => queryClient.invalidateQueries({ queryKey: ["cart"] });
    const onAdopt = (e: Event) => {
      const id = (e as CustomEvent<{ cartId?: string }>).detail?.cartId;
      if (!id) return;
      localStorage.setItem(CART_KEY, id);
      queryClient.invalidateQueries({ queryKey: ["cart"] });
    };
    window.addEventListener("totebot:medusa-cart-refresh", onRefresh);
    window.addEventListener("totebot:medusa-cart-adopt", onAdopt);
    return () => {
      window.removeEventListener("totebot:medusa-cart-refresh", onRefresh);
      window.removeEventListener("totebot:medusa-cart-adopt", onAdopt);
    };
  }, [queryClient]);
}
```

```tsx theme={null}
// In your storefront component:
const { data: cart } = useQuery({ queryKey: ["cart"], queryFn: fetchCart });
useTotebotMedusaBridge(cart);
```

**Vanilla JS**

Drop this in a `<script>` after the widget loader. It shows the whole loop end to end.

```html theme={null}
<script>
  // ---- Replace this block with YOUR storefront's real cart helpers ----
  function getCartId() { return localStorage.getItem("cart_id") || undefined; }
  function setCartId(id) { localStorage.setItem("cart_id", id); }
  async function fetchCart() {
    const id = getCartId();
    if (!id) return null;
    const res = await fetch(`https://your-medusa-server.com/store/carts/${id}`, {
      headers: { "x-publishable-api-key": "pk_..." },
    });
    return (await res.json()).cart;
  }
  function renderMiniCart(cart) { /* update your cart icon / drawer from `cart` */ }
  // ---------------------------------------------------------------------

  function pushTotebotCart() {
    window.__totebotMedusa = {
      cartId: getCartId(),
      baseUrl: "https://your-medusa-server.com",
      publishableKey: "pk_...",
      regionId: "reg_...",
    };
    window.dispatchEvent(new Event("totebot:medusa-cart"));
  }

  pushTotebotCart();                       // 1. push on load

  // 2. After YOUR own add/update/remove, call pushTotebotCart() again so the agent
  //    sees the latest cart (this is also what flips cartId from undefined → real
  //    the first time the shopper creates a cart without reloading).

  // 3. React to the agent's cart events:
  window.addEventListener("totebot:medusa-cart-refresh", async () => {
    renderMiniCart(await fetchCart());     // agent changed the cart → re-read + re-render
  });
  window.addEventListener("totebot:medusa-cart-adopt", async (e) => {
    const id = e.detail && e.detail.cartId; // agent made a cart for an empty shopper
    if (!id) return;
    setCartId(id);
    pushTotebotCart();                     // tell the agent we adopted it
    renderMiniCart(await fetchCart());
  });
</script>
```

## Notes

* **Checkout URL on the Medusa Next.js starter:** the starter's `/checkout` page renders nothing until it knows which step to show — it hangs on a "Shipping Address" spinner without a `step` query param (its own checkout button always adds one). Set `checkoutUrl` to `https://your-store.com/<region>/checkout?step=address`, **not** `…/checkout`. Custom storefronts: point `checkoutUrl` at whatever URL actually renders your checkout.
* **The two `totebot:medusa-cart-*` events are framework-agnostic** — Totebot only dispatches plain DOM events. How you react is up to your stack: Next.js needs `revalidateTag` + `router.refresh()` (shown above); Remix uses `revalidator.revalidate()`; Nuxt/SvelteKit re-run the cart load; a plain SPA just re-fetches the cart. Only the *handler* is stack-specific, never the Totebot side.
* In a SPA that mutates the cart without navigation, dispatch `totebot:medusa-cart` after your own cart ops so the widget picks up changes.
* **Region:** the agent binds every new cart to a region (currency/tax/shipping). On a starter storefront you need nothing — Totebot derives the region from the `/<countryCode>/` segment of the page URL (matched against your `/store/regions`). Custom storefronts that don't carry a country in the URL may set `regionId` on `window.__totebotMedusa` so the agent bootstraps the cart in the right region; otherwise it falls back to your store's first region.
* `publishableKey` is public by design (safe to expose client-side).
* Only the server can write the httpOnly `_medusa_cart_id` cookie, so adoption uses a server action (Next.js example). Totebot can't write your cart cookie from another origin — it can only tell your storefront which cart to adopt.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.