Guide

How to Track Shopify Product Views, Add-to-Cart, Checkout, and Purchases

Shopify's web pixel events and OpenAI Ads' standard events use different names for the same funnel steps: product_viewed maps to contents_viewed, product_added_to_cart maps to items_added, checkout_started maps to checkout_started, and checkout_completed maps to order_created. The mapping itself is simple — the work is translating Shopify's event data into OpenAI Ads' documented shape and getting purchase events onto a server-side path, since the browser-side purchase signal alone isn't reliable enough to depend on.

If you're setting up ecommerce tracking on Shopify for OpenAI Ads, the first thing to sort out isn't code — it's which Shopify event corresponds to which OpenAI Ads event. Get the mapping wrong and everything built on top of it, dashboards included, is measuring the wrong thing correctly.

The event mapping

Shopify's web pixel standard events and OpenAI Ads' supported events cover the same ecommerce funnel, just under different names:

Shopify web pixel eventOpenAI Ads standard eventFunnel step
page_viewedpage_viewedAny page load
product_viewedcontents_viewedProduct detail page
product_added_to_cartitems_addedAdd to cart
checkout_startedcheckout_startedCheckout begins
checkout_completedorder_createdPurchase complete

Two of these share the exact same name across both systems (page_viewed, checkout_started), which can create a false sense that the whole mapping is a pass-through. It isn't. Even where the names match, the event data doesn't — Shopify's payload is shaped around Shopify's own object model, and OpenAI Ads expects its documented contents data shape (amount, currency, contents[] with only the fields it defines). The mapping is a translation step, not a rename.

Where each event fires, and what data it should carry

product_viewed → contents_viewed. Fires when a shopper views an individual product page — not a collection or category listing page, which is a distinct and common mistake covered below. The translated event should carry the product's id, optionally its name and content_type (for example, "product"), and if you want to pass value at this stage, an amount in minor units with currency.

product_added_to_cart → items_added. Fires the moment an item is successfully added to the cart — after the add-to-cart action completes, not on button click alone, since a click can fail (out of stock, validation error) without an actual cart addition happening. The event should carry amount (the item's price in minor units), currency, and a contents[] entry with the item's id and quantity.

checkout_started → checkout_started. Fires when checkout begins. On Shopify this is checkout-side, meaning it has to come from a custom pixel rather than a theme script — more on that below. The event should carry the cart's total amount in minor units, currency, and contents[] listing every item in the cart being checked out.

checkout_completed → order_created. Fires when checkout finishes in the browser. This is the funnel's most important event and also its least reliable one as a pure browser signal — see the purchase-truth section below for why. When it does fire, it should carry the order total as amount in minor units, currency, and contents[] for every purchased line item.

Across all four, the two formatting details that break more implementations than anything else: amount must be an integer in the currency's minor unit (12999 for $129.99, never 129.99), and currency is required on any event that includes an amount.

Implementation via a custom pixel

Because Shopify restricts third-party script access at checkout, a plain theme.liquid script can cover contents_viewed and items_added on the storefront, but not checkout_started or checkout_completed. The sanctioned path for those is Shopify's customer events system — a custom pixel that runs in a sandboxed environment, subscribes to Shopify's own checkout lifecycle events, and calls oaiq from inside that sandbox.

Here's a worked example of a custom pixel subscribing to Shopify events and translating them:

// Custom pixel, added via Shopify's customer events (Settings > Customer events)

// Re-initialize oaiq inside the sandbox — it's a separate context from theme.liquid
window.oaiq = window.oaiq || function () {
  (window.oaiq.q = window.oaiq.q || []).push(arguments);
};
oaiq("init", { pixelId: "YOUR-PIXEL-ID" });

analytics.subscribe("product_viewed", (event) => {
  const product = event.data.productVariant;
  oaiq("measure", "contents_viewed", {
    type: "contents",
    currency: product.price.currencyCode,
    amount: Math.round(product.price.amount * 100),
    contents: [{ id: String(product.product.id), quantity: 1 }]
  });
});

analytics.subscribe("product_added_to_cart", (event) => {
  const line = event.data.cartLine;
  oaiq("measure", "items_added", {
    type: "contents",
    currency: line.merchandise.price.currencyCode,
    amount: Math.round(line.merchandise.price.amount * 100) * line.quantity,
    contents: [{ id: String(line.merchandise.id), quantity: line.quantity }]
  });
});

analytics.subscribe("checkout_started", (event) => {
  const checkout = event.data.checkout;
  oaiq("measure", "checkout_started", {
    type: "contents",
    currency: checkout.totalPrice.currencyCode,
    amount: Math.round(checkout.totalPrice.amount * 100),
    contents: checkout.lineItems.map((item) => ({
      id: String(item.variant.id),
      quantity: item.quantity
    }))
  });
});

Note the Math.round(price.amount * 100) pattern repeated in each handler — Shopify's customer events typically expose price as a decimal, and OpenAI Ads requires an integer minor-unit value, so this conversion has to happen explicitly every time, not once.

checkout_completed can be subscribed to the same way for a browser-side order_created signal, but don't stop there — which is the point of the next section.

The purchase-truth problem

checkout_completed, even correctly implemented inside a custom pixel, is a browser event. It depends on the confirmation page finishing its load, the sandboxed pixel executing without error, and no ad blocker or tracking-prevention mechanism intercepting the request. Any one of those failing means a real purchase happens and no order_created event ever reaches OpenAI Ads from the browser side — not because anything in your code is wrong, but because browser-side measurement has an inherent ceiling on reliability at exactly the moment that matters most.

This is precisely the gap OpenAI's own Measure Results guidance addresses for Shopify and headless storefronts: use the browser Pixel for browser-side events, and the Conversions API for server-side events. On Shopify, the reliable trigger for a server-side purchase event is the orders/create webhook — fired the instant a real order is created, independent of what happened or didn't happen in the shopper's browser.

Your webhook handler builds and sends the Conversions API request:

curl -X POST "https://bzr.openai.com/v1/events?pid=<YOUR-PIXEL-ID>" \
  -H "Authorization: Bearer <YOUR-CAPI-KEY>" \
  -H "Content-Type: application/json" \
  --data '{
    "validate_only": false,
    "events": [
      {
        "id": "order_1001",
        "type": "order_created",
        "timestamp_ms": 1755446400000,
        "oppref": "<value captured from __oppref cookie or cart attribute>",
        "source_url": "https://yourstore.com/checkout/thank-you",
        "action_source": "web",
        "data": {
          "type": "contents",
          "amount": 8998,
          "currency": "USD",
          "contents": [
            { "id": "SKU-1001", "quantity": 1, "amount": 8998, "currency": "USD" }
          ]
        }
      }
    ]
  }'

Both signals can — and should — coexist. The browser event catches purchases quickly when nothing interferes; the webhook-driven CAPI event catches every purchase, including the ones the browser missed. To avoid counting the same order twice, deduplicate: reuse the same unique ID (the Shopify order ID works well) as the pixel's event_id and the CAPI event's id, sent through the same Pixel ID. OpenAI Ads treats a matched pair as one event.

One detail specific to Shopify: oppref, the click reference the pixel captures automatically into a first-party __oppref cookie (roughly a 30-day window), is not captured automatically by CAPI. Since the webhook fires server-side, potentially after the browser session has ended, you need to carry oppref forward yourself — commonly by writing it into a cart or order attribute during checkout so it's present in the orders/create payload when the webhook fires.

The no-code path

Everything above — the custom pixel, the event-data translation, the webhook, the Conversions API call, the dedup logic, and the oppref handoff — is exactly what the free Comercio Shopify app, Pixel OpenAI Ads & ChatGPT Ads, installs automatically. It maps Shopify's events to OpenAI Ads' standard events, formats amounts correctly, and forwards server-side purchase events via CAPI, without you writing a custom pixel or standing up a webhook handler. Install it, paste in your Pixel ID from Ads Manager's conversions tab, and the mapping in this article is handled for you.

Validating each funnel step

Whichever path you take, confirm each step actually works before trusting the data:

  1. Pick one test product and walk it through the full funnel deliberately — don't rely on live traffic to surface a gap.
  2. Product page — dev tools' Network tab, filter for bzr.openai.com, confirm a request fires with contents_viewed and the correct product id.
  3. Add to cart — confirm items_added fires with amount as an integer in minor units and a populated contents[].
  4. Checkout — since this runs in Shopify's sandbox, use Shopify's own customer events debugging tools (admin → Settings → Customer events) rather than relying solely on dev tools, to confirm the custom pixel is registered and firing.
  5. Purchase — place a real or test-mode order, check your webhook logs for a successful CAPI call, and confirm order_created appears in OpenAI Ads Manager's event stream a few minutes later.
  6. If an event fires but a campaign reports 0, check that the event type — or exact custom event name — matches what's configured on that campaign; a mismatch there is invisible from the event stream alone.

Common mapping mistakes

  • Firing contents_viewed on collection or category pages. contents_viewed is for viewing a specific product or content item — a shopper scrolling a category grid hasn't viewed any single product yet. Firing it there inflates the event and muddies what it's supposed to represent.
  • Missing currency. OpenAI Ads requires currency whenever amount is present, at both the event level and inside each contents[] item that carries its own amount. An event with an amount and no currency is malformed, not just incomplete.
  • Major-unit amounts instead of minor-unit integers. Sending 89.98 instead of 8998 is the single most common formatting error across every event type in this mapping, not just purchases — check it on contents_viewed and items_added too, not only order_created.

For the manual-versus-app comparison this article's implementation section is drawn from, see OpenAI Ads pixel and CAPI for Shopify. If your events are already implemented but something's not showing up correctly, the Shopify tracking debugging guide walks through diagnosing exactly where they're breaking.

Want this done for you?

Fixed-scope setup, tested end-to-end and documented.

Explore the serviceTake the free audit