
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
- A ChatGPT Ads account with a Pixel ID (Tools → Conversions).
- A Conversions API key (for the server-side calls).
- Access to your site’s
<head>(or a tag manager like GTM / server-side GTM). - A server endpoint or backend that can send POST requests (for CAPI).
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
type— data shape (e.g."contents","customer_action","plan_enrollment")amount— monetary value in CENTS, as an integer (e.g.$44.00→4400). ⚠️ This is the #1 tracking mistake — sending dollars instead of cents inflates values 100×.currency— ISO code, e.g."USD"contents— array of{ id, name, content_type, quantity, amount }plan_id— for subscription events
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.
- Endpoint:
https://bzr.openai.com/v1/events?pid=<PIXEL-ID> - Auth:
Authorization: Bearer <CONVERSIONS-API-KEY> - Method:
POST, JSON body
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:
id— your unique event id; used withtypeto deduplicate against the pixel.timestamp_ms— must be within the last 7 days and no more than 10 minutes in the future.action_source—"web"for website events.- Set
validate_only: trueto test payloads without recording events.
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) andid(CAPI) for the same event. OpenAI matches on Pixel ID + event type + id. For custom events, also matchcustom_event_nameon 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:
- Hashes must be lowercase, 64-character hex (SHA-256). Normalise first (trim, lowercase the email) before hashing.
- Never send raw email/phone — only the hash.
obrefandip_address/user_agentare sent unchanged. - Every field is optional — send what you have.
oppref / obref (the click identifiers)
oppref— OpenAI’s privacy-preserving click identifier (theirgclidequivalent). Capture it from the landing-page URL when a user arrives from a ChatGPT ad, persist it, and send it with the conversion.obref— read from the__obrefbrowser cookie and pass unchanged in theuserobject.
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:
- Tag every destination URL with consistent UTMs (
utm_source,utm_campaign,utm_content,utm_term) — a strict naming convention, applied to every ad. - Persist
oppref(and UTMs) from the landing URL into your session/order record, so the conversion carries the source with it. - Send
contents[].id(product/SKU) on purchase events so revenue is attributable by product, not just in aggregate. - 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
- Pixel: set
debug: trueininit; watch the browser console and Network tab for calls tobzr.openai.com. - CAPI: send with
validate_only: truefirst; confirm a success response, then flip tofalse. - Dedup check: fire a real test order through both pixel and CAPI with the same id; confirm it appears once in the Ads Manager conversions view.
- Values: confirm amounts show correctly (remember: cents). A $44 order should read $44, not $4,400.
- Match quality: after AAM/advanced matching is live, check the match-rate/quality indicator in Tools → Conversions.
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
- Sending dollars, not cents —
amountis an integer in cents. This is the most common error. - Pixel only — you lose ad-blocked and browser-dropped conversions; add CAPI.
- Mismatched
event_id— if pixel and CAPI ids differ, the same sale double-counts. - Random UUIDs for dedup — use a deterministic id (order id + timestamp) so both sides agree and retries don’t duplicate.
- Sending raw PII — email/phone must be SHA-256, lowercase, 64-char hex. Never raw.
- Stale timestamps —
timestamp_msmust be within 7 days (and ≤10 min ahead). - 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
- OpenAI Developers — JavaScript Pixel
- OpenAI Developers — Conversions API
- OpenAI Developers — Ads overview
- OpenAI Developers — Conversion-optimized campaigns
Prepared for GPT Ads Partner · doubles as the basis for a site article: “ChatGPT Ads Conversion Tracking: Pixel vs Conversions API”.