Ads

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.

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" }'
Response
{ "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:

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"]
  }'
Response (201)
{
  "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:

A conversion destination
{
  "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.

Response (200)
{
  "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.

Create the pixel
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.

#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).

cURL
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.

#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 Connect an OpenAI Ads account
GET /v1/ads/accounts List ad accounts
POST /v1/ads/create Create standalone ad
POST /v1/ads/campaigns Create a standalone campaign
GET /v1/ads/tree Get campaign tree
GET /v1/ads/{adId} Get ad details
PUT /v1/ads/{adId} Update ad
PUT /v1/ads/{adId}/status Pause or resume a single ad
PUT /v1/ads/ad-sets/{adSetId} Update an ad set
PUT /v1/ads/campaigns/{campaignId} Update a campaign
PUT /v1/ads/campaigns/{campaignId}/status Pause or resume a campaign
DELETE /v1/ads/{adId} Cancel an ad
GET /v1/ads/{adId}/analytics Get ad analytics
GET /v1/accounts/{accountId}/tracking-tags List tracking tags
POST /v1/accounts/{accountId}/tracking-tags Create a tracking tag
GET /v1/accounts/{accountId}/tracking-tags/{tagId} Get a tracking tag
GET /v1/accounts/{accountId}/tracking-tags/{tagId}/stats Get aggregated event stats
POST /v1/accounts/{accountId}/tracking-tags/{tagId}/events Create a conversion event
POST /v1/accounts/{accountId}/tracking-tags/{tagId}/install Install on a Shopify, WordPress, Wix or Webflow site
GET /v1/accounts/{accountId}/conversion-destinations List conversion destinations
POST /v1/ads/conversions Send conversion events

#Webhook events

Event When it fires
account.ads.initial_sync_completed Ads initial sync completed.
account.ads.sync_failed Ads sync failed.
account.ads.sync_recovered Ads sync recovered.
ad.review.approved Ad review approved.
ad.review.rejected Ad review rejected.