# Where stores lose their ChatGPT signal: a 5-check audit you can run today

> Five checks find most broken ChatGPT tracking: browser vs server events, duplicate orders, feed rejections, failed API batches, attribution evidence.

Canonical: https://convrail.com/blog/where-stores-lose-chatgpt-signal-audit/

Most ChatGPT tracking failures fall into five patterns: a browser pixel that stopped firing while the server still reports, orders counted twice because the pixel and the server used different event IDs, feed rows rejected until the product expires after 14 days, Conversions API batches refused whole, and attributed orders with no evidence of how they were attributed. Each has a manual check, a fix, and a monitor.

## Why an audit, not a dashboard glance

The Ads Manager reports on the conversion signal it receives. It does not tell you what it did not receive, and the OpenAI documentation describes no per-row error report for feed rows that fail ingestion. So a store can look healthy from the reporting side while purchases never arrive, or arrive twice. The five checks below start from your own data, where the missing pieces are visible. None requires a code change to perform, and each ends with how Convrail's health monitors detect the problem, with the exact thresholds used.

## Check 1: browser events vs server events

The OAIQ pixel runs in the shopper's browser and sends `page_viewed`, `contents_viewed`, `items_added`, `checkout_started` and `order_created` as the session unfolds. Your server-side integration sends the Conversions API call for `order_created` when the order is paid, from a webhook or a hook that does not depend on the browser. Both are meant to run. The ratio between them is the first thing to read.

### How to check manually

Open your store in a private browser window with the developer tools on the Network tab, filter on `bzr.openai.com`, and walk through a product page, a cart add and the checkout. The pixel documentation puts the script at `https://bzrcdn.openai.com/sdk/oaiq.min.js`; you should see it load, then one request per event. Then compare with what your server sent for the same session: the Conversions API call to `https://bzr.openai.com/v1/events?pid=<PIXEL-ID>` in your application logs.

For a longer window, count both sides over the last 24 hours. If you keep your own copy of events (Convrail stores a relayed copy of every pixel event next to the server events), this is one query.

### What healthy looks like

Browser events outnumber server events, because browsing produces many events and each order produces one server call. Server `order_created` events are close in number to browser `order_created` events, with the server side slightly higher: some shoppers close the tab before the thank-you page paints, block scripts or refuse consent (the pixel documentation states that when consent is false it "doesn't send measurement-event pings"). We have not measured a universal ratio; what matters is your own baseline and departures from it.

### What unhealthy looks like

Zero browser events in 24 hours while server events keep arriving is the clearest failure: the pixel is not loading, because a theme update removed the script, a consent banner now defaults to denial, a content security policy blocks `bzrcdn.openai.com`, or the pixel ID changed. The reverse (browser events but no server events) means the webhook is not firing or the Conversions API credential expired.

### The fix

Reinstall the pixel through a mechanism that survives theme changes. On Shopify, a web pixel registered through the Web Pixel API loads independently of the theme; on WooCommerce, a plugin hook in the footer does the same. Confirm the pixel ID matches the Ads Manager. Then confirm the consent flow: `oaiq("consent", false)` blocks events, `oaiq("consent", true)` allows them, and the default is true unless a denial is stored.

### How Convrail detects it

The "Pixel broken" monitor fires when there are zero browser events in the last 24 hours while server events keep arriving, severity warning. The "Volume drop" monitor covers the softer version: events in the last 24 hours below 30% of the daily baseline (the average of the previous 7 days), evaluated only once the baseline reaches at least 50 events a day so small stores are not flooded with noise. Both run every 15 minutes, and each opens exactly one alert per store and per type, resolved automatically when the condition clears.

## Check 2: duplicate orders

An order sent twice inflates conversions and revenue in the Ads Manager and makes ROAS look better than it is. The Conversions API reference gives one mechanism to prevent it: deduplication on "Pixel ID, `event_name`, and `id`", with the rule that "OpenAI uses the first event it receives for a matching key and ignores later duplicates." The pixel documentation says the same from the other side: "If you send the same conversion from both the Measurement Pixel and a server-side integration, reuse the same event_id in both places."

### How to check manually

Take ten recent orders. For each, find the `event_id` the pixel sent on the thank-you page (Network tab, the `order_created` request) and the `id` your server sent in the Conversions API payload for the same order. They must be identical strings. If your pixel generates a random UUID and your server uses the order number, every order counts twice.

Then look at the Ads Manager conversions for a day with a known order count. A count close to double your paid orders is the symptom.

### The fix

Derive the event ID from the order on both sides, so neither needs to know what the other generated. Convrail uses `order_<orderId>` as the event ID on the pixel and on the server webhook for the same order; a custom integration can adopt the same convention. One subtlety: the deduplication key includes the pixel ID and, for custom events, `custom_event_name`, so an order sent to two pixel IDs is two events, not a duplicate.

### How Convrail detects it

There is no separate monitor for duplicates because the shared event ID removes the cause at the source. The attributed orders export lists each order once with its ID, so a comparison with the Ads Manager count is a straightforward join.

## Check 3: feed rejections and the 14-day expiry

A product that is not in the feed cannot be recommended, shown as an ad or bought through checkout. The feed side loses signal in a quieter way than tracking does: rows are rejected without a report, and the product disappears on a delay. The file upload overview states that "OpenAI retains its most recently processed record for up to 14 days." A row rejected today keeps its product visible for two weeks on the strength of the last accepted snapshot, then the product expires.

### How to check manually

Open your last delivered feed file and validate it against the specification yourself. The nine required fields are `item_id`, `title`, `description`, `url`, `brand`, `seller_name`, `image_url`, `availability` and `price`. Then check the formats that most often fail: price as `amount CURRENCY` ("a decimal amount in major units, a space, and an uppercase three-letter ISO 4217 currency code"), availability as one of `in_stock`, `out_of_stock`, `pre_order`, `backorder`, `unknown`, booleans as lowercase strings, GTIN with "Exactly 8, 12, 13, or 14 digits, including a valid check digit", and no placeholder values ("Do not use placeholder strings such as `null`, `unknown`, or `n/a`").

Compare the number of rows in the file with the number of active products in your store, then with last week's file: rows that were present and are now absent started their 14-day countdown. The overview names the usual causes: "Missing required fields, Outdated or non-spec field names, Malformed field values."

### The fix

Validate before delivery, not after: every row checked against the specification on your side, rejected rows listed with the field and the rule, so the product can be fixed in the store and picked up by the next run. Use stable file names and full snapshots ("Keep the same file name on every update and overwrite it with the latest snapshot"). To remove a product on purpose, set `is_eligible_search=false` instead of dropping the row; the overview describes that as the way to make a product ineligible on the next processed snapshot.

### How Convrail detects it

Two layers. First, the validator: every row is validated before export, and each run's journal records `itemsTotal`, `itemsValid`, `itemsRejected`, the diff against the previous run (added, removed, changed) and one error entry per rejected item with `item_id`, field and message. Second, the "Feed delivery failed" monitor: at least one feed run failed to deliver in the last 24 hours, severity critical, after the delivery retries are exhausted. A product silently vanishing after two weeks becomes a journal line the day it starts failing. The [products not showing in ChatGPT shopping](/blog/products-not-showing-chatgpt-shopping/) guide goes deeper on individual rejection reasons.

## Check 4: failed Conversions API batches

Server events travel in batches. The reference is direct about what a bad event does to its neighbors: "The API accepts batches of up to 1,000 events." and "If one event in the batch fails, the full batch fails." A single order with a malformed timestamp or a missing `source_url` can take up to 999 valid orders down with it, and the Ads Manager simply never sees any of them.

### How to check manually

Look at your integration's logs for non-2xx responses from `POST https://bzr.openai.com/v1/events`. Then check the per-event rules most likely to trip: `timestamp_ms` "must be within the last 7 days and no more than 10 minutes in the future" (clock skew or a backfill of old orders fails this), `source_url` is required for web events with a scheme and host, `data.amount` is "an integer in the currency's standard minor unit" (`4200` for $42.00, never `42.00`), and hashed user fields must be lowercase 64-character SHA-256 hex.

If you are unsure whether a payload is valid, the reference offers `validate_only`, which "Validates events without saving them when `true`." A validation pass on a sample batch tells you whether the shape is right without recording anything.

### The fix

Retry with exponential backoff on 429 and 5xx responses, since those are transient. After the last attempt, move the batch to a dead-letter queue rather than discarding it, so the events can be inspected, corrected and resent within the 7-day timestamp window. And treat a permanently failed batch as an incident: the orders in it are not in the Ads Manager.

### How Convrail detects it

Batches are sent with up to 1,000 events, retried with exponential backoff on 429 and 5xx, and dead-lettered after the last attempt. The "Conversions API errors" monitor fires when at least one batch failed permanently in the last hour after all retries, severity critical. Test mode uses `validate_only` so a new store can check payload shape before sending live data. The setup walkthrough for Shopify is in [track ChatGPT ads conversions on Shopify](/blog/track-chatgpt-ads-conversions-shopify/).

## Check 5: attribution evidence coverage

The last leak is not in what reaches OpenAI but in what you can prove on your side. An order attributed to ChatGPT should carry the reason: the `oppref` click identifier from an ad, a `utm_source` on the landing page, or a referrer. If the attribution is a label without evidence, you cannot reconcile it with the Ads Manager, and you cannot tell ads from organic.

### How to check manually

Take your attributed orders for a period and classify each by the evidence available on the order as recorded by your platform. Convrail applies these rules, first match wins:

| Evidence on the order | Verdict |
| --- | --- |
| Landing URL contains `oppref=<id>` | ChatGPT ads |
| `utm_source` in chatgpt, openai or chatgpt.com, with `utm_medium` in 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 |

Count how many attributed orders fall into each row. A healthy distribution has ads orders carrying `oppref`: the Conversions API reference calls it "An opaque, OpenAI-provided attribution identifier", and the pixel "stores oppref in a first-party `__oppref` cookie so later page views can reuse it", so it should survive from the ad click to the order. If your ads-attributed orders rely mostly on UTM or referrer, the `oppref` is being lost somewhere between the landing page and the order.

### What breaks the evidence

Redirects that strip query parameters (a country selector, a `www` to apex redirect, an app that rewrites the landing URL), a platform that records the referrer on the first page view but not on the order, and consent flows that clear cookies before checkout. The referrer is the weakest evidence: often present on the first page, gone by the thank-you page.

### The fix

Preserve `oppref` through the landing chain and attach it to the conversion events; the pixel does this when it loads on the landing page, and the Conversions API accepts `oppref` on the event so the server call carries it too. Add `utm_source` and `utm_medium` to your ad and feed URLs as a second line of evidence (the best practices page suggests parameters such as `utm_medium=feed` on `url`). Store the evidence with the order, not just the verdict.

### How Convrail reports it

There is no automated monitor for evidence coverage; it is a review, not a threshold. Convrail stores the matched rule and the evidence with every attributed order and exports attributed orders as CSV with order ID, amount, medium, method and evidence, so the count above is one pivot table. The full rule set and its limits are on the [attribution page](/attribution/).

## The audit in one table

| Check | Manual signal | Fix | Convrail monitor and threshold |
| --- | --- | --- | --- |
| 1. Browser vs server events | Zero pixel requests to bzr.openai.com while orders arrive server-side | Theme-independent pixel install, correct pixel ID, consent flow | Pixel broken: 0 browser events in 24h while server events arrive (warning). Volume drop: last 24h below 30% of the 7-day daily average, baseline at least 50 events/day (critical) |
| 2. Duplicate orders | Pixel `event_id` differs from server `id` for the same order | Derive the ID from the order on both sides (`order_<orderId>`) | Prevented at source; audit with the attributed orders export |
| 3. Feed rejections and expiry | Rows missing or malformed; products gone after 14 days | Validate every row before delivery; journal rejections; `is_eligible_search=false` to remove on purpose | Run journal with per-row errors. Feed delivery failed: at least one run not delivered in 24h (critical) |
| 4. Failed CAPI batches | Non-2xx on POST /v1/events; whole batch refused | Backoff on 429/5xx, dead-letter after last attempt, `validate_only` in test | Conversions API errors: at least one batch failed permanently in the last hour (critical) |
| 5. Attribution evidence | Ads orders without `oppref` | Preserve query parameters, add UTMs, store evidence with the order | No monitor; evidence column in the CSV export |

All monitors are evaluated every 15 minutes per store. Each condition opens one alert at most, sent once by email if notifications are enabled, and is resolved automatically when the condition clears.

## Common mistakes

**Reading the Ads Manager as the source of truth for volume.** It reports what arrived. The gap is only visible from your side.

**Backfilling old orders through the Conversions API.** Events older than 7 days fail the timestamp rule and, in a batch, take the fresh orders down with them.

**Deleting a product row to hide it.** The product stays visible for up to 14 days anyway. `is_eligible_search=false` is the documented way to remove it on the next snapshot.

## What to do next

Run the five checks once by hand, then let [Convrail's health alerts](/health-alerts/) run four of them every 15 minutes and keep the evidence for the fifth in the attributed orders export.