Documentation
Integration reference for developers wiring Milly Chat into a storefront. This page covers the headless bridge — how a storefront that manages its own cart handles the events the widget emits. More integration topics land here as they ship.
Bridge implementation (headless storefronts)
On a native Shopify theme, the widget mutates the cart itself: it POSTs to /cart/add.js to add a line, and hits /discount/CODE?redirect=/cart to apply a coupon. Nothing to wire — it works the same way your theme's own Add to Cart button does.
A headless storefront (a Next.js app on the Shopify Storefront API, a WordPress front end talking to a custom cart service, a Hydrogen build, anything not running Shopify's native theme) manages its own cart, so the widget can't write to it directly. Instead it hands the work to your code through a small bridge: the widget tells you what the shopper wants (a variant to add, a discount to apply) and your storefront performs the mutation against whatever cart system you already run.
Turning the bridge on
Two things enable callback (headless) mode:
- Your store config is set to
cart_mode: "callback". This is a store-level setting, not a script attribute — contact support to switch it on for your store. In callback mode the widget never POSTs to/cart/add.js; it emits events instead. - Your widget script tag carries a
data-cart-bridgeattribute. This is the widget's signal that a bridge listener is actually installed on the page, so it unlocks the Add to Cart button. Without it, the button stays hidden in callback mode — a deliberate guard so shoppers never click a button that would no-op. Presence is all that's checked; the value is ignored.
<script src="https://cdn.millysoftware.com/widget.js" data-store-id="YOUR_STORE_ID" data-cart-bridge ></script>
The two events
The bridge is two different mechanisms — one callback, one DOM event — so wire each the way it's actually delivered:
- Add to cart is a callback, registered through the widget's callback API:
window.MillyChat.on("add_to_cart", handler). The handler receives the product and variant to add. After your cart write resolves, you report the outcome back to the widget withwindow.MillyChat.addToCartResult(...)so it can render the confirmation (or an error) inside the conversation. It is not amilly:addToCartwindow event. - Apply discount is a window event:
milly:applyDiscount, with the code onevent.detail.code. The widget dispatches it when a coupon is issued in the conversation (the auto-apply-at-cart flow after an email capture). There is no result callback for this one — the chat reply already shows the shopper the code as a fallback, so a failed apply still leaves them able to enter it manually.
The add_to_cart payload:
{
productId, // Milly Chat product id
productTitle, // product name (may be null)
variantId, // the selected variant's id
variantName, // e.g. "Black / Large"
price, // string, e.g. "24.00"
shopifyVariantId // Shopify variant GID — use this for cart mutations
} // e.g. "gid://shopify/ProductVariant/123..."The milly:applyDiscount payload is just { code } — the discount code string.
Example 1 — Next.js + Shopify Storefront API
A Next.js App Router storefront that keeps a Storefront-API cart already has functions to add a line (cartLinesAdd) and set a discount (cartDiscountCodesUpdate). The bridge just calls them. Register the listeners once, in a client component mounted on every page (e.g. rendered from the root layout):
"use client";
import { useEffect } from "react";
// Your existing Storefront API cart helpers:
import { addLine, applyDiscountCode, getCheckoutUrl } from "@/lib/cart";
export function MillyChatBridge() {
useEffect(() => {
const MillyChat = (window as any).MillyChat;
if (!MillyChat) return;
// Add to cart — a callback. shopifyVariantId is the merchandise GID
// the Storefront API's cartLinesAdd expects.
async function handleAddToCart(data: any) {
try {
await addLine(data.shopifyVariantId, 1);
MillyChat.addToCartResult("success", {
checkoutUrl: getCheckoutUrl(),
});
} catch (err) {
console.error("Milly Chat add-to-cart bridge failed:", err);
MillyChat.addToCartResult("failure");
}
}
MillyChat.on("add_to_cart", handleAddToCart);
// Apply discount — a window event. No result callback.
async function handleApplyDiscount(e: Event) {
const { code } = (e as CustomEvent).detail;
try {
await applyDiscountCode(code); // cartDiscountCodesUpdate under the hood
} catch (err) {
console.error("Milly Chat discount bridge failed:", err);
}
}
window.addEventListener("milly:applyDiscount", handleApplyDiscount);
return () => {
MillyChat.off("add_to_cart", handleAddToCart);
window.removeEventListener("milly:applyDiscount", handleApplyDiscount);
};
}, []);
return null;
}Because this is a SPA, the widget script (with data-cart-bridge) can live in the document head via next/script; the listeners above attach on mount and clean up on unmount. Passing checkoutUrl back on success lets the in-chat "Checkout" reply jump straight to your Storefront-API checkout for that cart.
Example 2 — Vanilla JS (WordPress-style headless)
A server-rendered storefront — a WordPress front end, a static site, anything without a React runtime — wires the same two events in a plain <script> placed after the widget script. Here the cart lives behind a custom endpoint (a WP REST route, a serverless function, your own API):
<script
src="https://cdn.millysoftware.com/widget.js"
data-store-id="YOUR_STORE_ID"
data-cart-bridge
></script>
<script>
// Add to cart — a callback.
window.MillyChat.on("add_to_cart", function (data) {
fetch("/wp-json/mystore/v1/cart/add", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ variantId: data.shopifyVariantId, quantity: 1 }),
})
.then(function (res) {
if (!res.ok) throw new Error("Cart add failed");
return res.json();
})
.then(function (cart) {
window.MillyChat.addToCartResult("success", {
checkoutUrl: cart.checkoutUrl,
});
})
.catch(function (err) {
console.error("Milly Chat add-to-cart bridge failed:", err);
window.MillyChat.addToCartResult("failure");
});
});
// Apply discount — a window event.
window.addEventListener("milly:applyDiscount", function (event) {
var code = event.detail.code;
fetch("/wp-json/mystore/v1/cart/discount", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ code: code }),
}).catch(function (err) {
console.error("Milly Chat discount bridge failed:", err);
});
});
</script>The shape is identical to the Next.js version — the only difference is that the cart mutation is an HTTP call to your endpoint instead of a Storefront API helper. Report success/failure the same way so the widget keeps the conversation honest about what happened.
Testing your bridge
You don't need to trigger a real chat capture to verify the wiring. Paste these into the browser console on a page where the bridge is installed:
// Fire the discount event exactly as the widget does:
window.dispatchEvent(
new CustomEvent("milly:applyDiscount", { detail: { code: "TEST10" } })
);
// Exercise the add-to-cart path by invoking your handler with a fake payload.
// (add_to_cart is a callback, so there's no public "emit" — call your handler,
// or re-dispatch through your own reference to it.)
handleAddToCart({
productId: "test",
productTitle: "Test Product",
variantId: "1",
variantName: "Default",
price: "1.00",
shopifyVariantId: "gid://shopify/ProductVariant/REPLACE_WITH_A_REAL_ID",
});
// See the widget's in-chat confirmation without touching the cart at all:
window.MillyChat.addToCartResult("success", { checkoutUrl: "/checkout" });A successful addToCartResult("success") makes the widget inject an "added to your cart" follow-up with Checkout / Continue-shopping replies; "failure" surfaces an error message instead of a phantom confirmation.
For the shopper-facing side of add to cart (the variant picker, native cart flow, and attribution), see In-Chat Add to Cart on Shopify. Questions on a specific stack? Email [email protected].