By Dylan HuntJune 8th, 2026shopifypixelscustomer-events

Tracking Add-to-Cart and Checkout Events on Shopify with analytics.subscribe

Every tracking migration ends at the same question: what exactly do Shopify's customer events give me, and what do I forward to my analytics? This is the reference I wish existed when I started. The funnel events that matter, their real payload shapes, and the GA4 wiring for each.

If you haven't created a pixel yet, start with adding a custom pixel without an app, then come back. Shopify's own standard events reference is the canonical field list; what follows is the practical subset.

The funnel in four events

analytics.subscribe("product_viewed", (event) => { /* PDP view */ });
analytics.subscribe("product_added_to_cart", (event) => { /* ATC */ });
analytics.subscribe("checkout_started", (event) => { /* begin checkout */ });
analytics.subscribe("checkout_completed", (event) => { /* purchase */ });

Every handler receives the same envelope: event.context is a snapshot of where it happened (page, navigator, window) and event.data is the commerce payload, different per event. You also get event.timestamp and event.id for deduplication.

product_added_to_cart

The payload is a cartLine:

analytics.subscribe("product_added_to_cart", (event) => {
  const line = event.data.cartLine;

  line.quantity;                          // how many were just added
  line.cost.totalAmount.amount;           // line cost, e.g. "39.98"
  line.cost.totalAmount.currencyCode;     // "USD"
  line.merchandise.product.title;         // parent product
  line.merchandise.title;                 // variant title
  line.merchandise.sku;                   // variant SKU
  line.merchandise.price.amount;          // unit price
});

And the GA4 mapping:

gtag("event", "add_to_cart", {
  currency: line.cost.totalAmount.currencyCode,
  value: Number(line.cost.totalAmount.amount),
  items: [{
    item_id: line.merchandise.sku || line.merchandise.product.id,
    item_name: line.merchandise.product.title,
    item_variant: line.merchandise.title,
    quantity: line.quantity,
    price: Number(line.merchandise.price.amount),
  }],
});

checkout_started and checkout_completed

Both carry event.data.checkout. Same shape, two moments in time:

analytics.subscribe("checkout_completed", (event) => {
  const checkout = event.data.checkout;

  checkout.order?.id;                  // the order, only on completed
  checkout.totalPrice.amount;          // grand total after discounts + tax
  checkout.subtotalPrice.amount;       // before shipping and tax
  checkout.currencyCode;
  checkout.discountApplications;       // applied discounts, with codes
  checkout.lineItems;                  // [{ title, quantity, variant, ... }]
  checkout.email;                      // when provided and consented
});

The purchase event, mapped:

analytics.subscribe("checkout_completed", (event) => {
  const checkout = event.data.checkout;
  gtag("event", "purchase", {
    transaction_id: checkout.order?.id,
    currency: checkout.currencyCode,
    value: Number(checkout.totalPrice.amount),
    coupon: checkout.discountApplications?.[0]?.title,
    items: checkout.lineItems.map((item) => ({
      item_id: item.variant?.sku || item.variant?.product?.id,
      item_name: item.title,
      quantity: item.quantity,
      price: Number(item.variant?.price?.amount ?? 0),
    })),
  });
});

Two things worth internalizing before you ship this:

  • checkout_completed fires when the thank-you page renders. A buyer who closes the tab a beat early is a real order your pixel never saw. Browser-side numbers run a little under Shopify's order count for everyone. That's the nature of pixels, not a bug in yours. Treat the Shopify order admin as the source of truth and your analytics as directional.
  • Consent gates delivery. If the visitor hasn't granted the consent categories your pixel declared, the events simply don't arrive. Don't burn an afternoon debugging what is actually your privacy settings working as intended.

Custom events when the standard ones aren't enough

The sandbox can't watch your DOM. For anything Shopify doesn't publish, like size-guide opens or quiz completions, publish from theme code where the interaction happens:

// In the theme (has DOM access):
document.querySelector("#size-guide")?.addEventListener("click", () => {
  Shopify.analytics.publish("size_guide_opened", {
    productHandle: window.location.pathname.split("/").pop(),
  });
});
// In the pixel:
analytics.subscribe("size_guide_opened", (event) => {
  gtag("event", "size_guide_opened", {
    product_handle: event.customData?.productHandle,
  });
});

That split, theme publishes and pixel subscribes, keeps every outbound network call in one auditable place. The publish API is documented in the Web Pixels API reference.

Verify against reality

After wiring, place a test order and check that three numbers agree: Shopify's order total, the purchase event's value in your tracker's debug view, and the line-item count. If they match on one real order, your mapping is right. If conversions later drift low by a few percent, re-read the first note above before rewriting anything.

The same structured thinking applies to how AI agents read your store. Events are how you measure; structured data is how you're understood. The Customer Events migration guide covers the first, and AgentReady covers the second.

Make your store agent-ready

Get found and recommended by AI shopping assistants.

AgentReady adds Schema.org structured data, an llms.txt directory, and an AI-readability audit to your Shopify store, so ChatGPT, Perplexity, and Google can understand and recommend your products. Free for stores under 500 products.

Comments

Every comment here comes from a verified email. Write yours, confirm from your inbox, and it's live.

Loading comments…

Leave a comment

ShareXLinkedInFacebook

Written by Dylan Hunt, Founder, Caffeine and Commerce. We build Shopify stores that rank and that AI agents can read. Have a project? Get in touch.