# ChatGPT Ads

Run chat card campaigns in ChatGPT from an openaiads connection, connected with an Ads Manager API key, and measure them with the pixel and server-side conversions.

ChatGPT Ads shows a chat card (a title, a short body, an image and a link) inside ChatGPT. You connect it with an API key instead of OAuth, create a campaign in one request, and measure it with a pixel on your site plus server-side conversions. The connection platform is `openaiads` and the ad endpoints call the network `openai`.

| | |
| --- | --- |
| Connection platform | `openaiads` |
| `platform` on ad endpoints | `openai` |
| Structure | Campaign, ad group, chat card ad, all created by one call |
| Goals | `traffic`, `awareness`, `conversions` |
| Title | `headline`, 3 to 50 characters, required |
| Body | Up to 100 characters, required |
| Creative | A static image (`imageUrl`) and a link (`linkUrl`), both required |
| Budget | `daily` or `lifetime`, at least 1 in the ad account currency |
| Targeting | Country only |
| Changes after launch | Status and budget |
| Measurement | Pixel plus Conversions API |
| Dry run | `validateOnly: true` |

Only businesses based in the US can use ChatGPT Ads Manager. The ads are shown to people on the ChatGPT Free and Go plans in the United States, Canada, Australia and New Zealand.

## Connect with an API key

Create a key in ChatGPT Ads Manager under **Settings**. A key belongs to exactly one ad account, so connect each ad account with its own key.

> **The key can write:**
> 
> Ads Manager has no read-only key. The key you paste can create and change campaigns, and Pozy uses it to read performance and to manage the campaigns you create through Pozy. Campaigns built in Ads Manager stay editable there.

```bash cURL
curl -X POST https://api.pozy.ai/v1/connect/openai-ads/credentials \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "apiKey": "sk-ads-...", "profileId": "66b2e19d8c3f5a7e9d0b1c2a" }'
```
```ts Node
const res = await fetch("https://api.pozy.ai/v1/connect/openai-ads/credentials", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.POZY_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ apiKey: process.env.CHATGPT_ADS_KEY!, profileId: "66b2e19d8c3f5a7e9d0b1c2a" }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
console.log(data.accountId, data.adAccountName);
```
```python Python
import os, requests

resp = requests.post(
    "https://api.pozy.ai/v1/connect/openai-ads/credentials",
    headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
    json={"apiKey": os.environ["CHATGPT_ADS_KEY"], "profileId": "66b2e19d8c3f5a7e9d0b1c2a"},
)
print(resp.json())
```

```json
{ "accountId": "66b2e19d8c3f5a7e9d0b1c2d", "adAccountName": "Acme US" }
```

Before saving, Pozy tests the key on the ad account. Sending a new key for an ad account you already connected updates that connection rather than adding another. The dashboard can connect it too.

| Response | Meaning |
| --- | --- |
| `401 invalid_credentials` | The key cannot read an ad account. Create a new one in Ads Manager. |
| `402 payment_required` | The workspace is past its free connected accounts with no payment method. `reason` is `free_tier_exceeded` and `dashboard_url` is where to add one. |

## Create a campaign

`POST /v1/ads/create` builds the campaign, ad group and chat card. This example runs a lifetime budget with a fixed bid until the end date:

```bash cURL
curl -X POST https://api.pozy.ai/v1/ads/create \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: planner-launch-chatgpt" \
  -d '{
    "accountId": "66b2e19d8c3f5a7e9d0b1c2d",
    "adAccountId": "acct_abc123",
    "name": "Planner launch",
    "goal": "traffic",
    "budgetAmount": 25,
    "budgetType": "lifetime",
    "endDate": "2027-02-28T23:59:00Z",
    "bidStrategy": "LOWEST_COST_WITH_BID_CAP",
    "bidAmount": 2,
    "headline": "Try the new planner",
    "body": "Tasks, docs and meetings in one place.",
    "imageUrl": "https://cdn.example.com/planner.png",
    "linkUrl": "https://example.com/planner",
    "countries": ["US"]
  }'
```
```ts Node
const res = await fetch("https://api.pozy.ai/v1/ads/create", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.POZY_API_KEY}`,
    "Idempotency-Key": "planner-launch-chatgpt",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    accountId: "66b2e19d8c3f5a7e9d0b1c2d",
    adAccountId: "acct_abc123",
    name: "Planner launch",
    goal: "traffic",
    budgetAmount: 25,
    budgetType: "lifetime",
    endDate: "2027-02-28T23:59:00Z",
    bidStrategy: "LOWEST_COST_WITH_BID_CAP",
    bidAmount: 2,
    headline: "Try the new planner",
    body: "Tasks, docs and meetings in one place.",
    imageUrl: "https://cdn.example.com/planner.png",
    linkUrl: "https://example.com/planner",
    countries: ["US"],
  }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
console.log(data.ad.status);
```
```python Python
resp = requests.post(
    "https://api.pozy.ai/v1/ads/create",
    headers={
        "Authorization": f"Bearer {os.environ['POZY_API_KEY']}",
        "Idempotency-Key": "planner-launch-chatgpt",
    },
    json={
        "accountId": "66b2e19d8c3f5a7e9d0b1c2d",
        "adAccountId": "acct_abc123",
        "name": "Planner launch",
        "goal": "traffic",
        "budgetAmount": 25,
        "budgetType": "lifetime",
        "endDate": "2027-02-28T23:59:00Z",
        "bidStrategy": "LOWEST_COST_WITH_BID_CAP",
        "bidAmount": 2,
        "headline": "Try the new planner",
        "body": "Tasks, docs and meetings in one place.",
        "imageUrl": "https://cdn.example.com/planner.png",
        "linkUrl": "https://example.com/planner",
        "countries": ["US"],
    },
)
print(resp.json()["ad"]["status"])
```

```json
{
  "ad": {
    "_id": "66f0a1b2c3d4e5f6a7b8c9d0",
    "name": "Planner launch",
    "platform": "openai",
    "status": "pending_review",
    "adType": "standalone",
    "goal": "traffic",
    "budget": { "amount": 25, "type": "lifetime" }
  },
  "message": "Ad created"
}
```

`budgetAmount` and `bidAmount` are whole units of the ad account currency; the ad's `currency` field names it.

### Budget

A ChatGPT campaign has exactly one spend cap:

| `budgetType` | Behavior |
| --- | --- |
| `daily` | A daily cap. Required for automatic bidding. |
| `lifetime` | A total cap. Send `endDate` to set the window it is spent over. |

The minimum is 1 in the ad account currency (`422` below that). Some currencies have a higher daily minimum, which is only checked on a real create; the error names the amount.

### Bidding

| `bidStrategy` | Bidding | Needs |
| --- | --- | --- |
| Omitted or `LOWEST_COST_WITHOUT_CAP` | Automatic: most clicks on `traffic`, most conversions on `conversions` | `budgetType: "daily"` |
| `LOWEST_COST_WITH_BID_CAP` or `COST_CAP` | A fixed bid (the cost per action under `conversions`) | `bidAmount` |

Automatic bidding on a lifetime budget or on `awareness` returns `400 missing_required_field` with `param: "bidStrategy"`; send a fixed bid there. There is no ROAS bidding, so `LOWEST_COST_WITH_MIN_ROAS` returns `422`.

### Status at launch

With `status: "PAUSED"`, only the campaign is paused and its ad group and ad are left on, so a single `PUT /v1/ads/campaigns/{campaignId}/status` with `active` starts it all. Add `campaignStatus: "ACTIVE"` to flip that around: the campaign runs and the ad group holds the pause. Send neither and the ad starts as soon as it passes review.

### Targeting

Countries are the only targeting. Ages, genders, interests and audiences return `400` instead of being ignored. Custom audiences are managed in Ads Manager only.

## Optimize for a conversion

`goal: "conversions"` needs an optimizable conversion event on the ad account before you create the campaign. Create one with the pixel (`defaultEventType`, below) or in Ads Manager.

Pick the event with `promotedObject.customEventType`. Read the candidates from `conversionEvents[]` on `GET /v1/accounts/{accountId}/conversion-destinations`:

```json
{
  "conversionEvents": [
    { "id": "evs_123", "name": "Purchase", "eventType": "order_created", "customEventName": null, "archived": false, "optimizable": true }
  ]
}
```

Pass the event's `id`, its `eventType` (such as `order_created`) or its `name` (such as `Purchase`):

```json
{ "goal": "conversions", "promotedObject": { "customEventType": "order_created" } }
```

- Only `optimizable` events qualify: active, standard events, not custom ones.
- If the event is unknown, archived or custom, you get `400` with the events the account can optimize for. If several events share the name, the `400` asks you to send the `id`.
- Without `customEventType`, the campaign optimizes for the most recently created optimizable event.
- `customEventType` is the only `promotedObject` field ChatGPT Ads accepts, and only with `conversions`.

## Click tracking

Every click adds `tracking.urlTags` to the end of `linkUrl`. Pozy leaves ChatGPT's macros `{campaign_id}`, `{ad_group_id}`, `{ad_id}` and `{oppref}` (the click id) unescaped there, so ChatGPT fills them in on the click. Macros written into `linkUrl` itself stay as literal text.

```json
{
  "tracking": {
    "urlTags": [
      { "key": "utm_source", "value": "chatgpt" },
      { "key": "utm_campaign", "value": "{campaign_id}" },
      { "key": "click_id", "value": "{oppref}" }
    ]
  }
}
```

## Check before you spend

Send `validateOnly: true` to test a request without creating anything. Pozy checks copy lengths, budget, bid strategy and targeting, and looks up the conversion event and countries live.

```json
{
  "validateOnly": true,
  "results": [
    { "node": "campaign", "status": "validated" },
    { "node": "adSet", "status": "validated" },
    { "node": "ad", "status": "validated" }
  ],
  "message": "..."
}
```

Checks that only run at write time, such as fetching the image, currency minimums and ad review, still happen on the real create. `POST /v1/ads/campaigns` also takes `validateOnly`.

## Manage a running campaign

| Change | Call | Notes |
| --- | --- | --- |
| Pause or resume a campaign | `PUT /v1/ads/campaigns/{campaignId}/status` with `{ "status": "active", "platform": "openai" }` | Pozy reads the switch first; if it already matches, nothing is written (`updated: 0, skipped: 1`). |
| Pause or resume one ad | `PUT /v1/ads/{adId}/status` | |
| Change the budget | `budget: { amount, type }` on `PUT /v1/ads/{adId}` or `PUT /v1/ads/campaigns/{campaignId}` | Overwrites the one budget cap. A lifetime budget may become daily, but a daily one cannot become lifetime (`422`). |
| Rename an ad group or change its bid | `PUT /v1/ads/ad-sets/{adSetId}` | |
| Create a campaign on its own | `POST /v1/ads/campaigns` | Needs a budget (`422` without one). Takes `goal` and `promotedObject.customEventType`. |
| Stop | `DELETE /v1/ads/{adId}` | There is no delete on ChatGPT Ads. Pozy archives the ad, its ad group and its campaign, and marks the ad `cancelled`. Archiving is final. |

Targeting and creative cannot change after launch (`501 feature_not_available`). There is no boost, because ChatGPT has no organic posts, and no video.

`GET /v1/ads/tree` reports two kinds of status on ChatGPT Ads: `status` (is it delivering, inherited from parents) and the levels' own switches (`platformCampaignStatus` on campaigns, `configuredStatus` on ads, each `active`/`ACTIVE`, `paused` or `archived`). Objects archived in Ads Manager settle as archived in Pozy.

## Measure results

Pozy syncs impressions, clicks and spend once a day and derives CTR, CPC and CPM. `GET /v1/ads/{adId}/analytics` defaults to the last 90 days. Every other metric, `reach` included, reads `0`, and demographic breakdowns are not available. History starts 90 days before the connection.

### The pixel

The pixel measures what people do on your site after a chat card. Manage it through the tracking tags endpoints on the `openaiads` connection.

```bash
curl -X POST https://api.pozy.ai/v1/accounts/66b2e19d8c3f5a7e9d0b1c2d/tracking-tags \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "adAccountId": "acct_abc123", "name": "Acme website", "defaultEventType": "order_created" }'
```

- One call creates the pixel and a Conversions API key for it. `defaultEventType` also creates the conversion event a `conversions` campaign needs.
- `adAccountId` is required by the endpoint but the API key already decides the ad account.
- Each pixel has two ids, and per-pixel calls accept either: `tag.id` (`cds_...`) and `tag.siteTagId` (also `pixelId`), the id the site code and the Conversions API send.
- Pozy creates one pixel per connection; a second create returns `409`. Creating is not idempotent and pixels cannot be deleted, so never retry it automatically. After a `502`, list the pixels first.
- `GET .../tracking-tags/{tagId}/stats` shows the latest events received, up to 50 from the last 15 minutes. Use it to check the pixel fires, not for totals.

Conversion events take `name`, `type` and `siteEvent`, with ChatGPT's fixed 30-day window. `type` is a standard event (`order_created`, `lead_created`, `items_added`, `contents_viewed`, `checkout_started`, `registration_completed`, `subscription_created`, `trial_started`, `appointment_scheduled`, `page_viewed`, `app_installed`, `app_opened`) or a custom name of 1 to 64 letters, digits, `_` or `-`. To edit or remove an event, or the pixel, use Ads Manager: the API cannot.

Install the pixel on a connected Shopify, WordPress, Wix or Webflow site with one call. See [Pixels](https://pozy.ai/docs/ads/pixels).

### Conversions API

Server-side events go to `POST /v1/ads/conversions` with the pixel's site id as `destinationId`. A pixel must exist first (`422 tracking_tag_required` otherwise).

```bash
curl -X POST https://api.pozy.ai/v1/ads/conversions \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "accountId": "66b2e19d8c3f5a7e9d0b1c2d",
    "destinationId": "4Kx9TqLm2VbN7wRz1PsYcD",
    "events": [{
      "eventName": "Purchase",
      "eventTime": 1798804800,
      "eventId": "order_12345",
      "value": 25.99,
      "currency": "USD",
      "user": { "email": "customer@example.com" }
    }]
  }'
```

`Purchase`, `Lead`, `AddToCart`, `ViewContent`, `InitiateCheckout`, `CompleteRegistration`, `Subscribe`, `StartTrial` and `Schedule` map to ChatGPT's own event types (`Purchase` arrives as `order_created`). Any other name is sent as a custom event. Events go in chunks of 1,000, and one malformed event fails its chunk. See [Conversions](https://pozy.ai/docs/ads/conversions).

## Limits

| Limit | Value |
| --- | --- |
| Title | 3 to 50 characters |
| Body | Up to 100 characters |
| Budget | At least 1 in the ad account currency |
| Events per conversions request | 1,000 per chunk |
| ChatGPT Ads rate limits | 600 requests per minute per endpoint, 1,200 per minute overall |

## Errors you will see

| Status and code | Cause | Fix |
| --- | --- | --- |
| `401 invalid_credentials` | The key cannot read an ad account | Create a new key in Ads Manager. |
| `400` on a `conversions` create | No optimizable event, or an unknown, archived, custom or ambiguous `customEventType` | Create the event, or pass its `id`. |
| `400 missing_required_field` (`bidStrategy`) | Automatic bidding combined with a lifetime budget or the `awareness` goal | Switch to a daily budget, or set a fixed bid. |
| `400` on a targeting field | Targeting other than `countries` | Leave it out. |
| `422` with `param: "budget.amount"` | Budget below 1 | Raise it. |
| `422` with `param: "budget.type"` | A lifetime budget on a campaign with a daily cap | Keep daily, or create a new campaign. |
| `422 tracking_tag_required` | Conversions sent before a pixel exists | Create the pixel. |
| `422 feature_not_available` | Pixel management is not switched on for the ad account | Ask your ChatGPT Ads contact to turn it on. |
| `501 feature_not_available` | Targeting or creative edits after launch | Create a new ad. |

## Endpoints

| Endpoint | What it does |
| --- | --- |
| [`POST /v1/connect/openai-ads/credentials`](https://pozy.ai/docs/api/connectOpenAIAdsCredentials) | Connect an OpenAI Ads account |
| [`GET /v1/ads/accounts`](https://pozy.ai/docs/api/listAdAccounts) | List ad accounts |
| [`POST /v1/ads/create`](https://pozy.ai/docs/api/createStandaloneAd) | Create standalone ad |
| [`POST /v1/ads/campaigns`](https://pozy.ai/docs/api/createAdCampaign) | Create a standalone campaign |
| [`GET /v1/ads/tree`](https://pozy.ai/docs/api/getAdTree) | Get campaign tree |
| [`GET /v1/ads/{adId}`](https://pozy.ai/docs/api/getAd) | Get ad details |
| [`PUT /v1/ads/{adId}`](https://pozy.ai/docs/api/updateAd) | Update ad |
| [`PUT /v1/ads/{adId}/status`](https://pozy.ai/docs/api/updateAdStatus) | Pause or resume a single ad |
| [`PUT /v1/ads/ad-sets/{adSetId}`](https://pozy.ai/docs/api/updateAdSet) | Update an ad set |
| [`PUT /v1/ads/campaigns/{campaignId}`](https://pozy.ai/docs/api/updateAdCampaign) | Update a campaign |
| [`PUT /v1/ads/campaigns/{campaignId}/status`](https://pozy.ai/docs/api/updateAdCampaignStatus) | Pause or resume a campaign |
| [`DELETE /v1/ads/{adId}`](https://pozy.ai/docs/api/deleteAd) | Cancel an ad |
| [`GET /v1/ads/{adId}/analytics`](https://pozy.ai/docs/api/getAdAnalytics) | Get ad analytics |
| [`GET /v1/accounts/{accountId}/tracking-tags`](https://pozy.ai/docs/api/listTrackingTags) | List tracking tags |
| [`POST /v1/accounts/{accountId}/tracking-tags`](https://pozy.ai/docs/api/createTrackingTag) | Create a tracking tag |
| [`GET /v1/accounts/{accountId}/tracking-tags/{tagId}`](https://pozy.ai/docs/api/getTrackingTag) | Get a tracking tag |
| [`GET /v1/accounts/{accountId}/tracking-tags/{tagId}/stats`](https://pozy.ai/docs/api/getTrackingTagStats) | Get aggregated event stats |
| [`POST /v1/accounts/{accountId}/tracking-tags/{tagId}/events`](https://pozy.ai/docs/api/createTrackingTagEvent) | Create a conversion event |
| [`POST /v1/accounts/{accountId}/tracking-tags/{tagId}/install`](https://pozy.ai/docs/api/installTrackingTagOnStore) | Install on a Shopify, WordPress, Wix or Webflow site |
| [`GET /v1/accounts/{accountId}/conversion-destinations`](https://pozy.ai/docs/api/listConversionDestinations) | List conversion destinations |
| [`POST /v1/ads/conversions`](https://pozy.ai/docs/api/sendConversions) | Send conversion events |

## Webhook events

| Event | When it fires |
| --- | --- |
| [`account.ads.initial_sync_completed`](https://pozy.ai/docs/webhooks/events#account-ads-initial-sync-completed) | Ads initial sync completed. |
| [`account.ads.sync_failed`](https://pozy.ai/docs/webhooks/events#account-ads-sync-failed) | Ads sync failed. |
| [`account.ads.sync_recovered`](https://pozy.ai/docs/webhooks/events#account-ads-sync-recovered) | Ads sync recovered. |
| [`ad.review.approved`](https://pozy.ai/docs/webhooks/events#ad-review-approved) | Ad review approved. |
| [`ad.review.rejected`](https://pozy.ai/docs/webhooks/events#ad-review-rejected) | Ad review rejected. |
