A full OpenAI pixel GTM setup for single-page applications — tag sequencing, History Change triggers, checkout and purchase events, and how to test every one of them in GTM Preview before you publish.

If you have started running ChatGPT Ads, you have probably already found the part nobody warns you about: getting the ads live is the easy half. Getting conversions to report back is where most setups quietly fail — and on a single-page website, they fail in a way that looks like everything is working.

This guide walks through a complete implementation on a live product: Mobile Mandir, our devotional app at mobilemandirapp.com. It runs on Next.js, which makes it a single-page application, which means the standard tracking advice does not work. We will cover the OpenAI pixel GTM setup, tag sequencing, page view tracking across seven content sections, seva checkout and purchase events, and — the part most guides skip entirely — how to verify each one before you publish.

Why ChatGPT Ads Is Not Tracking Your Conversions

Before the fix, the diagnosis. If your ChatGPT Ads pixel is not firing — or fires but reports nothing — it is almost always one of five things, in roughly this order of frequency:

  • The Custom HTML tag has no <script> wrapper, so GTM injects your JavaScript as plain text and never executes it. The tag still reports “Fired”.
  • Your event tag ran before the base pixel initialised, so oaiq was undefined and the call threw. Again: reported as “Fired”.
  • Your site is a single-page application, so route changes never fire a Page View trigger at all.
  • The event data type does not match the event name, and OpenAI rejects the payload on arrival.
  • Everything works, but you are reading the Conversion Events count instead of the Event Stream, and the count lags.
  • Each one is covered below with the exact fix. If you only have five minutes, the first two are where the odds are.

    A Short Primer on ChatGPT Ads and the OpenAI Pixel (oaiq)

    ChatGPT Ads places sponsored results inside ChatGPT conversations. For a category like devotional content that is genuinely new territory — you are reaching someone at the moment they are asking a question, not scrolling a feed.

    Measurement works the way most ad platforms do. OpenAI gives you a pixel: a JavaScript SDK exposing a single global function, oaiq. Install it once, then call it whenever something worth measuring happens.

    ChatGPT Ads conversion setup screen showing the exact oaiq measure event call to copy
    ChatGPT Ads gives you the exact event call for every conversion you create.

    Three arguments matter: the command (always "measure"), the event name, and an event data object whose type field must match the shape that event expects. OpenAI defines three shapes:

  • contents — page and product interactions: page_viewed, contents_viewed, items_added, checkout_started, order_created
  • customer_action — lead and signup events: lead_created, registration_completed, appointment_scheduled
  • plan_enrollment — subscriptions: subscription_created, trial_started
  • Get the type wrong and the event is rejected silently. That is the first thing to check when events appear to fire but never arrive.

    Why Single-Page Application Tracking Breaks in Google Tag Manager

    Here is the problem in one sentence: on a single-page application, navigating between pages does not load a new page.

    On a traditional website, clicking a link tears down the document and builds a new one. The GTM container re-initialises, fires its gtm.js event, and every Page View trigger re-evaluates. That is the model almost every tracking tutorial assumes.

    A React, Vue or Next.js site does not do this. Clicking from the Mobile Mandir home page to the Aarti section calls history.pushState(). The URL in the address bar changes to /aarti, the content swaps, and the document is never reloaded. GTM has no idea anything happened.

    This is exactly what we hit. The base pixel fired perfectly. The Aarti tag sat in Tags Not Fired.

    GTM Tag Assistant showing the base OpenAI pixel fired while the Aarti page event tag sits under Tags Not Fired
    The failure state: base pixel fires, the page-specific event tag does not.

    The obvious next move is to check the Page URL variable — and this is where the trap springs. It reads exactly what you expect:

    GTM Preview variables panel showing the Page URL variable resolving to the correct Aarti route
    Page URL looks correct. The trigger still never fired.

    The trap: variables in Tag Assistant belong to the event you have selected in the left rail — not to the event the trigger was evaluated on. Your trigger was tested back when the document loaded, on a completely different URL. Everything looks right and nothing fires.

    Opening the tag itself confirms the configuration is sound — valid firing trigger, no blocking triggers, correct HTML. The configuration was never the problem. The navigation model was.

    GTM Tag Details panel showing a valid firing trigger, no blocking triggers and no messages
    Tag Details: valid trigger, no blocking triggers, and still “No messages”.

    Step 1 — OpenAI Pixel GTM Setup With the Official Ads Measurement Pixel Template

    Most guides still tell you to paste the pixel into a Custom HTML tag. You no longer need to. OpenAI has published an official tag template in the GTM Community Template Gallery, and it removes an entire category of problems.

    In GTM: Templates → Tag Templates → Search Gallery → search for “OpenAI Ads Measurement Pixel” → Add to workspace. It will then appear as a native tag type.

    The official OpenAI Ads Measurement Pixel tag template in the GTM Community Template Gallery
    The official OpenAI template, installed from the Community Template Gallery.

    Why this is worth switching to:

  • No hand-written JavaScript, so no missing <script> wrapper — the single most common reason a Custom HTML pixel tag reports “Fired” while executing nothing at all
  • Event names and fields are dropdowns and inputs, not strings you can typo
  • Event tags initialise the pixel themselves, so ordering is far less fragile
  • It is sandboxed, which means it passes stricter Consent Mode and CSP setups that block Custom HTML
  • Use the same Pixel ID on every OpenAI tag in the container. If you are staying on Custom HTML for any reason, the base snippet is:

    <script> (function (w, d, s, u) { if (w.oaiq) return; var q = function () { q.q.push(arguments); }; q.q = []; w.oaiq = q; var js = d.createElement(s); js.async = true; js.src = u; var f = d.getElementsByTagName(s)[0]; f.parentNode.insertBefore(js, f); })(window, document, "script", "https://bzrcdn.openai.com/sdk/oaiq.min.js"); oaiq("init", { pixelId: "YOUR-PIXEL-ID", debug: true }); </script>

    The <script> tags are mandatory — GTM’s Custom HTML tag injects its contents as HTML, so raw JavaScript becomes a text node and never runs. And when you inspect the tag later you will see it rendered as type=\"text/gtmscript\" with escaped quotes. That is normal GTM behaviour, not corruption. Rewriting it will not help.

    Keep debug: true while building. Remove it before you publish.

    Step 2 — Google Tag Manager Tag Sequencing: Base Pixel First

    This step is easy to skip and expensive to skip. Your base pixel and your event tags typically fire on the same event, and GTM does not guarantee the order they run in. If an event tag executes before the pixel has initialised, oaiq is undefined, the call throws, and the tag still reports “Fired” in Tag Assistant. You get a green tick and zero data.

    On every OpenAI event tag: Advanced Settings → Tag Sequencing → tick “Fire a tag before this tag fires” → select your base pixel tag.

    What correct looks like: on Mobile Mandir the base pixel shows “Fired 8 times” for seven content event tags plus its own trigger. That is sequencing doing its job — the SDK snippet guards itself with an early return, so repeated initialisation is harmless.

    Step 3 — GTM Single-Page Application Tracking With the History Change Trigger

    Your base pixel covers the first document load of the session. Every navigation after that is invisible to it. You need a second signal.

    The GTM History Change trigger

    GTM ships a History Change trigger that listens for pushState calls. Triggers → New → History Change → Some History Changes, with a condition matching the route:

    A GTM History Change trigger configured for Some History Changes with a condition matching the route
    A History Change trigger — the missing half of SPA page view tracking.

    Do not remove your Page View trigger. Attach both to the same tag. Someone clicking a ChatGPT ad lands directly on the URL — a real document load that fires Page View, not History Change. Swap to History Change alone and you will track internal browsing perfectly while missing the exact paid traffic you are paying to measure.

    GTM SPA tracking on Next.js: watch for duplicate fires

    Next.js frequently pushes history more than once per navigation, so a single route change can fire your tag two or three times. You will see it immediately in Tag Assistant as “Fired 2 times” on one tag while its neighbours show “Fired 1 time”.

    Harden the trigger with a second condition using Data Layer Variables instead of Page URL:

    gtm.newUrl contains /aarti gtm.oldUrl does not contain /aarti
  • gtm.newUrl is the value the history event itself carried, so it cannot go stale mid-navigation the way Page URL can
  • gtm.oldUrl stops the tag re-firing when the route pushes again on a URL it is already on
  • GTM pushes both keys into the data layer on every history change. Create them once as Data Layer Variables and reuse them across every route tag.

    The more reliable option: a dataLayer event and Custom Event trigger

    History Change is a workaround. If you can get twenty minutes of developer time, do this instead — it is more reliable, self-documenting, and carries real data with the event:

    window.dataLayer = window.dataLayer || []; window.dataLayer.push({ event: "mm_virtual_pageview", content_id: "hanuman-chalisa", content_name: "Hanuman Chalisa", content_type: "chalisa" });

    Then in GTM: Trigger type → Custom Event, event name mm_virtual_pageview. You now control exactly when the event fires and what rides along with it, instead of inferring intent from a URL string. Feed those values into the tag’s contents array:

    oaiq("measure", "contents_viewed", { type: "contents", contents: [{ id: {{DLV - content_id}}, name: {{DLV - content_name}}, content_type: {{DLV - content_type}} }] });

    Populate contents, always. An event sent with an empty contents array arrives at OpenAI as {"contents": [], "type": "contents"}. If seven different section tags all send that identical payload, nothing in the data distinguishes an Aarti view from a Katha view. Name your conversions carefully in the dashboard and give each event real content data.

    Step 4 — Seva Checkout and Purchase Events

    Page views tell you nothing about whether the campaign is working. On Mobile Mandir the events carrying money are the seva flow: someone starts a booking, then completes payment.

    checkout_started

    Have your developer push this when the user reaches the payment step:

    window.dataLayer.push({ event: "mm_checkout_started", seva_id: "gau-seva-101", seva_name: "Gau Seva", amount: 501, currency: "INR" });

    The GTM tag, on a Custom Event trigger for mm_checkout_started:

    oaiq("measure", "checkout_started", { type: "contents", amount: {{DLV - amount}}, currency: {{DLV - currency}}, contents: [{ id: {{DLV - seva_id}}, name: {{DLV - seva_name}}, content_type: "seva", quantity: 1 }] });
    order_created

    This fires on the payment success screen — on confirmed payment, never on redirect to the gateway. Pass the order ID as an event_id:

    oaiq("measure", "order_created", { type: "contents", amount: {{DLV - amount}}, currency: {{DLV - currency}}, contents: [{ id: {{DLV - seva_id}}, name: {{DLV - seva_name}}, content_type: "seva", quantity: 1 }] }, { event_id: {{DLV - order_id}} });

    That event_id matters more than it looks. When you later add OpenAI’s server-side Conversions API — and for payment events you should, because browser events get blocked — matching on pixel ID, event name and event_id is how OpenAI knows the two reports are one order rather than two sales.

    Field discipline: integers for amount and quantity, and only documented fields inside contents[] — id, name, content_type, quantity. Adding something sensible-looking like price gets the event rejected.

    App signup uses a different shape entirely:

    oaiq("measure", "registration_completed", { type: "customer_action" });

    Step 5 — Testing in GTM Preview When a Tag Is Not Firing

    This is where most implementations go wrong, because “Fired” in Tag Assistant proves far less than people assume. It confirms GTM executed the tag. It says nothing about whether the JavaScript ran, or whether OpenAI received anything.

    Work through four checks in order. Do not skip to the last one.

    Check 1 — Did the trigger evaluate on the right event?

  • Click Preview and load the site.
  • In the left rail, select the specific event you care about — Container Loaded for a hard load, History for an in-app navigation. Every SPA route change appears as its own History entry, and that rail is where you confirm your navigation registered at all.
  • Open Variables and read Page URL and gtm.newUrl at that event.
  • If values look right but the tag is in Tags Not Fired, click the tag — Tag Assistant shows each trigger condition pass/fail, plus blocking triggers.
  • GTM Tag Assistant showing History events stacked in the left rail with every section tag firing correctly
    Working state: History events stacked in the left rail, and every section tag firing.

    Two things to read carefully in that panel. The base pixel shows “Fired 8 times” — sequencing working. And the Aarti tag shows “Fired 2 times” where its siblings show one: that is the Next.js duplicate-history issue, caught before it ever reached reporting.

    When a GTM Custom HTML tag is not working at all

    If Tag Assistant reports Fired but nothing ever reaches OpenAI, the tag is almost certainly executing nothing. Re-read the HTML field: raw JavaScript with no <script> wrapper is injected as a text node, and GTM has no way to tell you. This is the single most common cause of a GTM Custom HTML tag not working, and it is invisible in every panel except the Network tab.

    Switching that tag to the official OpenAI template removes the failure mode entirely, which is the main practical argument for using it.

    Check 2 — Did the JavaScript execute?

    With debug: true set, open the browser console. The OpenAI SDK logs what it accepts and rejects. A rejection usually means a bad type field or an undocumented property inside contents[].

    Check 3 — Did the request leave the browser?

    DevTools → Network, filter for “openai” or “bzrcdn”. One outbound request per event. No request means nothing was sent, whatever Tag Assistant claims.

    Check 4 — Did OpenAI receive it?

    In ChatGPT Ads: Conversions → Event Stream, filtered to your pixel. Events land within seconds with full detail — event type, API channel, action source, identifiers, and the raw event data JSON.

    ChatGPT Ads Event Stream showing incoming pixel events with type, action source and raw event data
    Event Stream is the source of truth — not Tag Assistant, not the Conversion Events counts.

    Use Event Stream, not the Conversion Events tab. Those counts lag, and a fresh setup showing 0 there while events stream in is normal — it will send you chasing a problem that does not exist.

    A note on identifier coverage

    You will likely see Identifier coverage at 0%, with Oppref, Email and External ID all “Not provided”. For your own manual tests that is expected — you did not arrive via an ad click, so there is no oppref to capture.

    If it stays at 0% for real traffic, conversions cannot be attributed back to ad clicks and the campaign optimises on nothing. Pass hashed user data at initialisation:

    oaiq("init", { pixelId: "YOUR-PIXEL-ID", user: { email_sha256: "<sha256 of lowercased, trimmed email>", phone_number_sha256: "<sha256 of normalised phone>", external_id_sha256: "<sha256 of your user id>" } });

    Normalise before hashing: emails trimmed and lowercased; phone numbers stripped of the leading +, leading zeroes, spaces, brackets, dots and hyphens, leaving 8–15 digits. Hash client-side — never send raw values.

    What the Finished Setup Looks Like

    Once every route and the seva flow are wired up, your conversion list stops being a single page view and starts describing the actual product:

    ChatGPT Ads conversions list showing eight conversions mapped to real user behaviour including Seva Completed
    Eight conversions mapped to real user behaviour, not one generic page view.

    Seva Completed maps to order_created. The content sections map to page views. That distinction is the whole point: you can now tell the campaign to optimise toward completed sevas rather than toward whoever loads a page most cheaply.

    Debugging Reference: Why Your GTM Tag Is Not Firing

    The failure modes we hit, and what each actually means:

    Symptom Likely cause Fix
    Tag shows Fired, OpenAI receives nothingCustom HTML has no <script> wrapper, so nothing executedWrap the JavaScript in <script> tags, or switch to the official template
    Tag shows Fired, console throwsEvent tag ran before the base pixel initialisedSet the base pixel as a setup tag under Tag Sequencing
    Tag never fires; Page URL looks correctSPA navigation — no gtm.js on route changeAdd a History Change or Custom Event trigger alongside Page View
    Tag fires 2–3× per navigationNext.js pushes history multiple timesAdd condition: gtm.oldUrl does not contain the path
    Event rejected in consoleWrong type value, or undocumented field in contentsMatch the documented shape; integers for amount and quantity
    Works internally, not from adsPage View trigger was replaced by History ChangeAttach both triggers to the same tag
    Conversions indistinguishable from each otherEmpty contents array on every eventPopulate id, name and content_type per section
    Identifier coverage stuck at 0%No hashed user data being passedAdd the user object to oaiq init
    Conversion Events shows 0Dashboard aggregation lagCheck Event Stream instead

    The Checklist Before You Publish

  • Pixel installed via the official GTM template rather than hand-written Custom HTML
  • Base pixel set as the setup tag on every OpenAI event tag
  • Each conversion tag has both a Page View and a History Change (or Custom Event) trigger
  • History Change triggers deduplicated with a gtm.oldUrl condition
  • Every event carries a populated contents array
  • order_created carries amount, currency and an event_id
  • Hashed user data passed at init so identifier coverage is not 0%
  • Every event confirmed in Event Stream, not just in Tag Assistant
  • debug: true removed
  • The conversion the campaign optimises toward is a purchase or signup — not a page view
  • The One That Matters Most

    If you take a single thing from this guide, make it the last checklist item. A page view is not a conversion. Optimise a ChatGPT Ads campaign toward page_viewed and the algorithm will do exactly what you asked — find you the cheapest possible visitors, in volume, who do nothing. Point it at order_created with a real INR amount attached and it optimises toward people who complete a seva.

    The tracking work in this guide exists to make that second option possible. Everything else is plumbing.

    New to the platform itself? Start with our complete guide to ChatGPT Ads, then come back here to wire up the measurement.

    Hitting the Same Walls?

    Setting up ChatGPT Ads tracking on your own site and hitting the same walls? Seoily builds and audits measurement stacks across GA4, GTM, Meta and the new AI ad platforms.

    Seoily Technologies

    Get Your ChatGPT Ads Tracking Audited

    Pixel installation, tag sequencing, SPA route tracking, purchase events and server-side conversions — wired up, verified in Event Stream, and pointed at the conversion that actually earns money.

    Latest Insights

    Related Blogs

    Drop Us a Line

    Connect with Seoily

    Grow your business using the tools, strategies, and support it needs to excel within the industry. Contact Seoily today to find out how our end-to-end solution-based approach can help you turn challenges into solutions to success.

    IN

    D 235A,Sector 74, Mohali, Punjab 160055

    To More Inquiry
    +91 98129 69000
    UK

    8 O'Donnell Road, Leamington Spa CV31 2FJ

    To More Inquiry
    +44 778 489 8418