> For the complete documentation index, see [llms.txt](https://docs.chord.co/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.chord.co/activate/event-streaming/destinations/advertising/openai-ads/openai-ads-conversions-api.md).

# OpenAI Ads Conversions API

**OpenAI Ads Conversions API** is OpenAI's server-side conversion API for ChatGPT Ads. This destination sends conversion events directly from Chord to OpenAI, so measurement is unaffected by ad blockers, browser tracking restrictions and cookie expiry.

Pair it with the **OpenAI Ads Pixel** destination. The two are complementary: the pixel captures OpenAI's `oppref` click identifier in the browser, which the Conversions API needs but does not collect for itself.

## Getting started

Create your credentials in **OpenAI Ads Manager** under **Tools > Conversions**, which offers a **Conversion Key** and a **Pixel ID**.

{% hint style="warning" %}
**Use the Conversion Key — not the API key under Settings.** Ads Manager exposes three credentials and only two of them belong here. The key under **Settings** authenticates OpenAI's separate management API, and the Conversions API rejects it as unauthorized. Chord never needs it.
{% endhint %}

The Conversion Key is **per ad account, not per pixel** — one key covers every pixel in the account.

## Settings

* **Destination Name** (required): a name to tell this destination apart.
* **Pixel ID** (required): your pixel ID from OpenAI Ads Manager under **Tools > Conversions**. Use the same ID as the OpenAI Ads Pixel destination, or OpenAI counts one purchase twice.
* **API Key** (required): your **Conversion Key** from the same place, not the API key under **Settings**.
* **Storefront URL** (optional): your storefront address, for example `https://www.example.com`. OpenAI requires a source URL on every event, and server-side (back-end) events carry none, so Chord uses this value as the fallback. **Required for any back-end connection.** See [Source URL](#source-url).
* **Custom Event Mapping** (optional): routes a Chord event with no standard OpenAI equivalent onto a custom event you registered in Ads Manager. Use the same mapping as the OpenAI Ads Pixel destination.
* **Validate Only** (optional): OpenAI validates each event and records nothing. Use it to test, then turn it off. Off by default.
* **Consent Requirements** (optional): the consent categories a visitor must accept before Chord sends data to this destination. See [Consent](/activate/event-streaming/consent.md).

## Connect

Add **OpenAI Ads Conversions API** from the destination catalog, using the settings above. See [Connect a destination](/activate/event-streaming/connect-a-destination.md) for the full steps.

## Verifying an integration

Enable **Validate Only** before sending live traffic. OpenAI will validate the full shape of every event and record nothing, so you can confirm the integration is correct against a production pixel without affecting reporting. Turn it off once you are satisfied.

## Deduplication

Every event carries `id`, set to the Chord message ID, so a conversion reported by both the browser and your server is recorded once. See [How they deduplicate](/activate/event-streaming/destinations/advertising/openai-ads.md#how-they-deduplicate).

## Event mapping

OpenAI accepts a fixed set of event names. A Chord event with no equivalent is skipped and logged — it is not forwarded under a made-up name, because OpenAI silently discards an unregistered event name, which is indistinguishable from a successful send.

| Chord event                                                                   | OpenAI event             | Notes                          |
| ----------------------------------------------------------------------------- | ------------------------ | ------------------------------ |
| `page`                                                                        | `page_viewed`            | `data.type`: `contents`        |
| [`Product Viewed`](/collect-data/tracking-plan/product-viewed.md)             | `contents_viewed`        | `data.type`: `contents`        |
| [`Product Added`](/collect-data/tracking-plan/product-added.md)               | `items_added`            | `data.type`: `contents`        |
| [`Checkout Started`](/collect-data/tracking-plan/checkout-started.md)         | `checkout_started`       | `data.type`: `contents`        |
| [`Order Completed`](/collect-data/tracking-plan/order-completed.md)           | `order_created`          | `data.type`: `contents`        |
| [`Signed Up`](/collect-data/tracking-plan/signed-up.md)                       | `registration_completed` | `data.type`: `customer_action` |
| [`Email Captured`](/collect-data/tracking-plan/email-captured.md)             | `lead_created`           | `data.type`: `customer_action` |
| [`Subscription Created`](/collect-data/tracking-plan/subscription-created.md) | `subscription_created`   | `data.type`: `plan_enrollment` |

To forward anything else, first create a custom event in ChatGPT Ads Manager, then add a **Custom Event Mapping** entry pointing your Chord event name at it. Names are lowercased and spaces become dashes, so `Wishlist Added` becomes `wishlist-added`; the result must be 1–64 characters of lowercase letters, numbers, underscores or dashes.

## Event attributes

| Chord source (priority order)                                           | OpenAI field        | Notes                                                 |
| ----------------------------------------------------------------------- | ------------------- | ----------------------------------------------------- |
| `messageId`                                                             | `id`                | The deduplication key                                 |
| —                                                                       | `type`              | Set from the event mapping table                      |
| **Custom Event Mapping**                                                | `custom_event_name` | Custom events only                                    |
| `timestamp` → `receivedAt`                                              | `timestamp_ms`      | Milliseconds since epoch                              |
| —                                                                       | `action_source`     | Always `web`                                          |
| `page.url` → `context.page.url` → `properties.url` → **Storefront URL** | `source_url`        | Required by OpenAI on every event. See **Source URL** |
| `properties.oppref` → the captured click identifier                     | `oppref`            | See **Attribution** below                             |
| See below                                                               | `data`              |                                                       |

### Conversion data

| Chord source (priority order)                                  | OpenAI field | Notes                                                        |
| -------------------------------------------------------------- | ------------ | ------------------------------------------------------------ |
| —                                                              | `type`       | Set from the event mapping table. Required by OpenAI         |
| `properties.total` → `properties.revenue` → `properties.value` | `amount`     | **Minor currency units.** See below                          |
| `properties.currency`                                          | `currency`   | Defaults to `USD`. Required whenever `amount` is present     |
| `properties.products[]`                                        | `contents`   | Omitted for `customer_action` events, which do not accept it |
| `properties.subscription_id` → `properties.plan_id`            | `plan_id`    | `subscription_created` only                                  |

Each entry in `contents` is built from a product:

| Chord source (priority order)                       | OpenAI field   | Notes |
| --------------------------------------------------- | -------------- | ----- |
| `properties.products[].product_id` → `.sku` → `.id` | `id`           |       |
| `properties.products[].name`                        | `name`         |       |
| `properties.products[].category`                    | `content_type` |       |
| `properties.products[].quantity` (defaults to `1`)  | `quantity`     |       |
| `properties.products[].price`, in minor units       | `amount`       |       |
| The order currency                                  | `currency`     |       |

### Amounts are sent in minor units

OpenAI expects every monetary amount as an **integer in the currency's minor unit**, so `$129.99` is sent as `12999`. Chord converts this for you.

Zero-decimal currencies are handled correctly: `1000 JPY` is sent as `1000`, not `100000`. This matters because OpenAI accepts either value without complaint — a mistake here would silently report every conversion 100 times too high.

## Source URL

Every event this destination sends is a web event, and OpenAI requires a `source_url` on all of them. An event without one is rejected outright with a 400 and the error code `source_url_required_for_web` — the conversion is lost, not merely reported without a URL.

Browser events carry the page URL automatically. **Server-side (back-end) events do not** — a Shopify `Order Completed` sent from your backend has no page context at all. Set **Storefront URL** so those events have a URL to fall back on.

The value is used only when the event carries no URL of its own, so setting it never overrides a real page URL.

{% hint style="warning" %}
**If you have a back-end connection and no Storefront URL configured, every event on that connection will fail.** This is the most common cause of a connection showing delivery errors on every event while the browser connection succeeds.
{% endhint %}

## Conversion matching

Visitor identifiers are SHA-256 hashed before being sent. Plaintext PII is never transmitted.

| Chord source (priority order)                         | OpenAI field           | Notes                                                                                                                    |
| ----------------------------------------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `context.traits.email` → `properties.email`           | `emails_sha256`        | Normalization: Trimmed and lowercased, then hashed                                                                       |
| `context.traits.phone` → `properties.phone`           | `phone_numbers_sha256` | Normalization: Country code kept; whitespace, punctuation, a leading `+` and leading zeroes removed. Must be 8–15 digits |
| `userId`                                              | `external_ids_sha256`  | Normalization: Trimmed, **case preserved**, then hashed                                                                  |
| `context.traits.first_name` → `properties.first_name` | `first_names_sha256`   | Normalization: Lowercased, whitespace and ASCII punctuation removed, accents preserved                                   |
| `context.traits.last_name` → `properties.last_name`   | `last_names_sha256`    | Normalization: Same as first name                                                                                        |
| `context.traits.address.city`                         | `cities`               | Normalization: Sent unhashed                                                                                             |
| `context.traits.address.state`                        | `regions`              | Normalization: Sent unhashed                                                                                             |
| `context.traits.address.zipcode`                      | `postal_codes`         | Normalization: Sent unhashed                                                                                             |
| `context.traits.address.country`                      | `countries`            | Normalization: Sent unhashed                                                                                             |
| The `__obref` cookie set by the OpenAI Ads Pixel      | `obref`                | Normalization: Sent unhashed                                                                                             |
| `requestIp` → `context.ip`                            | `ip_address`           | Normalization: Sent unhashed                                                                                             |
| `context.userAgent`                                   | `user_agent`           | Normalization: Sent unhashed                                                                                             |

A value that cannot be used is omitted rather than hashed — placeholders such as `n/a` or `unknown`, and phone numbers too short to be real, are dropped, since hashing them would send a digest that matches nobody. A value that is already a SHA-256 hash is passed through rather than hashed twice.

These normalization rules match the **OpenAI Ads Pixel** destination exactly, so the same person produces the same digest on both surfaces. Note that the Conversions API's field names are **plural lists** (`emails_sha256`) while the pixel's are singular (`email_sha256`) — both are correct for their own surface.

Chord sends one value per identifier. OpenAI accepts up to three per list and ignores the rest.

Android advertising IDs are forwarded as `android_advertising_id` on Android traffic only. OpenAI has no equivalent field for an iOS advertising ID, so one is omitted rather than sent in the wrong place.

## Attribution

OpenAI appends an `oppref` query parameter to ad click-through URLs, and OpenAI's documentation is explicit that — unlike the pixel — **the Conversions API does not capture it for you**.

Chord handles this: the browser library captures `oppref` from the landing page URL, along with the `__oppref` and `__obref` cookies the OpenAI Ads Pixel sets, and carries them on every subsequent event including the purchase. Installing the **OpenAI Ads Pixel** destination alongside this one is therefore the recommended setup — without a source of `oppref`, server-side conversions cannot be attributed to the ad that produced them.

If your storefront resolves the click identifier itself, send it as `properties.oppref` and it will be used in preference.

## Timestamps

OpenAI rejects an event timestamped more than 7 days in the past or more than 10 minutes in the future. If an event carries no parseable timestamp, Chord falls back to the time it was received and then to the current time — an approximate time is better than a rejected batch, which would lose the event entirely.

## Retries

Rate limits (`429`) and OpenAI server errors (`5xx`) are retried. A rejected event — a validation failure such as a malformed field — is not retried, because the same payload would be rejected again; it is logged instead so it appears in the function logs in Chord.

## Property mappings

OpenAI validates each event against a closed schema: the conversion data accepts only `type`, `amount`, `currency`, `contents` and `plan_id`, and there is no custom-data field. An unrecognized field causes OpenAI to reject the event, so Chord forwards no custom properties to this destination.

Mappings that change **where a supported field is read from** work normally — that is what the table below is for. If you need to segment on a property OpenAI has no field for, register a custom event in Ads Manager and route to it with **Custom Event Mapping** instead.

Use custom property mappings if the default field resolution does not match your data model:

| Destination path                    | Description                                             |
| ----------------------------------- | ------------------------------------------------------- |
| `properties.email`                  | Email for conversion matching                           |
| `properties.phone`                  | Phone for conversion matching                           |
| `properties.first_name`             | First name for conversion matching                      |
| `properties.last_name`              | Last name for conversion matching                       |
| `properties.total`                  | Primary conversion value                                |
| `properties.revenue`                | Fallback conversion value                               |
| `properties.currency`               | Currency code (e.g. `USD`)                              |
| `properties.oppref`                 | OpenAI click identifier, if your storefront resolves it |
| `properties.subscription_id`        | Plan ID for subscription events                         |
| `properties.products.[].product_id` | Product content IDs                                     |
| `properties.products.[].price`      | Per-item price                                          |
| `properties.products.[].quantity`   | Per-item quantity                                       |
| `properties.products.[].name`       | Product display name                                    |
| `properties.products.[].category`   | Product content type                                    |

## Troubleshooting

**No events appear in Ads Manager**

* Confirm the **API Key** is the **Conversion Key** generated under **Ads Manager > Conversions > Manage conversion keys**, not the API key from **Settings** and not a platform key from platform.openai.com. Only the Conversion Key carries the `ads.third_party_events.write` scope; the others authenticate successfully and then fail with a 401 reading `Missing scopes: ads.third_party_events.write`.
* Confirm the **Pixel ID** and the key belong to the same ad account.
* Confirm **Validate Only** is switched off. While it is on, OpenAI validates events and records nothing.
* Check the function logs in Chord for the response OpenAI returned.

**Conversions are attributed to no campaign**

* Confirm the **OpenAI Ads Pixel** destination is installed on the storefront, since it is what captures `oppref`.
* Confirm the `__oppref` cookie is being set and is not blocked by a consent tool.

**A conversion is counted twice**

* Confirm the **OpenAI Ads Pixel** destination uses the **same Pixel ID**.
* Confirm any events you send to OpenAI outside Chord reuse the Chord `messageId` as their event ID.

**Every event on one connection fails with a 400**

* If the response reads `Missing required field for web events: events[0].source_url`, set **Storefront URL** on the destination. Back-end connections carry no page URL of their own. See **Source URL** above.

**An event is missing entirely**

* Check the function logs in Chord for a "Skipping unsupported event" warning. OpenAI accepts only the fixed event set above; anything else needs a registered custom event and a **Custom Event Mapping** entry.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.chord.co/activate/event-streaming/destinations/advertising/openai-ads/openai-ads-conversions-api.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
