An AI-native buying journey writes a chain of records into your systems: a feed row that made the product recommendable, a landing URL carrying oppref or a ChatGPT referrer, pixel events (page_viewed, contents_viewed, items_added, checkout_started, order_created) mirrored by one server-side order_created with the same event ID, and an attributed order with its evidence. Break any link and the order still happens, but your analytics cannot explain it.
A hypothetical shopper, step by step
Everything below follows one invented shopper. Call them Sam. Sam is not a real person, the store is not a real store, and none of the numbers are measurements; they are chosen to make the records readable. The point is to trace what each step writes into your data, because that is the only part of the journey you will ever see.
Sam wants a packable rain jacket for cycling to work and asks ChatGPT for one with a hood that fits over a helmet. What happens next depends on things you control weeks earlier.
Step 0: the product has to exist in the feed
Before Sam types anything, your catalog has already been delivered to OpenAI as a full snapshot over SFTP, at least daily, in parquet or jsonl.gz. The file upload overview asks for “full snapshots on a predictable cadence (at least daily)” under a stable file name that is overwritten on each run.
For the jacket to be a candidate, its row has to be valid. The specification requires nine fields: item_id, title, description, url, brand, seller_name, image_url, availability and price. A row for Sam’s jacket, in JSON Lines, might look like this:
{
"item_id": "JKT-RAIN-M-OLIVE",
"group_id": "JKT-RAIN",
"title": "Packable cycling rain jacket, olive, size M",
"description": "Waterproof 2.5-layer shell with a helmet-compatible hood, underarm vents and a stuff sack. Weight 210 g.",
"url": "https://shop.example.com/products/rain-jacket?variant=olive-m&utm_source=chatgpt&utm_medium=feed",
"brand": "Example Outdoor",
"seller_name": "Example Outdoor Store",
"image_url": "https://cdn.example.com/jkt-rain-olive-m.jpg",
"availability": "in_stock",
"price": "129.00 EUR",
"variant_dict": { "color": "olive", "size": "M" },
"is_eligible_search": true,
"is_ads_eligible": true,
"target_countries": ["FR", "BE"]
}
Two details in that row matter later. The utm_source and utm_medium parameters in url follow the best practices page, which suggests adding “feed attribution parameters to url (for example utm_medium=feed) when you need feed-specific click tracking.” And is_ads_eligible is set explicitly, because an omitted value means “disabled unless a feed-level Ads default applies.”
What this writes into your data: a feed run journal entry. In Convrail, that is itemsTotal, itemsValid, itemsRejected, the diff against the previous run, and a delivery status. If the jacket’s row had failed validation (a price written 129,00, an image in .webp), it would appear as one error entry with the field and the rule, and Sam would never see the jacket. The specification documents no per-row error report from OpenAI’s side, so this journal is the only place the rejection would be visible.
What breaks the chain here: a rejected row, or a product missing from a snapshot. The overview says a missing record persists “for up to 14 days”, so a broken export goes unnoticed for two weeks, then the product is gone.
Step 1: the prompt and the product card
ChatGPT answers Sam with a short list of jackets, each as a product card built from feed data: title, image, price, seller. Your jacket is one of them, either as an organic result or as an ad. From your side, nothing is written at this step: no impression event in your analytics, no query log you can read. What you can know is whether the product was eligible: is_eligible_search was true (the default when omitted, and the specification adds that “Eligibility does not guarantee display”), and is_ads_eligible was true if the card was an ad. The best practices page describes the copy that helps here: “Use concise, factual copy that helps users understand products.”
What this writes into your data: nothing directly. If the card is an ad, the impression is counted on OpenAI’s side and later appears in the Advertiser API insights that Convrail imports once a day (impressions, clicks, spend, CTR, CPC, CPM, conversions).
Step 2: the click, and the identifier it carries
Sam clicks the card. Two versions of this step exist, and they leave different fingerprints.
Ad click. The landing URL carries an oppref parameter. The Conversions API reference describes it as “An opaque, OpenAI-provided attribution identifier. Pass the original string without modification.” The pixel documentation explains what happens on landing: the pixel “captures oppref from the landing page URL” and “stores oppref in a first-party __oppref cookie so later page views can reuse it.” Convrail’s pixel keeps that cookie for 7 days.
Organic click. No oppref. The landing URL carries whatever you put in the feed url, here utm_source=chatgpt&utm_medium=feed, and the browser may send a referrer of chatgpt.com. “May” is the operative word: referrers are dropped by some browsers, privacy settings and redirects.
So the landing URL for Sam’s ad click looks like:
https://shop.example.com/products/rain-jacket?variant=olive-m&utm_source=chatgpt&utm_medium=feed&oppref=<opaque-id>
What this writes into your data: a session with a landing URL and, on the platform side, whatever your store records about the visit’s landing page and referrer. That record is what attribution will read at the end.
What breaks the chain here: any redirect between the click and the page the pixel loads on that drops query parameters: a geo-redirect that rebuilds the URL, a www to apex hop that forgets the query string, a currency selector that reloads without parameters. oppref is gone before the pixel ever sees it, and the click downgrades to organic or not attributed at the end of the journey.
Step 3: page_viewed and contents_viewed
The product page loads with the OAIQ pixel script from https://bzrcdn.openai.com/sdk/oaiq.min.js, initialized with your pixel ID. Two events fire: page_viewed for the page itself, and contents_viewed for the product being looked at. Convrail maps the Shopify product_viewed and collection_viewed events to contents_viewed, and search_submitted to page_viewed.
A contents_viewed call carries what was seen:
oaiq("measure", "contents_viewed", {
type: "contents",
amount: 12900,
currency: "EUR",
contents: [{ id: "JKT-RAIN-M-OLIVE", group_id: "JKT-RAIN", quantity: 1, amount: 12900, currency: "EUR" }]
}, { event_id: "cv_<session>_JKT-RAIN-M-OLIVE" });
Note the amount: 12900, not 129.00. The reference is explicit that data.amount is “an integer in the currency’s standard minor unit. For example, use 4200 for $42.00”. The content id matches the feed item_id, which is how the event is tied back to the product card Sam clicked.
What this writes into your data: a browser event with a timestamp, an event ID, the pixel ID, the oppref from the cookie, and the content identifiers. Convrail relays a copy of each pixel event to its own store with the same event ID, so you have your own record rather than depending on the Ads Manager for the count.
What breaks the chain here: the pixel not loading (theme change, content security policy, script blocker) or consent denied. The pixel documentation states that it “initializes consent to true by default unless you set it to false or the Pixel finds a stored denial”, and that with consent false it “doesn’t send measurement-event pings.” A denial is a legitimate end to the browser chain, which is why the server path in step 6 exists.
Step 4: items_added
Sam picks size M and adds the jacket to the cart. The pixel fires items_added with the same content structure. Convrail maps Shopify’s product_added_to_cart to it.
What this writes into your data: one more browser event in the same session, still carrying oppref from the cookie even though the cart page URL does not contain it. That is the practical value of the first-party cookie: the identifier follows the session without being in every URL.
What breaks the chain here: a cart hosted on a different domain, where the cookie set on the product domain is not readable. Events after the domain change lose oppref unless it is carried over deliberately.
Step 5: checkout_started
Sam proceeds to checkout. The pixel fires checkout_started with the cart total. On Shopify, the checkout runs on Shopify’s infrastructure, which is one reason Convrail installs the pixel through the Web Pixel API rather than the theme: a web pixel runs on the checkout pages too, where theme code does not.
What this writes into your data: the last browser event before the order. Many sessions with checkout_started and no order_created is either normal abandonment or a broken thank-you page pixel; step 6 tells you which.
Step 6: order_created, twice, counted once
Sam pays. Now two things happen, deliberately in parallel.
In the browser. The thank-you page fires order_created through the pixel, with the order total in minor units and the line items as contents. Convrail maps Shopify’s checkout_completed to it. The event ID is order_<orderId>.
On the server. Your platform fires an order webhook (orders/paid on Shopify, an order hook on WooCommerce). The server builds a Conversions API event and sends it, batched with others, to POST https://bzr.openai.com/v1/events?pid=<PIXEL-ID>:
{
"events": [
{
"id": "order_1001",
"type": "order_created",
"timestamp_ms": 1756720000000,
"action_source": "web",
"source_url": "https://shop.example.com/checkout/thank-you",
"oppref": "<opaque-id>",
"user": {
"emails_sha256": ["<64-char lowercase hex>"],
"countries": ["FR"]
},
"data": {
"type": "contents",
"amount": 12900,
"currency": "EUR",
"contents": [
{ "id": "JKT-RAIN-M-OLIVE", "group_id": "JKT-RAIN", "quantity": 1, "amount": 12900, "currency": "EUR" }
]
}
}
]
}
Three constraints from the reference shape this payload. timestamp_ms “must be within the last 7 days and no more than 10 minutes in the future.” source_url is required for web events and needs a scheme and host. And the email is not an email: it is a SHA-256 hash of the trimmed, lowercased address, as a lowercase 64-character hex string. Convrail hashes emails and phone numbers on receipt, never stores them in clear, and runs an automated guard that blocks any outgoing payload containing clear personal data.
Why counted once. The pixel sent order_created with event_id: "order_1001". The server sent order_created with id: "order_1001", same pixel ID. The reference states that deduplication matches on “Pixel ID, event_name, and id” and that “OpenAI uses the first event it receives for a matching key and ignores later duplicates.” If Sam had blocked the pixel, only the server event exists and the order is still counted. If the webhook was delayed, the pixel event is kept and the server copy ignored. Either way, exactly once.
What this writes into your data: two event records with the same ID, one flagged browser and one flagged server, plus a batch record for the server send (accepted, retried, or dead-lettered after the last attempt). The reference notes that “If one event in the batch fails, the full batch fails”, so a permanently failed batch means Sam’s order, and every other order in that batch, is not in the Ads Manager.
What breaks the chain here: different event IDs on the two sides (the order counts twice), a batch refused because one event in it was malformed (the order counts zero times), or a webhook that never fired.
Step 7: attribution
The order exists in your store. The question is whether your analytics can say where Sam came from. Convrail applies a fixed decision table to the landing URL and referrer as recorded by your platform, first match wins:
| Evidence on the order | Verdict |
|---|---|
Landing URL contains oppref=<id> | ChatGPT, ads |
utm_source is chatgpt, openai or chatgpt.com and utm_medium is cpc, ppc, paid or ads | ChatGPT, ads |
Same utm_source, any other medium | ChatGPT, organic |
| Referrer is chatgpt.com, chat.openai.com or openai.com (or a subdomain) | ChatGPT, organic |
| None of the above | not attributed |
For Sam’s ad click, the first row matches: ads, evidence oppref. For the organic version of the same journey, the third row matches on utm_source=chatgpt&utm_medium=feed: organic, evidence UTM. If the parameters were stripped and the referrer survived, the fourth row catches it. If everything was stripped, the order is “not attributed”, and Convrail stores that rather than guessing.
What this writes into your data: an attribution record on the order with medium (ads or organic), method (which rule matched) and evidence (the parameter or referrer value). This is the column you reconcile with the Ads Manager, which attributes on its own side: click-through conversions use “the applicable configured click window”, view-through conversions “a fixed one-day window after an eligible ad impression.”
Step 8: the dashboard
Sam’s order now appears in the attributed revenue for the day, under ads or organic. If the Advertiser API is connected, spend, clicks and impressions are imported once a day per campaign, and ROAS is computed as attributed ads revenue divided by imported spend. The jacket contributes to the top products list, built from the line items of attributed orders. The order can be exported as CSV with its evidence column.
If a link in the chain had broken, the health monitors would say so: zero browser events in 24 hours while server events arrive (pixel broken), a batch that failed permanently in the last hour (Conversions API errors), a feed run not delivered in 24 hours, or a volume drop below 30% of the 7-day baseline. The audit guide walks through each of those checks by hand.
The same purchase through a classic search journey
Put Sam’s journey next to the one you already know.
| Stage | Classic search journey | ChatGPT journey |
|---|---|---|
| Discovery | Sam types keywords; a results page lists links and shopping ads | Sam describes a need in a sentence; the answer includes product cards built from your feed |
| Your input | Indexed pages, and a Google Shopping feed for ads | A product feed validated against the OpenAI specification, delivered as a daily snapshot |
| Impression visibility | Search console and ads reporting show queries and impressions | No query log on your side; ad impressions come from the Advertiser API, organic impressions are not exposed |
| Click identifier | gclid or similar on paid clicks; referrer on organic | oppref on paid clicks; your own UTMs and the chatgpt.com referrer on organic |
| On-site events | Analytics tag and a conversion pixel | OAIQ pixel events plus a Conversions API server event, deduplicated on one event ID |
| Attribution | Platform-side and your analytics, on click identifiers and referrers | Ads Manager on oppref and click windows; your side on oppref, UTMs, referrer |
The structural difference is at the top. In a search journey, the query is visible to you and the link is a page you wrote. In a ChatGPT journey, the query is a conversation you do not see, and the “page” is a card assembled from your feed row. The feed does the job that the indexed page used to do, which is why a rejected row is not a technical footnote but a product that does not exist for that shopper. Everything downstream of the click is familiar: an identifier on the URL, a pixel, a server event, an attribution rule. The names are new; the discipline (one event ID, hashed identifiers, evidence stored with the order) is the same one that makes server-side tracking work on other channels.
Common mistakes
Letting a redirect eat oppref. The identifier is on the landing URL once. If the first hop drops it, the pixel never stores it and the ad click ends up labelled organic or not attributed.
Sending 129.00 as the amount. Minor units, always: 12900 for 129.00 EUR.
Generating the event ID randomly on the pixel. The server cannot reproduce a random ID. Derive it from the order (order_<orderId>) on both sides.
Reading “not attributed” as “not ChatGPT”. It means no evidence survived. A shopper who asked ChatGPT and typed your URL by hand is invisible to these rules, and to every other attribution method.
What to do next
Read the attribution rules and their limits with your own last thirty orders in front of you, and check which row of the decision table each one would land on.
Sources
Frequently asked questions
How does a ChatGPT visit show up in my analytics?
As a session whose landing URL carries an oppref parameter (ads), a utm_source such as chatgpt (if you tagged your URLs), or a referrer of chatgpt.com (organic). Without one of those three, the visit looks like direct traffic.
What is oppref?
The click identifier ChatGPT Ads appends to the landing URL. The OpenAI reference describes it as an opaque, OpenAI-provided attribution identifier; the pixel stores it in a first-party __oppref cookie and attaches it to later events so the order can be matched to the ad click.
Why do I need both the pixel and the Conversions API for one order?
They fail in different ways. The pixel misses shoppers who block scripts or leave before the thank-you page; the server call misses nothing about the order but knows nothing about the browser. Sharing one event ID lets OpenAI keep the first and drop the duplicate.
Can attribution see a shopper who asked ChatGPT and then typed my URL by hand?
No. Attribution works from first-party evidence on the order: click identifier, UTM parameters, referrer. A visit with none of those is recorded as not attributed, and Convrail labels it that way rather than guessing.