Milly Software
InsightsEngagementIn-Chat Add to Cart on Shopify: Closing the Loop Without Leaving the Conversation
Engagement··8 min read

In-Chat Add to Cart on Shopify: Closing the Loop Without Leaving the Conversation

How chat-based add-to-cart works on Shopify — the native /cart/add.js path, headless callback mode, the variant picker, and how attribution survives the handoff.

V
Viet Le
co-founder · Milly Software

AI product discovery in a chat widget is half the loop. The customer asks a question, the AI surfaces the right product — and then what? In most chat tools, the answer is "click through to the PDP, scroll, find the variant picker, click Add to Cart, click View Cart, click Checkout." That's the loop, and it leaks at every step.

This post covers the other half: the moment the AI recommends a product, the chat card itself becomes a buyable surface. Two cart modes (native Shopify and headless), one variant picker, and an attribution flag that survives the handoff to checkout.

The missing piece between AI discovery and conversion

Discovery surfaces are graded on click-through rate. Cart surfaces are graded on conversion. Most AI chat widgets treat themselves as a discovery surface — they hand the customer back to the merchant's PDP and walk away. The handoff is where the conversion drops, because the customer's context (what the AI just said, why this product, which variant) doesn't travel with them.

In-chat ATC keeps the context. The product card the AI surfaced carries an Add to Cart button; one click and it's in the cart, with a follow-up from the AI confirming what just happened. The customer never leaves the conversation, which means the AI gets to keep helping — "Want to add a case to match?", "Here's the matching cable." — instead of starting over from page reload.

The native cart flow

On native Shopify stores (the default mode, ajax), the widget POSTs directly to /cart/add.js — Shopify's built-in Ajax cart endpoint. The line item carries a property the widget sets unconditionally:

{
  items: [{
    id: variantId,
    quantity: 1,
    properties: { _millyChatAssisted: "true" }
  }]
}

That _millyChatAssisted property is what makes chat-attributed conversion measurable downstream. It rides on the cart line through checkout, lands on the order, and downstream analytics (revenue impact, ROI dashboard) can join on it without needing UTM tags or bespoke pixel setups.

After a successful add, the widget dispatches two DOM events:cart:build and cart:updated. Most major Shopify themes (Dawn, Archetype, Impulse, and most modern forks) listen for one or both — the cart icon in the theme refreshes its line count without a page reload, and any drawer or mini-cart UI redraws. Themes that don't listen still get the cart on the next page reload.

The variant picker

Single-variant products (or any product whose only variant is Shopify's default "Default Title") skip the picker entirely — one click adds. For multi-variant products with real options (size, color, etc.), the picker opens inline:

  • All variants render in a grid — including out-of-stock variants, which appear disabled with a strikethrough so customers see the full color/size landscape and don't assume the product is unavailable when one variant happens to be sold out
  • The first available variant is selected by default; out-of-stock variants are unclickable
  • The Add to Cart button stays disabled until a selection exists — no ambiguity about what gets added

The grid mirrors how a well-built PDP picker behaves, so customers don't have to relearn the pattern in chat. The only difference is that it lives inside the conversation flow — which means the AI's recommendation and the option selection happen in the same scroll position.

Headless: cart_mode: 'callback'

Headless storefronts (Hydrogen, custom Next.js, anything not running Shopify's native theme) don't have a /cart/add.js endpoint to POST to. The widget switches modes:

<script
  src="https://cdn.millysoftware.com/widget.js"
  data-store-id="YOUR_STORE_ID"
  data-cart-mode="callback"
></script>

<script>
  window.MillyChat.on('add_to_cart', (data) => {
    // data: {
    //   productId, productTitle,
    //   variantId, variantName, price,
    //   shopifyVariantId   // for stores that need the GID
    // }
    yourCart.add(data.shopifyVariantId);
    window.MillyChat.addToCartResult('success', {
      checkoutUrl: yourCart.checkoutUrl()
    });
  });
</script>

In callback mode, the widget doesn't POST anywhere itself. It emits an add_to_cart callback with the variant data; your storefront handles the actual cart write (against your GraphQL endpoint, your custom cart API, or whatever pattern you use). Once it's done, you call MillyChat.addToCartResult('success') (or 'failure') so the widget knows whether to render the "Added ✓" state and inject the follow-up suggestions, or fall back to an error state.

This is the same callback API the widget uses for product_click, widget_open, and other events. Same shape, same registration via window.MillyChat.on(). Klaviyo, GA4, custom event pipelines all wire in the same way.

Follow-up + analytics

After a successful add (in either mode), the widget emits a product_added_to_cart analytics event with { productId, variantId, variantName, price, placement }. That feeds the Impact dashboard, the weekly recap rollup, and any custom analytics handlers wired through the callback API.

In native mode, the AI also injects a follow-up message into the conversation: "[Product Name] has been added to your cart!" with two suggested replies — "Checkout" and "Continue shopping." The Checkout reply triggers a navigation to the configured checkout URL; Continue shopping keeps the conversation going.

In headless mode, the follow-up waits for the host to confirm. If addToCartResult('success') arrives with a checkoutUrl, the Checkout reply uses it; if not, the widget falls back to the merchant-configured default. If 'failure' arrives, the widget shows an error state and skips the follow-up — the customer sees what went wrong rather than a phantom "added" confirmation.

Why this was the v2.x anchor

v1 of Milly Chat shipped product discovery and recommendations but stopped at the chat card. The customer still had to navigate, find, and add the product themselves. v2.0.0 closed that loop: in-chat ATC made the chat surface a conversion surface, not just a discovery surface.

Everything in the v2.x line composes on top of this — the sticky ATC bar that follows the visitor across the chat, variant-aware availability that filters the picker properly, revenue attribution that joins on _millyChatAssisted, headless-callback support that's now table stakes for our enterprise integrations. ATC is the surface that earns its own version anchor because every measurement of widget value downstream depends on it.

Try Milly Chat

Want to see how this fits your store?
We'll set up a working session.

Get it on ShopifyTalk to sales →