# ChatGPT Ads conversion optimization: your conversion data now trains the bidding

> oCPC campaigns optimize toward one tracked conversion event, so duplicates, missing orders and late events distort bids. How to keep the signal clean.

Canonical: https://convrail.com/blog/chatgpt-ads-ocpc/

Conversion-optimized (oCPC) campaigns in ChatGPT Ads optimize delivery toward one tracked conversion event while you keep paying per valid click, so every order you send to OpenAI, and every order you fail to send, shapes what the auction learns. Duplicates inflate the signal, missing events starve it, and events older than 7 days are rejected outright. A deduplicated, complete and timely conversion pipeline is the prerequisite, not a refinement.

## What OpenAI documents about conversion-based bidding

This section only repeats what the official pages say, because the rest of the article depends on it and because bidding claims are easy to inflate.

The [Campaigns reference](https://developers.openai.com/ads/api-reference/campaigns) defines a `bidding_type` field with three values: `"impressions"`, `"clicks"`, or `"conversions"`, and notes that it "Defaults to `"impressions"`". Next to it sits `conversion_event_setting_ids`, described as "For conversions, exactly one active standard event setting ID from this account."

The [Conversion-Optimized Campaigns guide](https://developers.openai.com/ads/conversion-optimized-campaigns) explains the intent: "Use a conversion-optimized cost-per-click (oCPC) campaign when you want to optimize delivery toward one tracked conversion event while continuing to pay for valid clicks." It lists prerequisites in plain terms. You must have "set up conversion tracking with the JavaScript Pixel, the Conversions API, or both", and "The Conversions API is a more reliable tracking source than the pixel alone." You need "exactly one active standard conversion event to use as the optimization goal", and "Custom events cannot be oCPC optimization goals."

Bidding itself is expressed at the ad group level. The [Ad Groups reference](https://developers.openai.com/ads/api-reference/ad-groups) sets `billing_event_type` to "`impression` for impression campaigns; `click` for click and conversion campaigns", and reinterprets the bid for oCPC: "For an oCPC campaign, `max_bid_micros` is the CPA bid even though billing uses valid clicks." The worked example in the same reference: "100000000 is a $100.00 CPA bid for a USD account." Billing does not move to pay per conversion. In the guide's words, "OpenAI charges you only when a valid click occurs, and the auction determines the actual CPC."

Two more sentences settle which conversions count. From the [Insights reference](https://developers.openai.com/ads/api-reference/insights): "View-through conversions are for reporting only. CPA, post-click CVR, bidding, billing, and conversion optimization remain click-through-based." And from the [Measurement Pixel reference](https://developers.openai.com/ads/measurement-pixel): "Click-through attribution uses the applicable configured click window."

| Question | Documented answer | Where |
| --- | --- | --- |
| Can a campaign bid on conversions? | Yes, `bidding_type: "conversions"` | Campaigns reference |
| Which event is optimized? | One active standard event setting, no custom events | Conversion-Optimized Campaigns guide |
| What is the bid? | `max_bid_micros` as a CPA bid, billed per valid click | Ad Groups reference |
| Do view-through conversions train bidding? | No, click-through only | Insights reference |
| How is the learning phase sized or timed? | Not documented on the pages we read | n/a |
| Which features the model uses to predict conversion likelihood? | Not documented on the pages we read | n/a |

The last two rows matter as much as the first four. The documentation does not publish a minimum conversion volume, a learning-phase duration or the internals of the prediction. Anyone who quotes you a threshold is guessing. What the guide does say is qualitative: "Keep conversion tracking healthy. Incomplete or incorrectly configured tracking can make reporting and optimization less effective", and "Use a conversion event with enough volume to evaluate performance."

## Why the conversion signal is the only lever you fully control

In an oCPC campaign you set a CPA bid, a budget and a creative. The auction sets the actual CPC, and the optimization decides where and to whom your ad is delivered based on the conversion event you designated. That event stream is the one input entirely yours to get right, and it feeds the delivery optimization, the reported CPA and CVR, and the conversions column at once.

Because optimization is click-through based, the chain runs through the `oppref` identifier. The Measurement Pixel reference states that the pixel "captures `oppref` from the landing page URL" and "stores `oppref` in a first-party `__oppref` cookie so later page views can reuse it." The [Conversions API reference](https://developers.openai.com/ads/conversions-api) describes the same field on server events as "An opaque, OpenAI-provided attribution identifier. Pass the original string without modification." A conversion that reaches OpenAI without a way to tie it to a click is a conversion the optimization cannot learn from, whatever your dashboard says.

So the useful question is not "is my pixel firing" but "does every real order reach OpenAI exactly once, inside the window, with the click context attached". The three failure modes below are the ways that question gets a wrong answer.

## Failure mode 1: duplicates inflate the signal

The deduplication rule is short. The Conversions API reference: "Deduplication uses your Pixel ID, `event_name`, and `id`. OpenAI uses the first event it receives for a matching key and ignores later duplicates." The pixel side mirrors it through the `event_id` option, which the Measurement Pixel reference describes as "Set a unique ID to identify the same event sent from the browser and server."

Duplicates appear when that key is not honored:

- The pixel sends the checkout with an ID generated in the browser, and the server sends the same order with the database ID. Two keys, two conversions.
- A retry after a network error generates a fresh ID instead of resending the same one.
- A thank-you page reload fires the pixel again without a stable ID.

Inflated conversions make the campaign look cheaper than it is and, under oCPC, teach the optimization that clicks of a certain kind convert twice as often as they do.

The fix is one deterministic ID per order, shared by every source. Convrail uses `order_<orderId>` on the pixel at checkout and on the server event built from the paid-order webhook, so the two copies collapse into one at OpenAI. The design reasoning is covered in [OAIQ pixel vs Conversions API](/blog/oaiq-pixel-vs-conversions-api/).

## Failure mode 2: missing events starve the signal

Events go missing in the browser and on the server, for different reasons.

In the browser, a pixel only runs when a script is allowed to run and a request is allowed to leave the tab. Ad blockers, consent banners that fire after the visitor has moved on, tabs closed on the thank-you page and privacy features all remove events before they reach OpenAI. We have not measured this loss across stores, so we publish no percentage for it. The point stands without a number: a pixel-only setup under-reports, and an oCPC campaign fed by it is optimized toward a subset of your real buyers.

On the server, the losses are quieter and more mechanical:

- The order webhook fails or is not subscribed, so the order never becomes an event.
- A 4xx response is treated as transient and retried forever, or treated as final and discarded without anyone looking.
- One malformed event sinks the whole batch. The Conversions API reference is explicit: "The API accepts batches of up to 1,000 events. If one event in the batch fails, the full batch fails." A single order with a bad currency code or a mistyped hash can take 999 valid orders down with it.
- Consent is declined and the event is correctly withheld, but nothing records that it was withheld, so the gap looks like a bug rather than a decision.

Starvation is the mirror image of inflation: the optimization sees fewer conversions than happened, the reported CPA rises, and the campaign is judged or throttled on incomplete data.

## Failure mode 3: late timestamps are dropped

The Conversions API reference sets a hard window: the `timestamp_ms` "must be within the last 7 days and no more than 10 minutes in the future."

Both edges bite in practice. On the old side, a backfill of last month's orders is rejected, and because a batch fails as a whole, mixing one stale order into a batch of fresh ones rejects the fresh ones too. A queue that stalls for a week, then drains, sends events that are all dead on arrival. On the future side, a server whose clock drifts by more than ten minutes, or a job that uses "now" instead of the order time and runs ahead of a lagging database replica, produces events OpenAI refuses.

## What a clean conversion pipeline looks like

Putting the three failure modes together gives a short specification. A pipeline that satisfies every line is one you can point an oCPC campaign at without second-guessing the numbers.

| Property | What it means in practice |
| --- | --- |
| One ID per conversion, across sources | The same `order_<orderId>` on the pixel `event_id` and the API `id`, under the same Pixel ID |
| Server copy from the paid-order webhook | Every order becomes an event whether or not the browser fired |
| Click context carried to the server | `oppref` read from the landing URL, kept in the `__oppref` cookie for 7 days, attached to the conversion |
| Real order timestamps | `timestamp_ms` is the order time, never the send time, and never older than 7 days |
| Validation before batching | Every event checked locally so one bad row cannot fail a batch of 1,000 |
| Retries with a floor | Exponential backoff on 429 and 5xx, no retry on 4xx, a dead-letter queue after the last attempt, and an alert |
| Personal data hashed | Emails and phone numbers SHA-256 hashed before storage or transmission, never sent in clear |
| Test mode | `validate_only: true` while wiring things up, so nothing is counted until the payloads are accepted |

A server event that meets the specification looks like this:

```json
POST https://bzr.openai.com/v1/events?pid=<PIXEL-ID>
Authorization: Bearer <key>

{
  "validate_only": false,
  "integration_source": "convrail",
  "events": [
    {
      "id": "order_1001",
      "type": "order_created",
      "timestamp_ms": 1757001600000,
      "oppref": "<value read from the landing URL>",
      "source_url": "https://shop.example/checkouts/thank-you",
      "action_source": "web",
      "user": {
        "emails_sha256": ["<64 lowercase hex chars>"],
        "ip_address": "203.0.113.10",
        "user_agent": "Mozilla/5.0 ..."
      },
      "data": {
        "type": "contents",
        "amount": 12990,
        "currency": "EUR",
        "contents": [
          { "id": "sku-42", "quantity": 1, "amount": 12990, "currency": "EUR" }
        ]
      }
    }
  ]
}
```

Two details in that payload are easy to get wrong. The amount is an integer in the currency's minor unit (the reference's own example is "use 4200 for $42.00"), so 129.90 EUR is `12990`, not `129.9`. And the hashed email is the SHA-256 of the trimmed, lowercased address, rendered as 64 lowercase hexadecimal characters; anything else is not a match key.

This is the pipeline Convrail runs for Shopify and WooCommerce stores: browser events through the OAIQ pixel, server events from the `orders/paid` webhook (or the WooCommerce order hooks), batches of up to 1,000 with backoff and a dead-letter queue, hashing on receipt with an automated guard that refuses any outgoing payload containing clear personal data, and a health alert when a batch fails permanently. The Shopify walkthrough is in [How to track ChatGPT Ads conversions on Shopify](/blog/track-chatgpt-ads-conversions-shopify/).

## How to audit your own pipeline before switching to oCPC

You do not need to trust a vendor's description, including ours. Six checks, each taking minutes, tell you whether your conversion signal is fit to bid on.

### 1. Trace one order end to end

Pick an order placed in the last hour. Find its browser event and its server event and confirm they carry the same ID under the same Pixel ID. If you cannot see both copies side by side, you cannot know whether they deduplicate. Convrail's Events screen lists every event with its source, status and timestamp for exactly this reason.

### 2. Confirm the optimization event is a standard one

Open your event settings. The [Conversion Setup reference](https://developers.openai.com/ads/api-reference/conversion-setup) shows an event setting with an `event_type` that is either a "supported event or `custom`", plus an `attribution_window_days`. Your oCPC goal must be a standard type such as `order_created`, since custom events are excluded, and it must be the one your pipeline actually populates.

### 3. Check the pixel is arriving right now

The same reference documents `GET /conversions/events` with a `pid` parameter, which "Returns up to 50 conversion events from previous 15 minutes". Browse your store, trigger a page view and an add-to-cart, and confirm they show up. If nothing appears within a quarter of an hour, the browser layer is broken regardless of what your analytics say.

### 4. Send a validate-only batch

Take yesterday's orders, build the batch as you would send it, and post it with `validate_only: true`. The reference describes the flag as "Validates events without saving them when `true`." A rejected batch here is a batch that would have silently lost orders in production.

### 5. Compare counts over a fixed window

Count your paid orders for the last seven days. Compare with the `conversions` metric for the same days in the Ads Manager or the Insights endpoint, remembering that "`conversions` is always equal to `click_through_conversions`". The Ads Manager figure should be a subset of your orders: the ones attributed to a click within the window. If it is larger, you have duplicates. If the click-attributed share moves sharply week to week with no change in spend, you probably have intermittent gaps.

### 6. Look at the oldest timestamp in your queue

If your sending queue can ever hold an event older than a few hours, ask what happens at day 7. A queue with no age alarm is a queue that will one day fail an entire backlog at once.

## Common mistakes

- Choosing `bidding_type: "conversions"` on a store whose only source is the pixel. The guide's own words: the Conversions API "is a more reliable tracking source than the pixel alone."
- Generating the pixel's `event_id` in the browser with a random value while the server uses the order number. Same sale, two keys.
- Sending `amount` in major units. `42` is forty-two cents to the API, not forty-two dollars.
- Stamping events with the send time instead of the order time, which shifts orders relative to the click window and, after a queue stall, out of the 7-day window entirely.
- Treating a 4xx as retryable. The batch will fail the same way every time; the fix is in the payload, and the event belongs in a dead-letter queue with its error, not in an infinite loop.
- Pointing the optimization at a custom event. It is not allowed as an oCPC goal, and a campaign created that way is not optimizing toward what you think.

## What to do next

If you are about to switch a ChatGPT Ads campaign to conversion-optimized bidding, run the six checks above first, then look at how [Convrail's tracking](/tracking/) sends each order once, from both the browser and the server, with the click context attached and nothing personal in clear.