How to track ChatGPT Ads conversions on Shopify: pixel and Conversions API, step by step

Register the OAIQ pixel as a Shopify web pixel, send order_created from your server with the same event ID, hash emails and test with validate_only.

Published 11 min read

Tracking ChatGPT Ads conversions on Shopify takes two layers: the OAIQ pixel registered as a Shopify web pixel for browser events, and the Conversions API fed by your orders/paid webhook for the server copy of each order. Both copies carry the same event ID (order_<orderId>), amounts go out in minor units, emails and phones go out as SHA-256 hashes, and you validate the whole chain with validate_only before going live.

What you are building

Before the steps, the shape of the finished setup, because every decision below follows from it.

LayerRuns whereCapturesWeak point
OAIQ pixel (browser)The visitor’s browser, inside a Shopify web pixel sandboxThe oppref click identifier from the landing URL, page and cart events, the checkoutAnything that stops a script from running or a request from leaving the tab
Conversions API (server)Your server or a connector, triggered by Shopify webhooksThe paid order with amount, currency, line items and hashed customer identifiersCannot read the browser on its own; needs the click context relayed to it

The two layers meet on one rule from the OpenAI Conversions API reference: “If you send the same conversion from the pixel and the Conversions API, reuse the same value as the API id and pixel event_id.” OpenAI then keeps one copy and ignores the other. Sending both is not redundancy for its own sake: the browser copy carries the click context, the server copy carries the reliable amount and the hashed match keys. The reasoning behind the two-layer design is covered in more depth in OAIQ pixel vs Conversions API.

Step 1: get your Pixel ID and API key

Both credentials come from the conversions tab of the ChatGPT Ads Manager. The Pixel ID goes into the browser snippet and into the pid query parameter of every API call. The API key is a bearer token: the Conversions API endpoint is POST https://bzr.openai.com/v1/events?pid=<PIXEL-ID> with an Authorization: Bearer <key> header.

Treat the key like a payment secret. It never belongs in the theme, in a web pixel, or in any code that ships to the browser. If you use a connector, check that it encrypts the key at rest; Convrail stores it with AES-256-GCM and verifies it against OpenAI the moment you save it, so a typo is caught before the first batch.

Step 2: install the pixel as a Shopify web pixel

Shopify gives apps a dedicated mechanism for tracking scripts. According to the Shopify web pixels overview, “Web pixels are loaded in a sandbox on a visitor’s browser”, in a “strict sandbox environment using web workers”, and they “honor the consent signals chosen by the customer”. They can “securely access all surfaces, like storefront, checkout and post-purchase pages”, which matters because the checkout and the thank-you page are exactly where a theme-pasted script cannot go.

Practical consequences:

  • No theme edit. The pixel is registered through the Web Pixel API by the app. Nothing is pasted into theme.liquid, nothing breaks when you switch themes, and uninstalling the app removes the pixel.
  • Consent is handled by Shopify. Shopify runs app pixel callbacks only after the customer’s consent choice allows it, so you do not wire the OAIQ oaiq("consent", ...) call into your own banner logic on Shopify.
  • The sandbox limits what a script can touch. A web pixel receives Shopify’s standard events rather than reading the DOM. This is a feature: the pixel gets clean, structured data for checkout_completed, including the order identifier you will reuse server-side.

If you prefer to hand-roll it, the OpenAI Measurement Pixel reference documents the script at https://bzrcdn.openai.com/sdk/oaiq.min.js, initialized with oaiq("init", { pixelId }), and events fired with oaiq("measure", eventName, eventData, options). Inside a web pixel sandbox you cannot load that script the way a theme would, so a sandbox-native implementation of the same protocol is what Convrail ships: it reads oppref from the landing URL, stores it for 7 days in the first-party __oppref cookie, and fires OpenAI’s image endpoint with your Pixel ID.

Step 3: map Shopify events to OAIQ events

Shopify’s standard web pixel events include page_viewed, product_viewed, collection_viewed, search_submitted, product_added_to_cart, checkout_started and checkout_completed. The Conversions API accepts these event types: appointment_scheduled, checkout_started, contents_viewed, custom, items_added, lead_created, order_created, page_viewed, registration_completed, subscription_created, trial_started, app_installed, app_opened. The mapping Convrail applies:

Shopify eventOAIQ event typeSent from
page_viewedpage_viewedbrowser
product_viewed, collection_viewedcontents_viewedbrowser
search_submittedpage_viewedbrowser
product_added_to_cartitems_addedbrowser
checkout_startedcheckout_startedbrowser
checkout_completedorder_createdbrowser and server, same event ID

Every browser event carries an event_id. For the upper-funnel events a random unique value is fine. For the order it is not: the ID has to be something your server can rebuild independently, without talking to the browser. order_<orderId> satisfies that, because the Shopify order ID is present both in the checkout_completed event and in the orders/paid webhook payload.

Step 4: send the order from your server

Trigger on orders/paid, not on orders/create

An order can be created and never paid. orders/paid fires when the payment is captured, which is the moment the conversion becomes real. Build the event when that webhook arrives.

Build the event ID

order_5843921078 for Shopify order 5843921078. Same string the pixel used at checkout. Deterministic, so a webhook redelivery produces the same ID and OpenAI treats it as a duplicate rather than a second sale. Webhook handlers should be idempotent on your side too: store the event once per (store, event ID, source).

Convert the amount to minor units

The API wants data.amount as an integer in the currency’s minor unit; the reference gives the example “use 4200 for $42.00”. Worked examples:

Shopify totalCurrencyMinor-unit exponentdata.amount
129.90EUR212990
42.00USD24200
4200JPY04200
12.500KWD312500

Do the multiplication with the exponent of the actual currency, not with a hard-coded 100. Float arithmetic on 129.90 * 100 gives 12989.999999999998 in JavaScript; parse the string as an integer of minor units instead, or use a decimal library.

Hash the customer identifiers

The user object accepts hashed fields as lists of lowercase 64-character hex SHA-256 digests: emails_sha256, phone_numbers_sha256, external_ids_sha256, first_names_sha256, last_names_sha256. The normalization rules from the reference:

  • Email: trim whitespace, lowercase, then hash. Jane.Doe@Example.com becomes jane.doe@example.com.
  • Phone: keep “8-15 digits after removing a leading +, leading zeroes, whitespace, parentheses, periods, and hyphens”. +33 6 12 34 56 78 becomes 33612345678.
  • Names: lowercase, remove whitespace and ASCII punctuation. Jean-Pierre becomes jeanpierre.

Raw fields (not hashed): ip_address, user_agent, countries, regions, cities, postal_codes, obref, android_advertising_id. Never send a clear email or phone in any field, hashed or not. The full normalization and the GDPR reasoning are in Server-side tracking without leaking personal data.

The batch payload

One order_created event with the fields a paid Shopify order can fill. Hashes below are real SHA-256 digests of jane.doe@example.com, 33612345678 and customer-7781.

{
  "validate_only": false,
  "integration_source": "convrail",
  "events": [
    {
      "id": "order_5843921078",
      "type": "order_created",
      "timestamp_ms": 1788687000000,
      "action_source": "web",
      "source_url": "https://shop.example.com/checkouts/thank_you",
      "oppref": "<value captured from the landing URL>",
      "user": {
        "emails_sha256": ["86e0b9e56c17cc4d12387e1949b85053fbe73bc3ce5a1188713a9d300cc6133d"],
        "phone_numbers_sha256": ["8a3e7886c9335e82e02299fa3e87b46e2de3b0c63d56003e30a5029394a47661"],
        "external_ids_sha256": ["91d8dedaef47ad2016875e1ecbb6f01c00bba531c0d4c3c5661ed3129ae381b0"],
        "ip_address": "203.0.113.42",
        "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15",
        "countries": ["FR"]
      },
      "data": {
        "type": "contents",
        "amount": 12990,
        "currency": "EUR",
        "contents": [
          {
            "id": "SKU-1001-M",
            "group_id": "PRD-1001",
            "name": "Linen shirt, medium",
            "content_type": "product",
            "quantity": 1,
            "amount": 12990,
            "currency": "EUR"
          }
        ]
      }
    }
  ]
}

Field rules that trip people up, all from the reference:

  • source_url is “Required for web events when action_source is web”.
  • timestamp_ms “must be within the last 7 days and no more than 10 minutes in the future”. Server clock drift of a few minutes is tolerated; a backfill older than a week is rejected.
  • events holds up to 1,000 events, and “If one event in the batch fails, the full batch fails”. Validate each event before you add it to a batch, otherwise one malformed order blocks 999 good ones.
  • oppref is “An opaque, OpenAI-provided attribution identifier”. Your server does not have it unless the browser relays it. Convrail’s pixel posts a compact copy of each event to Convrail with the same event ID and the stored oppref, so the server copy can carry it.

Retries and failure handling

On 429 or 5xx, retry with exponential backoff and the same event IDs; deduplication makes a retry harmless. On 4xx, do not retry blindly: the payload is wrong and will stay wrong. Keep the failed batch with its HTTP status and error text somewhere you can read. Convrail parks exhausted batches in a dead-letter queue and opens one health alert per store, in the app and by email.

Step 5: test with validate_only

Set "validate_only": true at the top of the batch. The reference describes it as “Validates events without saving them when true”. Send a handful of real-shaped orders and fix everything OpenAI rejects: wrong amount type, missing source_url, a hash that is not 64 lowercase hex characters, a timestamp in seconds instead of milliseconds.

Convrail starts every store in test mode for this reason. Events flow through the whole pipeline (webhook, hashing, batching, HTTP call) and OpenAI checks them, but nothing is counted. When the Events screen shows browser and server copies of the same order arriving side by side with accepted status, switch to live.

What to check in the first 24 hours

  1. Pairs, not singles. For each real order, one browser event and one server event with the identical order_<orderId>. A server event alone on every order means the pixel is not firing on the thank-you page; a browser event alone means the webhook or the API call is failing.
  2. Amounts and currency. Pick three orders, compare data.amount against the Shopify total in minor units. Multi-currency stores: check an order in each presentment currency.
  3. Hashes only. Open a sent payload and search for @. It must not appear anywhere in user.
  4. oppref on ad-driven orders. Land on your store through a real ChatGPT ad click, place a test order, confirm the server event carries the identifier.
  5. Acceptance in the Ads Manager. Conversions API events are the signal the Ads Manager optimizes and reports on; confirm the conversions tab shows the events as received. Attribution follows OpenAI’s rules: “Click-through attribution uses the applicable configured click window. View-through conversions use a fixed one-day window.”
  6. Nothing in the dead-letter queue. If something is, read the error text before restarting anything.

Do it yourself or use Convrail

An honest comparison. Doing it yourself is entirely feasible for a team that already runs a server-side tracking stack for other channels.

ConcernDo it yourselfWith Convrail
Pixel installationWrite and publish a Shopify web pixel extension, or use a custom pixel in the Customer events settingsInstalled by the app through the Web Pixel API, removed on uninstall
oppref relay to the serverBuild your own endpoint to receive the browser’s oppref and join it with the orderPixel relays a compact copy of each event with the same event ID
Order webhook, hashing, minor unitsYour code, your tests; the currency exponent and the float pitfall are yours to handleHandled, with zero-decimal and three-decimal currencies covered
Batching and retriesYour queue, backoff and dead-letter storageBatches of up to 1,000, exponential backoff on 429 and 5xx, dead-letter queue, health alert
Personal data guardYour code review disciplineAutomated guard refuses any payload containing clear personal data, covered by an automated test
Test modeToggle validate_only in your configEvery store starts in test mode; switch in settings
VisibilityYour logsEvents screen with source, status and timestamp per event
CostEngineering time, ongoing maintenance as the API evolvesFree during early access; no paid plans exist yet
ControlTotalYou depend on a third party for a conversion-critical path

The last row is the real counter-argument. If ChatGPT Ads becomes a major channel and you already own a mature tracking pipeline, adding one more destination to it may be cheaper long-term than a dependency. If you do not have that pipeline, building one for a single channel is where the time goes.

Common mistakes

  • Random event IDs on the order. The browser and the server must be able to produce the same ID without coordinating. Anything other than a value derived from the order identifier breaks deduplication.
  • Amount as a float or in major units. 129.9 or "129.90" is not an integer in minor units. The API expects 12990.
  • Sending clear emails “to help matching”. The user object only accepts hashed emails, and a clear email in any field is a data leak toward a third party.
  • Firing the order event on orders/create. Unpaid and abandoned orders become conversions.
  • Retrying 4xx responses. The batch is malformed; retrying it wastes your rate limit and hides the bug.
  • Skipping validate_only. The first live batch is a bad time to learn that timestamp_ms was in seconds.
  • Pasting the pixel into the theme. A theme script does not follow the visitor into the Shopify checkout; a web pixel does (Shopify lists checkout and post-purchase pages among the surfaces a web pixel can access). Theme-pasted snippets miss the order event from the browser.

What to do next

Install the Convrail Shopify app, connect your Pixel ID and API key, and watch the first order arrive twice with one ID on the tracking page; details on the platform integration are on the Shopify page.

Sources

Frequently asked questions

Do I need to edit my Shopify theme to install the ChatGPT Ads pixel?

No. A Shopify web pixel is registered through the Web Pixel API and runs in the sandbox Shopify provides, so nothing is pasted into theme.liquid. Uninstalling the app that registered it removes it completely.

Will sending the same order from the pixel and the Conversions API count it twice?

No, as long as both copies carry the same ID under the same Pixel ID. OpenAI deduplicates on Pixel ID, event name and event ID, and keeps the first copy it receives.

What amount format does the Conversions API expect for a Shopify order?

An integer in the currency's minor unit, so 12990 for 129.90 EUR and 4200 for 42.00 USD. Zero-decimal currencies such as JPY are sent as the plain amount, 4200 for 4200 JPY.

Can I test ChatGPT Ads conversion tracking without polluting my real data?

Yes. Send your batches with validate_only set to true and OpenAI checks the format without saving the events. Switch the flag off once the payloads are accepted.

How long do I have to send an order to the Conversions API after it happens?

The event timestamp must be within the last 7 days and no more than 10 minutes in the future. A webhook that fires at payment time leaves plenty of room for retries.

Measure the ChatGPT channel this week

Free during early access while we onboard the first stores. Leave your email and we send the install link when your spot opens.

No newsletter. One email with the install link, nothing else.