Milly Software

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:

  1. 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.
  2. Your widget script tag carries a data-cart-bridge attribute. 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:

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].