← All guides
One clean signal beats guesswork

ChatGPT Ads Conversion Tracking: Pixel, Conversions API & Deduplication

Pixel + Conversions API + deduplication + advanced matching. The exact stack GPT Ads Partner sets up so you know which campaigns actually work — no guessing.

Why this matters: ChatGPT Ads optimises on the conversion signal you feed it. Weak or browser-only tracking loses events, and the platform then optimises on bad data. This guide builds a complete, deduplicated signal — the foundation everything else depends on. All field names below are from OpenAI’s developer docs (developers.openai.com/ads); verify against the live docs as the platform evolves.


The stack at a glance

Layer What it does Why
JavaScript pixel Fires events from the browser Fast to deploy, captures client-side context
Conversions API (CAPI) Sends events from your server Reliable — survives ad-blockers, ITP, browser loss
Deduplication Matches pixel + CAPI events by event_id Run both for coverage, count each sale once
Advanced matching Hashed customer data on events Improves match rate of conversions → clicks
Parameters / attribution UTMs + oppref/obref identifiers So every result traces to its source campaign/ad

Best practice: run the pixel AND CAPI together, deduplicated. The pixel alone is lossy; CAPI alone misses client context. Together, deduplicated, you get completeness without double-counting.


Prerequisites


Step 1 — Install the JavaScript pixel

Add to your site’s <head> on every page:

<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>" });
</script>

Add debug: true inside init while testing. The global function is oaiq (OpenAI’s equivalent of Meta’s fbq).


Step 2 — Fire standard events

Syntax: oaiq("measure", eventName, eventData, options)

Standard event names

page_viewed · contents_viewed · items_added · checkout_started · order_created · lead_created · registration_completed · appointment_scheduled · subscription_created · trial_started

Event data fields

Example: a purchase

oaiq("measure", "order_created",
  {
    type: "contents",
    amount: 4400,            // $44.00 in cents
    currency: "USD",
    contents: [
      { id: "SKU123", name: "Medical Cat Litter 5kg", content_type: "product", quantity: 1, amount: 4400 }
    ]
  },
  { event_id: "order_created_user123_1773892800000" }   // for dedup — see Step 4
);

Example: a lead

oaiq("measure", "lead_created", { type: "customer_action" },
  { event_id: "lead_created_user123_1773892800000" });

Custom events

oaiq("measure", "custom", { type: "custom" }, { custom_event_name: "quote_requested" });

Custom names: 1–64 chars, lowercase, alphanumeric + underscores/dashes.


Step 3 — Set up the Conversions API (server-side)

Send the same conversions from your backend for reliability.

Request body

{
  "validate_only": false,
  "events": [
    {
      "id": "order_created_user123_1773892800000",
      "type": "order_created",
      "timestamp_ms": 1773892800000,
      "source_url": "https://yoursite.com/thank-you",
      "action_source": "web",
      "user": { /* advanced matching — Step 5 */ },
      "data": { "type": "contents", "amount": 4400, "currency": "USD",
                "contents": [ { "id": "SKU123", "quantity": 1, "amount": 4400 } ] }
    }
  ]
}

Field notes:


Step 4 — Deduplication (the critical bit)

Running pixel + CAPI means the same sale can arrive twice. Prevent double-counting:

Send the identical event_id (pixel) and id (CAPI) for the same event. OpenAI matches on Pixel ID + event type + id. For custom events, also match custom_event_name on both sides.

Use a deterministic ID so both sides generate the same value independently, and retries don’t duplicate:

<event_type>_<user_or_order_id>_<timestamp_ms>
e.g.  order_created_user123_1773892800000

Prefer a stable business key (order ID) over a random UUID — that way the pixel and your server both produce the same id for the same order.


Step 5 — Advanced matching (improves match rate)

Attach a user object to each event with hashed customer data. This helps OpenAI match conversions to the click that drove them. It improves match rate — set expectations accordingly (it is not a magic revenue-recovery switch).

The user object

{
  "obref": "<value of the __obref browser cookie, sent UNCHANGED>",
  "email_sha256": "<lowercase 64-char hex SHA-256 of the email>",
  "external_id_sha256": "<lowercase 64-char hex SHA-256 of your user id>",
  "country": "US",
  "city": "san francisco",
  "zip_code": "94107",
  "ip_address": "203.0.113.1",
  "user_agent": "Mozilla/5.0 ..."
}

Rules:

oppref / obref (the click identifiers)

Automatic Advanced Matching (AAM) — the easy win

Rather than wiring hashed fields manually, you can enable the automatic version: Tools → Conversions → Data Source → “Enable for all Web data sources.” It captures and hashes available customer data automatically. Turn it on — it’s low-effort and improves matching. (Manual user-object matching via CAPI gives you more control; use both where it makes sense.)


Step 6 — Parameters & attribution (so you know what’s working)

Tracking that fires isn’t the same as tracking you can read. To know which campaign/ad/product drove a sale:

  1. Tag every destination URL with consistent UTMs (utm_source, utm_campaign, utm_content, utm_term) — a strict naming convention, applied to every ad.
  2. Persist oppref (and UTMs) from the landing URL into your session/order record, so the conversion carries the source with it.
  3. Send contents[].id (product/SKU) on purchase events so revenue is attributable by product, not just in aggregate.
  4. Standardise campaign/ad naming in the Ads Manager so reports are legible at a glance.

This is what turns “we got sales” into “campaign X, ad B, product SKU123 drove the sales” — the difference between scaling a winner and feeding a loser.


Step 7 — Verify & QA


Standard events reference

Event Use for
page_viewed Page loads (base signal)
contents_viewed Product / content views
items_added Add to cart
checkout_started Begin checkout
order_created Purchase (send amount in cents + contents)
lead_created Lead / enquiry
registration_completed Account sign-up
appointment_scheduled Booking
subscription_created New subscription (plan_id)
trial_started Free-trial start

For conversion-optimized (oCPC) campaigns, pick exactly one active standard event as the optimisation goal — custom events can’t be oCPC goals.


Common mistakes to avoid

  1. Sending dollars, not centsamount is an integer in cents. This is the most common error.
  2. Pixel only — you lose ad-blocked and browser-dropped conversions; add CAPI.
  3. Mismatched event_id — if pixel and CAPI ids differ, the same sale double-counts.
  4. Random UUIDs for dedup — use a deterministic id (order id + timestamp) so both sides agree and retries don’t duplicate.
  5. Sending raw PII — email/phone must be SHA-256, lowercase, 64-char hex. Never raw.
  6. Stale timestampstimestamp_ms must be within 7 days (and ≤10 min ahead).
  7. No parameters — tracking without UTMs/SKUs tells you that it converted, not what converted.

GTM / server-side GTM note

If you use Google Tag Manager, community tags exist for both the pixel (web GTM) and the Conversions API (server-side GTM), which handle oppref capture and user-data hashing for you. These are a faster path than hand-coding for most sites — evaluate them against a custom build.


Sources

Prepared for GPT Ads Partner · doubles as the basis for a site article: “ChatGPT Ads Conversion Tracking: Pixel vs Conversions API”.