Loops AI Docs

Add to Cart

Let shoppers add, remove, and change products in your store's own cart from inside the Loops AI widget. Your storefront listens for four browser events and replies with LoopsAI.cartResult.

With Add to Cart turned on, shoppers can put a recommended product into your store's cart without leaving the chat. They can also open a cart view inside the widget, change quantities, remove lines, and go to checkout.

Loops AI keeps no cart of its own. Every cart change runs through the cart code your storefront already uses, so stock checks, sessions, cart badges, mini-cart drawers, and your own analytics keep working as they do today. The widget asks, your storefront does the work and answers, and the widget shows the answer.

What you build

One listener for each of four window events (loopsai:addToCart, loopsai:removeFromCart, loopsai:updateQuantity, loopsai:getCart), and one reply for every event through window.LoopsAI.cartResult(...). Nothing else is required on your side.

How it works

  1. The shopper taps Add to Cart on a product card, in the product details sheet, or on an outfit or look. If the product has several sizes or colors, the widget first shows a picker and resolves the exact sellable variant.
  2. embed.js, which runs on your page, dispatches a CustomEvent on window with a unique requestId.
  3. Your listener runs your normal cart code and calls LoopsAI.cartResult with the same requestId.
  4. The widget shows the outcome. It then re-reads your cart with loopsai:getCart unless your reply already included the cart.

Before you start

RequirementWhy it matters
The widget is installed with embed.js on your siteThe events are dispatched by embed.js on your page. Add to Cart is not available in the mobile SDKs or when the chat URL is opened directly without embed.js.
Your product feed carries variants, each with a barcode (or a variant id), and size / color where they applyThe variant code is what the widget sends as productCode. See Product codes.
Your page JavaScript can add, remove, and change cart lines by that codeThis is the code your listener calls. Most storefronts already have it for their own product page and mini-cart.
You can read the current cart in the browserloopsai:getCart must return the live cart.

Integration steps

Agree on the product code

Confirm which identifier your cart code accepts (SKU, EAN/barcode, or a platform variant ID) and make sure that value is in the barcode field of each variant in your Loops AI product feed. Do this first: everything else depends on it. See Product codes.

Add the listeners to every page that loads the widget

Copy the reference listener into a script that runs on every page where the widget loads, before or right after the embed.js snippet. Replace the four adapter functions with calls into your store.

Test with the feature off for shoppers

Run through the test checklist on a staging site, or on production with the browser console mock first.

Turn it on in the dashboard

In the Loops AI dashboard, open the web channel configurator for your agent:

  • Advanced Features → Add to Cart Functionality: turn it on.
  • Checkout page URL (shown under the switch): optional. See Checkout button.
  • Content → Product card buttons: set a button's action to Add to Cart if you want the button on product cards (in chat results and in favorites). The product details sheet, outfits, and looks show their Add to Cart button as soon as the feature is on.

Publish the configuration.

Ship the listener before you flip the switch

Once Add to Cart Functionality is on, the widget shows cart buttons and the cart icon on every page where it loads. If a page has no listener, a tap waits 10 seconds and then shows "Checking your cart…" without ever adding the product. Deploy the listener everywhere the widget runs first, then turn the switch on.

Reference listener

This is a complete, production-shaped listener. It covers all four events, always replies, maps your errors to Loops AI reasons, includes the current cart in every reply, and is safe to load more than once.

Only the four functions in storeCart are specific to your store.

(function installLoopsAICartBridge() {
  if (window.__loopsAICartBridgeInstalled) return;
  window.__loopsAICartBridgeInstalled = true;

  const storeCart = {
    async add(productCode, quantity) {
      await window.MyStore.cart.add({ sku: productCode, quantity });
    },
    async remove(productCode) {
      await window.MyStore.cart.remove({ sku: productCode });
    },
    async setQuantity(productCode, quantity) {
      await window.MyStore.cart.update({ sku: productCode, quantity });
    },
    async read() {
      const cart = await window.MyStore.cart.get();
      return {
        items: cart.lines.map((line) => ({
          productCode: line.sku,
          quantity: line.quantity,
          name: line.title,
          imageUrl: line.imageUrl,
          size: line.size,
          color: line.color,
          unitPrice: line.unitPrice,
          lineTotal: line.lineTotal,
          productUrl: line.url,
        })),
        itemCount: cart.totalQuantity,
        subtotal: cart.subtotal,
        total: cart.total,
        currency: cart.currencyCode,
        checkoutUrl: "/checkout",
      };
    },
  };

  const KNOWN_REASONS = [
    "OUT_OF_STOCK",
    "INVALID_VARIANT",
    "INVALID_QUANTITY",
    "MAX_QUANTITY",
    "PRODUCT_NOT_AVAILABLE",
    "UNKNOWN_ERROR",
  ];

  function toReason(error) {
    const code = error && (error.code || error.reason);
    return KNOWN_REASONS.includes(code) ? code : "UNKNOWN_ERROR";
  }

  function reply(result) {
    const api = window.LoopsAI || window.LoopSmartAI;
    if (api && typeof api.cartResult === "function") api.cartResult(result);
  }

  async function readCartSafely() {
    try {
      return await storeCart.read();
    } catch {
      return undefined;
    }
  }

  function handle(eventName, run) {
    window.addEventListener(eventName, async (event) => {
      const detail = event.detail || {};
      const requestId = detail.requestId;
      if (typeof requestId !== "string") return;
      try {
        await run(detail);
        const cart = await readCartSafely();
        if (!cart && eventName === "loopsai:getCart") throw new Error("Cart unavailable");
        reply({ requestId, success: true, cart });
      } catch (error) {
        reply({
          requestId,
          success: false,
          reason: toReason(error),
          cart: eventName === "loopsai:getCart" ? undefined : await readCartSafely(),
        });
      }
    });
  }

  handle("loopsai:addToCart", ({ productCode, quantity }) =>
    storeCart.add(productCode, quantity),
  );
  handle("loopsai:removeFromCart", ({ productCode }) =>
    storeCart.remove(productCode),
  );
  handle("loopsai:updateQuantity", ({ productCode, quantity }) =>
    storeCart.setQuantity(productCode, quantity),
  );
  handle("loopsai:getCart", () => undefined);
})();

What this does for you:

  • Always replies. Every path ends in cartResult, including errors. A missing reply turns into a 10-second wait and an unknown outcome for the shopper.
  • Returns the cart with every reply. The widget updates its cart view and badge from your reply directly and skips an extra loopsai:getCart round trip. For loopsai:getCart the reply must include cart, so a failed cart read is reported as a failure there. If reading your cart is slow, reply to add, remove, and update without cart; the widget then asks with loopsai:getCart.
  • Installs once. In single-page apps, scripts can run again on route changes. Two listeners for loopsai:addToCart would add the product twice.
  • Maps errors. Throw (or reject) with error.code set to one of the reason codes and the shopper sees a matching message.

Your storefront's UI updates as usual

Because the listener calls your normal cart code, your header cart count, mini-cart, toasts, and analytics react exactly as they do when the shopper adds from your own product page. Decide whether you want your mini-cart to pop open for adds that came from the widget; the widget already shows its own confirmation.

Event reference

All four events are CustomEvents dispatched on window. Read the payload from event.detail. Each event carries a fresh requestId. Reply to it exactly once.

Eventevent.detailWhat your storefront should do
loopsai:addToCart{ requestId, productCode, quantity }Add quantity units of productCode. If the line already exists, increase it by quantity.
loopsai:updateQuantity{ requestId, productCode, quantity }Set the line to exactly quantity units. This is the new total, not a delta.
loopsai:removeFromCart{ requestId, productCode }Remove the whole line.
loopsai:getCart{ requestId }Change nothing. Reply with the current cart.

Field rules:

FieldTypeGuarantee
requestIdstringUnique per request, for example loopsai_atc_4f1c…. Treat it as opaque and send it back unchanged.
productCodestringNever empty, already trimmed. The sellable variant code. See Product codes.
quantitynumberWhole number, 1 or more. The widget currently sends 1 for every add; the cart view sends the new total for quantity changes.

The widget never sends updateQuantity with 0. To take a line to zero it sends removeFromCart.

LoopsAI.cartResult(result)

The only method your storefront calls. It is available on window.LoopsAI (and the legacy alias window.LoopSmartAI) as soon as embed.js has booted, which is always true by the time an event reaches you.

interface CartResult {
  requestId: string;
  success: boolean;
  reason?:
    | "OUT_OF_STOCK"
    | "INVALID_VARIANT"
    | "INVALID_QUANTITY"
    | "MAX_QUANTITY"
    | "PRODUCT_NOT_AVAILABLE"
    | "UNKNOWN_ERROR";
  cart?: CartSnapshot;
}
FieldRequiredNotes
requestIdYesThe requestId from the event you are answering. Replies with an unknown requestId are ignored.
successYesMust be the boolean true for success. Any other value is treated as a failure.
reasonWhen success is falseOne of the reason codes. Anything else, or no reason, is treated as UNKNOWN_ERROR.
cartRequired for getCart, recommended for the othersThe cart after the operation. See Cart snapshot.

You may call cartResult synchronously inside the listener or later after an await. Only the first reply for a requestId counts.

Cart snapshot

interface CartSnapshot {
  items: CartItem[];
  itemCount?: number;
  subtotal?: number;
  total?: number;
  currency?: string;
  checkoutUrl?: string;
}

interface CartItem {
  productCode: string;
  quantity: number;
  name?: string;
  imageUrl?: string;
  size?: string;
  color?: string;
  unitPrice?: number;
  lineTotal?: number;
  productUrl?: string;
}

Only items[].productCode and items[].quantity are required. Send an empty cart as { items: [] }.

FieldWhat the widget does with it
items[].productCodeMatches the line to the product the shopper added. Use the same code the widget sent. Matching is exact and case-sensitive.
items[].quantityLine quantity. Whole number, 0 or more.
items[].nameLine title. If missing, the widget uses the product it just added or looks the code up in your Loops AI catalog.
items[].imageUrlLine thumbnail. Same fallback as name.
items[].size, items[].colorShown under the title. Same fallback as name.
items[].unitPriceShown next to the color, and in the "Added to Cart" confirmation.
items[].lineTotalShown on the right of the line.
items[].productUrlAccepted; not displayed yet.
itemCountNumber on the cart badge. If missing, the widget sums items[].quantity.
subtotal, totalShown at the bottom of the cart view when present.
currencyISO 4217 code such as "EUR" or "TRY". Used to format every amount. Without a recognised code, amounts are shown as plain numbers with two decimals.
checkoutUrlTarget of the Go to Checkout button. See Checkout button.

Value rules:

  • Amounts are plain numbers in the currency's major unit: 49.9, not 4990 and not "€49,90".
  • imageUrl, productUrl, and checkoutUrl may be absolute or relative to the current page. Only http and https URLs are kept, up to 2,048 characters.
  • Text values are trimmed and cut at 512 characters. Empty strings count as missing.
  • Up to 200 lines are read. Lines without a valid productCode or quantity are skipped.
  • Fields not listed above are ignored. The snapshot must be plain JSON data; functions and class instances are dropped.

Send name and image

The catalog fallback finds a line only when its code matches a product code or a variant barcode in your Loops AI catalog. Lines your shopper added outside the widget, or codes that are not in the feed, show a generic "Product" title and a placeholder image. Sending name and imageUrl avoids that.

Reason codes

Reply with these when an operation fails. Choose the closest one; the shopper sees the message in the right-hand column (shown in the widget's language).

ReasonWhen to use itWhat the shopper sees
OUT_OF_STOCKThe variant has no stock."This size is sold out" ("This color is sold out" for products that only vary by color). The size picker stays open and marks that size as sold out.
MAX_QUANTITYThe line is at the maximum your store allows."You've reached the maximum quantity". The widget disables the + button for that line at the quantity your cart reports.
INVALID_QUANTITYThe quantity is not allowed (for example, sold in packs)."That quantity isn't available"
INVALID_VARIANTYour store does not recognise productCode."This product can't be added right now"
PRODUCT_NOT_AVAILABLEThe product exists but cannot be bought (unpublished, region-locked, not sold online)."This product can't be added right now"
UNKNOWN_ERRORAnything else."Something went wrong. Please try again."

Two more reasons come from Loops AI, never from your storefront:

ReasonMeaning
TIMEOUTNo reply within 10 seconds. The outcome is unknown. See Slow or missing replies.
NOT_SUPPORTEDAdd to Cart is off for this agent, or the embed.js on the page did not report cart support (for example, an outdated cached copy). The shopper sees "The store's cart isn't responding right now".

Loops AI sets no quantity limit of its own. Your MAX_QUANTITY reply is the only limit.

Slow or missing replies

Your listener has 10 seconds to call cartResult.

If it does not, the widget does not assume the change failed, because your store may still have added the product. Instead it:

  1. Marks the line as "Checking your cart…" and blocks further taps on that line.
  2. Re-reads your cart with loopsai:getCart right away and again 10 seconds later, and compares quantities.
  3. Still accepts your original reply for up to 60 seconds. A late reply settles the line with the real outcome.
  4. Clears the "checking" state after 60 seconds at most.

The widget never retries a change on its own, so a slow store cannot cause a double add. Each tap is one request with one requestId. If your cart API supports idempotency keys, you can pass requestId as one.

If your add-to-cart path can take longer than a few seconds (slow network, a third-party cart API), reply as soon as your store has accepted the change rather than after every follow-up task has finished.

Keeping the widget in sync

The widget never stores the cart. It reads it from you with loopsai:getCart:

  • when the widget loads on a page and whenever it is opened,
  • when the shopper opens the cart view,
  • after every change whose reply did not include cart,
  • when the browser tab becomes visible again,
  • when the page is restored from the browser's back/forward cache,
  • while it checks a change that timed out.

So a product the shopper added from your own product page appears in the widget's cart the next time it opens. You do not need to notify the widget about changes made outside it.

Requests for different products can arrive at the same time. For the same product there is never more than one request in flight: a second tap while the first is pending is ignored, not queued. If a getCart reply arrives after a newer one, the widget ignores the older snapshot.

Product codes

productCode is the code of the sellable variant: the exact size and color the shopper chose, not the parent product.

The widget takes it from your Loops AI product feed:

  1. the variant's barcode, if it has one,
  2. otherwise the variant's id.

If a product has a single variant, that variant is used directly. If it has several, the shopper picks a size (or a color, for color-only products) and the widget sends that variant's code. Variants with stock 0 in your feed are shown as sold out in the picker.

Your storefront must be able to find the line with that value. Two common setups:

Your cart is keyed byPut this value in the feed's variant barcode
SKUthe variant SKU
EAN / GTINthe barcode

If your cart code needs a different identifier (for example a platform variant ID), translate inside your listener: look the code up in a map on the page and pass the result to your cart code. Return the same productCode the widget sent in items[].productCode, so the widget can match the line.

Codes are treated as text

Codes are sent and compared as strings, exactly as they are in your feed. Leading zeros and letter case matter. Do not reformat them.

Checkout button

The cart view shows a Go to Checkout button when a checkout page is known. The button opens that page in the main browser window (not inside the widget).

The page comes from:

  1. the Checkout page URL set under Add to Cart Functionality in the dashboard, if one is set,
  2. otherwise checkoutUrl from your latest cart snapshot.

With neither, the button is hidden. The dashboard field accepts full https:// addresses only. A checkoutUrl in the snapshot may be relative, for example "/checkout".

What the shopper sees

PlaceBehavior
Product cards (chat results and favorites)An Add to Cart button, if a product card button's action is set to Add to Cart.
Product details sheet, outfits, looksAn Add to Cart button next to the existing actions.
Size or color pickerOpens when a product has more than one variant. Sold-out variants are marked.
ConfirmationA toast above the message box with the product image, size, and price, and a View cart link.
Chat headerA cart icon with the item count from your cart.
Cart viewYour cart lines with quantity steppers, remove buttons, subtotal and total, and Go to Checkout.

The widget does not change a line's variant, does not show shipping, and does not show compare-at prices for cart lines. Cart changes from chat messages ("add the black one in M") are not supported yet; shoppers use the buttons.

Analytics

When analytics delivery is set up for your agent (see Analytics), the widget pushes two extra ecommerce events to window.dataLayer:

Event (default name)Fires when
loops_ai_add_to_cartYour storefront confirmed an add, or a quantity increase, made in the widget. quantity is the number of units added.
loops_ai_remove_from_cartYour storefront confirmed a removal, or a quantity decrease, made in the widget. quantity is the number of units removed.

Both fire only after your storefront confirmed success (directly or through a late reply), never for failures or unknown outcomes. Both follow the GA4 ecommerce shape (ecommerce.items[] with item_id, item_name, price, quantity). You can rename or turn off each event in the dashboard's analytics settings.

Avoid double counting

Your own cart code will usually push its standard add_to_cart event too, because the listener calls the same code your product page does. That is expected: your add_to_cart counts every add, and loops_ai_add_to_cart tells you which of those came from Loops AI. Do not rename the Loops AI event to add_to_cart, or each widget add is counted twice.

Test checklist

You can test your listener before turning the feature on by calling it directly from the browser console:

const original = window.LoopsAI.cartResult;
window.LoopsAI.cartResult = (result) => {
  console.log("cartResult", result);
  return original(result);
};
window.dispatchEvent(
  new CustomEvent("loopsai:addToCart", {
    detail: { requestId: "test-1", productCode: "YOUR_VARIANT_CODE", quantity: 1 },
  }),
);
window.dispatchEvent(
  new CustomEvent("loopsai:getCart", { detail: { requestId: "test-2" } }),
);

The product should appear in your own mini-cart, and the console should log a cartResult for test-1 and test-2 with success: true and your cart. The widget ignores these replies because it did not send those request IDs.

Then, with the feature on in a staging agent, check each row:

ScenarioExpected
Add a single-variant product from a product card"Added to Cart" toast, cart badge goes up, your mini-cart shows the product.
Add a product with sizesSize picker opens; after choosing, the right variant is in your cart.
Add the same product againYour line goes to quantity 2 (not a second line).
Open the cart viewLines, names, images, prices, and totals match your own cart page.
+ and − in the cart viewYour cart quantity changes; loopsai:updateQuantity carries the new total.
Remove a lineLine disappears in the widget and in your cart.
Out-of-stock variant"This size is sold out", and the size is marked sold out.
Quantity limit"You've reached the maximum quantity", and + is disabled.
Add from your own product page, then open the widgetThe new line is in the widget's cart.
Go to CheckoutYour checkout page opens in the main window.
Every page type that loads the widget (home, listing, search, product, content)Listener answers loopsai:getCart on each of them.
Single-page navigation, then add onceExactly one unit is added (no duplicate listeners).

Troubleshooting

Migrating from the old postMessage example

Earlier versions of this page described a window.addEventListener("message", …) listener for { action: "addToCart" } messages. The widget never sent those messages. If you built against that example, replace it with the reference listener above; the old listener can be removed.

On this page