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_viewedmaps tocontents_viewed,product_added_to_cartmaps toitems_added,checkout_startedmaps tocheckout_started, andcheckout_completedmaps toorder_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 event | OpenAI Ads standard event | Funnel step |
|---|---|---|
page_viewed | page_viewed | Any page load |
product_viewed | contents_viewed | Product detail page |
product_added_to_cart | items_added | Add to cart |
checkout_started | checkout_started | Checkout begins |
checkout_completed | order_created | Purchase 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:
- Pick one test product and walk it through the full funnel deliberately — don't rely on live traffic to surface a gap.
- Product page — dev tools' Network tab, filter for
bzr.openai.com, confirm a request fires withcontents_viewedand the correct productid. - Add to cart — confirm
items_addedfires withamountas an integer in minor units and a populatedcontents[]. - 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.
- Purchase — place a real or test-mode order, check your webhook logs for a successful CAPI call, and confirm
order_createdappears in OpenAI Ads Manager's event stream a few minutes later. - 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_viewedon collection or category pages.contents_viewedis 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 requirescurrencywheneveramountis present, at both the event level and inside eachcontents[]item that carries its ownamount. An event with an amount and no currency is malformed, not just incomplete. - Major-unit amounts instead of minor-unit integers. Sending
89.98instead of8998is the single most common formatting error across every event type in this mapping, not just purchases — check it oncontents_viewedanditems_addedtoo, not onlyorder_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