# Ads — full documentation > Single-file Markdown export of the Ads docs for creating ads from static creatives or product feeds in ChatGPT and measuring website conversions. Curated index: https://developers.openai.com/ads/llms.txt # Account Management ## Account Details & Branding An ad account owns campaigns, creative assets, feeds, audiences, and conversion sources. Its brand name and advertiser icon identify the advertiser in ads. ### Retrieve account details ```bash curl -G "https://api.ads.openai.com/v1/ad_account" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" ``` Check your advertiser ID, name, URL, currency, timezone, configured status, and returned reviews. Currency and timezone are creation-time choices and cannot be edited through the branding update. ### Upload an advertiser icon ```bash curl -X POST "https://api.ads.openai.com/v1/upload" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" \ -F "file=@/path/to/favicon.png" \ -F "purpose=account_favicon" ``` Use JPEG, PNG, or WebP with dimensions of at least 256 × 256 pixels. Save the returned `file_id`. ### Apply branding ```bash curl -X POST "https://api.ads.openai.com/v1/ad_account/brand" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" \ -H "Content-Type: application/json" \ -d '{ "name": "Acme", "url": "https://example.com", "favicon_file_id": "file_123" }' ``` Replace the sample URL with your actual website. The current update schema makes these fields optional, so send only the branding fields you intend to change. ### Check review after updating Retrieve the account again. Brand changes can trigger review. The returned `preview_url` is a preview of the advertiser icon, not a permanent asset URL. Do not store it as your application's durable source of truth for the branding file. ## Account Statuses & Reviews An account's configured status and its reviews are separate. Check both when onboarding or investigating account-wide delivery problems. ### Retrieve current state ```bash curl -G "https://api.ads.openai.com/v1/ad_account" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" ``` | Field | What it tells you | | ---------------------------------------- | ---------------------------------------------------------- | | `status` | Whether the account is configured as active or paused | | `review.status` | Brand-review state | | `review.reason` | Returned reason for a brand-review issue, when available | | `account_integrity_review.review.status` | Separate account-review state, when the object is returned | The response can omit account-integrity review information when no corresponding state is available. Do not interpret an omitted object as an explicit approval or rejection. ### Approved branding is one requirement An active account with approved branding can still have another outstanding account review, an exhausted spend limit, or campaign-level delivery issues. Inspect the returned account-review fields. ## Activating & Pausing Pause your account to stop delivery across its campaigns. Activate it to allow eligible campaigns to deliver again. Use account controls when the change should apply to the entire account. For a narrower change, pause the relevant campaign, ad group, or ad instead. ### Pause the account ```bash curl -X POST "https://api.ads.openai.com/v1/ad_account/pause" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" ``` Confirm that the returned account status is `paused`. ### Activate the account ```bash curl -X POST "https://api.ads.openai.com/v1/ad_account/activate" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" ``` Confirm the returned status and inspect the account's reviews. Activation permits delivery only when the other conditions are satisfied. ### What activation does not establish Account activation does not mean that every campaign is active, every ad is approved, or every budget has available capacity. Check child resource statuses, schedules, reviews, and spending controls before expecting traffic. Similarly, do not infer that child resource statuses were rewritten after an account pause. Retrieve the campaign, ad group, or ad if your application needs its current configured state. ## Spend Limits An account spend-limit window caps total spending across the account's campaigns during a date range. Campaign budgets continue to apply independently. Spend-limit windows are available only to some accounts. ### Create a window ```bash curl -X POST "https://api.ads.openai.com/v1/ad_account/spend_limit_windows" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" \ -H "Content-Type: application/json" \ -d '{ "start_date": "2026-10-01", "end_date": "2026-11-01", "amount_micros": 10000000000, "name": "October account limit", "io_id": "IO-123" }' ``` The example is 10,000 currency units. Choose your intended amount and dates before submitting it. `start_date` is inclusive and `end_date` is exclusive, interpreted in the ad account's timezone. Save the returned `window_id`. The response also describes whether the window can be edited or deleted. ### List windows ```bash curl -G "https://api.ads.openai.com/v1/ad_account/spend_limit_windows" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" ``` Inspect active and scheduled windows before creating another one. Windows cannot overlap, and you can schedule up to 60 future windows. ### Update a window ```bash curl -X POST "https://api.ads.openai.com/v1/ad_account/spend_limit_windows/slw_123" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" \ -H "Content-Type: application/json" \ -d '{ "amount_micros": 12000000000 }' ``` Send only the fields you intend to change. An active window's start date cannot be changed. The amount cannot be reduced below spending that has already occurred. Completed windows cannot be edited or deleted. ### Delete a window ```bash curl -X POST "https://api.ads.openai.com/v1/ad_account/spend_limit_windows/slw_123/delete" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" ``` Deleting or increasing an exhausted active limit can allow delivery to resume if no other requirement blocks it. Review the intended account-wide spending effect before making that change. ### Account limits versus campaign budgets An account limit does not allocate a budget to each campaign. A campaign can have budget remaining while the account's active limit is exhausted. Conversely, removing an account limit does not remove the campaigns' own budgets. The example endpoints manage date-window limits. If your integration also uses another account spending control, inspect it separately; do not assume that deleting a window removes every account-level control. --- # Overview Use the Advertiser API to create and manage campaigns, ad groups, ads, product feeds, conversion tracking, and reporting in your own application or for automating workflows. ## Getting Started You will need an [ad account](https://ads.openai.com/) and an Advertiser API key. You can create your Advertiser API key within the [Settings page](https://ads.openai.com/settings) in Ads Manager. Store API keys securely on your server. Examples throughout this guide will use a placeholder for the Advertiser API key and sample IDs such as `cmpn_123`, `adgrp_123`, and `ad_123` in requests. Replace these placeholders with real values returned by your requests. API partners can follow [API Partner Setup](https://developers.openai.com/ads/api-partner-setup). For another end-to-end example, see the [Quickstart](https://developers.openai.com/ads/api-quickstart). ### Request conventions Use the following base URL for API requests. Provide your API key in the Authorization header when making requests. | Convention | What to use | | -------------- | ---------------------------- | | Base URL | `https://api.ads.openai.com` | | Authentication | `Authorization: Bearer …` | On supported create endpoints, you may send an `Idempotency-Key`. Reuse that key and the same request when retrying the same creation; use a new key for a new resource. This prevents a network retry from creating a duplicate. ### Verify your API key Retrieve your ad account to verify the key: ```bash curl -G "https://api.ads.openai.com/v1/ad_account" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" ``` Confirm that the response contains information about your account. Inspect the currency, timezone, account status, and reviews before creating a campaign. An account marked `active` may still have an outstanding review. ### Next steps Read through the [Campaign Structure](#campaign-structure) section and then follow the steps in [Your First Campaign](#your-first-campaign). ## Campaign Structure An ad account contains campaigns. Each campaign contains ad groups, and each ad group contains ads. Configure each setting at the level that owns it. Use separate campaigns when you need separate budgets, objectives, or targeting. Use ad groups for different bid configurations, context hints, or product selections. Keep related creative variations together where those settings are shared. ```text Ad account └── Campaign └── Ad group └── Ad ``` | Level | What you configure | Example | | ---------- | ------------------------------------------------------------------------------------------------------------------- | ----------------------------- | | Ad account | Advertiser branding, currency, timezone, account access, spend limits | Acme's US advertising account | | Campaign | Objective, budget, schedule, geographic and platform targeting, audience inclusion and exclusion, conversion events | US spring sales | | Ad group | Bid strategy, context hints, audience bid multipliers, product set | Trail running products | | Ad | Creative, image or product template, destination URL | A running-shoe ad | ## Your First Campaign Let's create a paused campaign, an ad group, and an ad. This example creates a clicks campaign with a fixed bid. The budget and bid are illustrative USD amounts. Use values appropriate to your account's currency and your advertising plan. ### 1. Create the campaign ```bash curl -X POST "https://api.ads.openai.com/v1/campaigns" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" \ -H "Idempotency-Key: first-campaign-001" \ -H "Content-Type: application/json" \ -d '{ "name": "Spring launch", "status": "paused", "bidding_type": "clicks", "budget": { "daily_spend_limit_micros": 50000000 }, "targeting": { "locations": { "countries": [ "US" ] } } }' ``` Save the returned `id`. Use it in place of `cmpn_123` below. The example daily budget is $50 for a USD account. ### 2. Create the ad group ```bash curl -X POST "https://api.ads.openai.com/v1/ad_groups" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" \ -H "Idempotency-Key: first-ad-group-001" \ -H "Content-Type: application/json" \ -d '{ "campaign_id": "cmpn_123", "name": "Trail running", "status": "paused", "context_hints": [ "Trail running shoes for rocky terrain" ], "bidding_config": { "billing_event_type": "click", "strategy": "fixed_bid", "max_bid_micros": 2000000 } }' ``` Save the returned ad-group ID. The example maximum bid is $2 per click for a USD account. ### 3. Upload a creative image ```bash curl -X POST "https://api.ads.openai.com/v1/upload" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" \ -F "file=@/path/to/product-image.png" ``` Use an image at least 640 × 640 pixels. Save the returned `file_id`. You'll use this image in the ad. ### 4. Create the ad ```bash curl -X POST "https://api.ads.openai.com/v1/ads" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" \ -H "Idempotency-Key: first-ad-001" \ -H "Content-Type: application/json" \ -d '{ "ad_group_id": "adgrp_123", "name": "Trail shoe launch", "status": "paused", "creative": { "type": "chat_card", "title": "Find your next trail shoe", "body": "Explore shoes made for your next outdoor run.", "target_url": "https://example.com/trail-shoes", "file_id": "file_123" } }' ``` Replace the `target_url` with your real, accessible landing page. Save the returned ad ID. Creating an ad submits its creative for review. ### 5. Preview and inspect Check the creative, destination, review status, account reviews, targeting, and budget. A preview shows appearance; it does not confirm serving eligibility. ```bash curl -X POST "https://api.ads.openai.com/v1/ads/ad_123/preview" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" ``` ```bash curl -G "https://api.ads.openai.com/v1/ads/ad_123" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" \ --data-urlencode 'include[]=serving_issues' ``` ### 6. Activate when ready ```bash curl -X POST "https://api.ads.openai.com/v1/ads/ad_123/activate" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" ``` ```bash curl -X POST "https://api.ads.openai.com/v1/ad_groups/adgrp_123/activate" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" ``` ```bash curl -X POST "https://api.ads.openai.com/v1/campaigns/cmpn_123/activate" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" ``` Activation enables delivery when the remaining requirements are satisfied. Use Insights to monitor results and Serving Issues if delivery does not begin. ## Rate limits The Advertiser API enforces limits by both ad account and IP address: | Scope | Limit | | ------------ | ------------------------- | | Per endpoint | 600 requests per minute | | Overall | 1,200 requests per minute | Requests must stay within both the ad-account and IP-address limits. Bulk job creation has a separate limit of 10 requests per 10 seconds for each ad account. See [Bulk API limits](https://developers.openai.com/ads/bulk-api#limits-and-retries). ## OpenAPI spec [{"Download the OpenAPI spec"}](https://developers.openai.com/ads/openapi.json) ## Changelog ### September 10th, 2026 - Added granular web platform targeting with `desktop_web`, `ios_web`, and `android_web` in `targeting.platforms.included`. Target desktop, iOS, and Android browsers separately, or use `web` to include all web platforms. See [Platform Targeting](https://developers.openai.com/ads/platform-targeting). Platform breakdowns in [Insights](https://developers.openai.com/ads/api-reference/insights#platform-breakdown) also separate web platforms while preserving historical Web totals. ### September 9th, 2026 - Added [daily account spending limits](https://developers.openai.com/ads/api-reference/ad-account#set-a-daily-limit) for ad accounts on postpaid invoice billing. Set a shared allowance across campaigns that renews at midnight in the account timezone, with an optional end date. Existing date range limits remain available. ### August 25th, 2026 - Added custom audience Add, Remove, Replace, and Merge operations, automatic identifier matching, and support for small and empty exclusion-only audiences. See [Custom Audiences](https://developers.openai.com/ads/custom-audiences). ### July 16th, 2026 - Added support for passing the Pixel browser reference as `events[].user.obref` in [Conversions API](https://developers.openai.com/ads/conversions-api) requests. ### June 16th, 2026 - Added conversion-optimized campaign bidding with `bidding_type: "conversions"` and one standard conversion event setting. ### June 11th, 2026 - Added segmented insights for product, country, and device breakdowns, plus zero-impression product expansion. ### June 3rd, 2026 - Added location targeting support, including `/geo_lookup/search` and campaign `targeting.locations.include` for country, region, and market location IDs. - Added conversion setup and reporting endpoints for API keys, pixels, event settings, and conversion insights. ### v1 - Published the initial API version. --- # API Partner Setup API partners can use the Ads API to configure measurement and campaign resources for client accounts. Use the API key associated with the client ad account for every request in this guide. ## Before you begin You need: - An Ads API key for the client ad account. - Access to the client's website and brand assets. - A server-side secret manager for the Ads API key and any Conversions API keys you create. Send requests to `https://api.ads.openai.com/v1`. Store the account's Ads API key in `OPENAI_ADS_API_KEY` for the examples in this guide. Brand updates, pixel management, and Conversions API key creation must be enabled for the client account. If a brand update returns `403`, or `/conversions/pixels` or `/conversions/api_keys` returns `404` with `Not found`, contact your OpenAI partner representative. A `Client data source not found` response means an event setting references a source that does not exist in the current ad account. Conversion-optimized campaigns must also be enabled for the client account. If campaign creation returns `403` with `Conversion bidding is not enabled`, contact your OpenAI partner representative. ## 1. Confirm account access Use `GET /ad_account` to verify that the key is associated with the intended client account: ```bash curl -X GET "https://api.ads.openai.com/v1/ad_account" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" ``` ```json { "id": "adacct_123", "name": "Acme Ads", "url": "https://www.acme.example", "preview_url": null, "status": "active", "timezone": "America/New_York", "currency_code": "USD", "review": { "status": "approved" } } ``` Check the account name, URL, time zone, and currency before you create campaign resources. Contact your OpenAI partner representative if the account details or API access are incorrect. ## 2. Add a brand icon An account cannot serve ads until its brand review is approved. If `review.reason` is `missing_favicon`, upload and assign a brand icon before you create active campaign resources. First, upload an image with `purpose` set to `account_favicon`. The image must be at least 128 × 128 pixels. You can pass the client's website URL and let the API resolve a suitable icon: ```bash curl -X POST "https://api.ads.openai.com/v1/upload" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "image_url": "https://www.acme.example", "purpose": "account_favicon" }' ``` Save the returned `file_id`, then assign it to the account: ```bash curl -X POST "https://api.ads.openai.com/v1/ad_account/brand" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "favicon_file_id": "file_123" }' ``` The brand update starts a review. Poll `GET /ad_account` until `review.status` is `approved`. If the status is `rejected`, use `review.reason` to correct the brand metadata before you activate a campaign. ## 3. Configure conversions Create conversion resources before you instrument the client's site. First, create a web pixel: ```bash curl -X POST "https://api.ads.openai.com/v1/conversions/pixels" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Acme website", "client_type": "web" }' ``` Save both returned identifiers. Use `id` when you create an event setting and use `pixel_id` when you send conversion events. ```json { "id": "clidsrc_123", "client_type": "web", "name": "Acme website", "pixel_id": "134534..." } ``` Web pixels created through the Ads API automatically use [automatic advanced matching](https://developers.openai.com/ads/measurement-pixel#automatic-advanced-matching). Next, create a server-side Conversions API key: ```bash curl -X POST "https://api.ads.openai.com/v1/conversions/api_keys" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Acme production conversions" }' ``` Store the returned `api_key` in a server-side secret manager. Use this key only to send conversion events; do not expose it in browser code. Finally, define the conversion event you want to measure or optimize for: ```bash curl -X POST "https://api.ads.openai.com/v1/conversions/event_settings" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Purchases", "event_type": "order_created", "attribution_window_days": 30, "source_ids": ["clidsrc_123"] }' ``` Save the returned event setting `id`. For endpoint fields and responses, see [Conversion Setup](https://developers.openai.com/ads/api-reference/conversion-setup). To implement event delivery, use the [JavaScript Pixel](https://developers.openai.com/ads/measurement-pixel), the [Conversions API](https://developers.openai.com/ads/conversions-api), or both with shared event IDs for deduplication. When you send conversion events on behalf of a client, include the same `integration_source` identifier on every request. See [Identify partner integrations](https://developers.openai.com/ads/conversions-api#identify-partner-integrations) for the request format and naming requirements. ## 4. Create campaigns and ads Follow the [Quickstart](https://developers.openai.com/ads/api-quickstart) to create a campaign, ad group, creative asset, and ad in the correct order. For partner setup, create the campaign as `paused` instead of `active`, then activate it after all child resources are ready. To create a conversion-optimized campaign (oCPC), set `bidding_type` to `conversions` and pass exactly one event setting ID. For the bidding model, prerequisites, and reporting guidance, see [Conversion-Optimized Campaigns](https://developers.openai.com/ads/conversion-optimized-campaigns). ```bash curl -X POST "https://api.ads.openai.com/v1/campaigns" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Acme purchases", "status": "paused", "budget": { "lifetime_spend_limit_micros": 250000000 }, "bidding_type": "conversions", "conversion_event_setting_ids": ["ces_123"] }' ``` The event setting must be active, belong to the current ad account, connect to one active conversion source, and use an event from [Supported Events](https://developers.openai.com/ads/supported-events), such as `order_created`, `lead_created`, or `registration_completed`. Custom events cannot be optimization goals. For product-feed campaigns in open beta, use the same campaign endpoint and add `mode: "product_feed"` and the linked `product_feed_id`. You cannot change the campaign objective or selected conversion event after creation. Next, create each ad group with `billing_event_type` set to `click`. For an oCPC campaign, `max_bid_micros` is the CPA bid; for example, `100000000` is a $100.00 CPA bid for a USD account. ```bash curl -X POST "https://api.ads.openai.com/v1/ad_groups" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "campaign_id": "cmpn_101", "name": "US English", "status": "active", "bidding_config": { "billing_event_type": "click", "max_bid_micros": 100000000 } }' ``` Create campaigns as `paused` while you add and validate their ad groups and ads. Activate the campaign only after all child resources are ready. ## Next steps - [Authentication](https://developers.openai.com/ads/api-reference/authentication) - [Ad Account](https://developers.openai.com/ads/api-reference/ad-account) - [Conversion Setup](https://developers.openai.com/ads/api-reference/conversion-setup) - [Campaigns](https://developers.openai.com/ads/api-reference/campaigns) - [Ad Groups](https://developers.openai.com/ads/api-reference/ad-groups) - [Ads](https://developers.openai.com/ads/api-reference/ads) - [Files](https://developers.openai.com/ads/api-reference/files) - [Insights](https://developers.openai.com/ads/api-reference/insights) --- # Quickstart The Ads API can programmatically create ad campaigns and monitor your results. This guide covers the minimal implementation to get an ad live, and check your results. ## Ad Structure Ads live inside an Ad Group, and Ad Groups live inside a Campaign. Campaigns and Ad Groups define your budget and targeting, while Ads host your Ad's title, description and images. ## 1. Confirm access to your ad account Issue an API key in the Settings tab of your [Ads Manager](https://ads.openai.com) account. See the [authentication reference](https://developers.openai.com/ads/api-reference/authentication) for more info. Call `GET /ad_account` to confirm that your bearer token works and that you are using the right account: ```bash curl -X GET "https://api.ads.openai.com/v1/ad_account" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" \ -H "Accept: application/json" ``` Response: ```json { "id": "adacct_123", "name": "Acme Ads", "url": "https://www.acme.example", "preview_url": null, "status": "active", "timezone": "UTC", "currency_code": "USD", "review": { "status": "approved" } } ``` ## 2. Upload a creative asset Upload a remote image and store the returned `file_id`. You'll use it when you create the ad. ```bash curl -X POST "https://api.ads.openai.com/v1/upload" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "image_url": "https://example.com/assets/workspace-planner-card.png" }' ``` Response: ```json { "file_id": "file_901" } ``` If you already have a local file, the same endpoint also accepts `multipart/form-data`. ## 3. Create a campaign Create the top-level campaign first. Save the returned campaign ID for the next step. ```bash curl -X POST "https://api.ads.openai.com/v1/campaigns" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Spring launch", "status": "active", "budget": { "lifetime_spend_limit_micros": 25000000 } }' ``` Response: ```json { "id": "cmpn_101", "created_at": 1735689600, "status": "active", "bidding_type": "impressions", "budget": { "lifetime_spend_limit_micros": 25000000 }, "conversion_event_setting_ids": [], "description": null, "end_time": null, "mode": null, "name": "Spring launch", "start_time": null, "targeting": {}, "updated_at": 1735689600 } ``` ## 4. Create an ad group Create an ad group inside the campaign. Save the returned ad group ID for the ad creation step. ```bash curl -X POST "https://api.ads.openai.com/v1/ad_groups" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "campaign_id": "cmpn_101", "name": "US English", "status": "active", "context_hints": ["productivity", "team collaboration"], "bidding_config": { "billing_event_type": "impression", "max_bid_micros": 60000 } }' ``` Response: ```json { "id": "adgrp_301", "created_at": 1735689700, "updated_at": 1735689700, "name": "US English", "description": null, "context_hints": ["productivity", "team collaboration"], "status": "active", "bidding_config": { "billing_event_type": "impression", "max_bid_micros": 60000 } } ``` ## 5. Create an ad Create the ad with a `chat_card` creative and attach the uploaded asset by `file_id`. ```bash curl -X POST "https://api.ads.openai.com/v1/ads" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "ad_group_id": "adgrp_301", "name": "Planner launch card", "status": "active", "creative": { "type": "chat_card", "title": "Try the new workspace planner", "body": "Coordinate tasks, docs, and meetings in one place.", "target_url": "https://example.com/workspace-planner", "file_id": "file_901" } }' ``` Response: ```json { "id": "ad_501", "name": "Planner launch card", "created_at": 1735689800, "updated_at": 1735689800, "creative": { "type": "chat_card", "title": "Try the new workspace planner", "body": "Coordinate tasks, docs, and meetings in one place.", "file_id": "file_901", "image_url": "https://cdn.openai.com/ads/file_901.png", "target_url": "https://example.com/workspace-planner" }, "status": "active", "review_status": "in_review" } ``` ## 6. Retrieve insights Once the ad is serving, retrieve performance data from the ad-level insights endpoint. ```bash curl -sS -G "https://api.ads.openai.com/v1/ads/ad_501/insights" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" \ --data-urlencode "time_granularity=daily" \ --data-urlencode "limit=7" ``` ```json { "object": "list", "count": 1, "data": [ { "id": "start=1775088000:end=1775174400:entity_id=ad_501", "readable_time": "2026-04-02", "timezone": "UTC", "impressions": 15548, "clicks": 312, "spend": 42.75, "start_time": 1775088000, "end_time": 1775174400 } ], "first_id": "start=1775088000:end=1775174400:entity_id=ad_501", "last_id": "start=1775088000:end=1775174400:entity_id=ad_501", "has_more": false } ``` ## Next steps Once the basic flow works, use the full reference for each part of the integration: - [Authentication](https://developers.openai.com/ads/api-reference/authentication) - [Ad Account](https://developers.openai.com/ads/api-reference/ad-account) - [Campaigns](https://developers.openai.com/ads/api-reference/campaigns) - [Ad Groups](https://developers.openai.com/ads/api-reference/ad-groups) - [Ads](https://developers.openai.com/ads/api-reference/ads) - [Campaign Targeting](https://developers.openai.com/ads/campaign-targeting) - [Product Feeds](https://developers.openai.com/ads/product-feeds) - [Insights](https://developers.openai.com/ads/api-reference/insights) - [Files](https://developers.openai.com/ads/api-reference/files) --- # Ad Account ## Update account brand metadata Set the account name or favicon and start a new brand review. At least one of `name` or `favicon_file_id` is required. This operation must be enabled for the ad account. If it returns `403`, contact your OpenAI partner representative. `POST /ad_account/brand` | Field | Type | Required | Notes | | ----------------- | ------ | -------- | --------------------------------------------------- | | `name` | string | No | Updated account display name. | | `favicon_file_id` | string | No | File ID uploaded with `purpose: "account_favicon"`. | Upload the favicon with the [file endpoint](https://developers.openai.com/ads/api-reference/files#upload-an-account-favicon), then assign it to the account: ```bash curl -X POST "https://api.ads.openai.com/v1/ad_account/brand" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "favicon_file_id": "file_123" }' ``` The response includes the updated account. Poll `GET /ad_account` until `review.status` is `approved`. An account with any other review status cannot serve ads. ## Get ad account metadata Fetch metadata for the current ad account. `GET /ad_account` This endpoint takes no request body or query parameters. ```bash curl -X GET "https://api.ads.openai.com/v1/ad_account" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" ``` ```json { "id": "adacct_123", "name": "Acme Ads", "url": "https://www.acme.example", "preview_url": null, "status": "active", "timezone": "UTC", "currency_code": "USD", "review": { "status": "approved" } } ``` The response includes: - `id` for the ad account - `name` for the display name - `url` for the primary destination - `preview_url` for the favicon preview URL when one is available - `status` when an account status is available - `timezone` for the ad account timezone - `currency_code` for the account currency - `review` for the account's brand review status ## Account spending limits Set a shared spending limit across all campaigns in an ad account. Choose a date range limit for a total allowance over a fixed period, or a daily limit for an allowance that renews each day. Campaign budgets still apply; account limits do not allocate spend between campaigns or pace delivery. > **Note:** Account spending limits are available only for ad accounts on > postpaid invoice billing. Existing daily limits can still be viewed and removed > if the account's billing changes. All requests in this section, including reads, > require permission to manage billing for the account. Both types use the account currency and timezone: - Amounts are nonnegative integers in micros: `100000000` is 100 currency units. Use the currency's smallest unit, such as multiples of `10000` micros for USD. The maximum is 1 billion currency units (`1000000000000000` micros). - Dates use `YYYY-MM-DD`. Limits start at midnight on the start date and end at midnight on the end date, in the account timezone. The end date is excluded. - Daily and date range limits cannot overlap. Existing scheduled limits are not removed when you create a daily limit. - Daily allowances do not carry over or change with daylight saving time. The account timezone cannot change while a daily limit exists. Limits apply to billable ad spend, not taxes or the total invoice. Delivery can take time to stop after a limit is reached. Amount edits take effect on save and retain counted spend; a decrease below recorded spend is rejected. Edits are not retroactive: delayed charges use the limit effective when the event occurred, and concurrent billing can add spend while an edit is saved. A lower limit cannot undo spend already billed. ## List account spending limits `GET /ad_account/spend_limit_windows` Read the configuration before making changes. This endpoint takes no request body or query parameters. Use the [account's Ads API key](https://developers.openai.com/ads/api-reference/authentication). ```bash curl "https://api.ads.openai.com/v1/ad_account/spend_limit_windows" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" ``` The response has `object: "list"` and these fields: | Field | Description | | --------------------------- | ---------------------------------------------------------------------------------------------------------------------- | | `data` | Date range limits, ordered by start date. Daily limits are not in this array. | | `daily_limit` | The active or upcoming daily limit, or `null` when none exists. | | `revision` | Configuration revision to send as `expected_revision` in daily mutations. Use `0` only if the field is absent. | | `earliest_daily_start_date` | Earliest allowed start date for a new daily limit. It is never today. The full period must also avoid existing limits. | | `earliest_dated_start_date` | Additional start-date floor for new or rescheduled date range limits after a daily limit, or `null` when none applies. | | `can_create_daily_limit` | Whether you can create a new daily limit. `false` while one exists does not prevent editing it. | | `evaluated_at` | Configuration evaluation time, not the freshness of spend data. | Each date range limit includes `window_id`, `start_date`, `end_date`, `amount_micros`, `name`, `io_id`, and `status` (`upcoming`, `active`, or `completed`). Use `can_edit`, `can_edit_start_date`, and `can_delete` to determine which changes are available. `spent_micros` is included for the active window when available; an omitted value does not mean zero spend. The `daily_limit` object includes `amount_micros`, `start_date`, `end_date` (`null` for no end date), `timezone`, and `status` (`upcoming` or `active`). It also includes: | Field | Description | | ------------------ | ------------------------------------------------------------------------------------------------------- | | `spent_micros` | Spend counted toward today's allowance; `null` if unavailable or not active. | | `remaining_micros` | Today's remaining allowance; `null` if unavailable or not active. | | `spend_as_of` | Spend counter update time in ISO 8601 format, or `null` if unavailable. | | `next_reset` | Next midnight with a fresh allowance, in ISO 8601 format; `null` before activation or on the final day. | ## Create a date range limit `POST /ad_account/spend_limit_windows` | Field | Type | Required | Notes | | --------------- | -------------- | -------- | --------------------------------------------------------------------------------------------- | | `start_date` | string | Yes | Today or later in the account timezone; also honor `earliest_dated_start_date` when returned. | | `end_date` | string | Yes | Exclusive end date, after `start_date`. | | `amount_micros` | integer | Yes | Total allowance for the entire period. | | `name` | string or null | No | Optional label, up to 256 characters. | | `io_id` | string or null | No | Optional insertion order reference, up to 256 characters. | For a USD account, this example sets a $1,000 total allowance for October 1–7. Replace the dates with a valid period that does not overlap another limit. ```bash curl -X POST "https://api.ads.openai.com/v1/ad_account/spend_limit_windows" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "start_date": "2026-10-01", "end_date": "2026-10-08", "amount_micros": 1000000000, "name": "October promotion" }' ``` The response is the created date range limit object, including its `window_id`. You can schedule up to 60 future date range limits. ## Update a date range limit `POST /ad_account/spend_limit_windows/{window_id}` Send at least one field from the create request. Omitted fields retain their values; send `null` to clear `name` or `io_id`. Dates and amounts cannot be `null`. Date range mutations do not require `expected_revision`. Only upcoming limits can change their start date. An active limit's end date must remain in the future. Completed limits cannot be edited. For example, to change the total allowance to $1,500 for a USD account, send: ```json { "amount_micros": 1500000000 } ``` The response is the updated date range limit object. ## Delete a date range limit `POST /ad_account/spend_limit_windows/{window_id}/delete` No request body is required. You can delete active or upcoming limits, but not completed limits. Deletion takes effect on save and does not erase historical spend. ```json { "window_id": "slw_123", "object": "ad_account_spend_limit_window.deleted", "deleted": true } ``` ## Set a daily limit `POST /ad_account/daily_spend_limit` Use this endpoint to create or update a daily allowance. It repeats until removed or until its optional end date. New daily limits start tomorrow or later; use `earliest_daily_start_date` from the list response. | Field | Type | Required | Notes | | ------------------- | -------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `amount_micros` | integer | Yes | Allowance for each day. Required even when only changing the end date. | | `expected_revision` | integer | Yes | `revision` from the latest list response. | | `start_date` | string | No | First allowed day. On first creation, defaults to `earliest_daily_start_date`. Required when creating a new limit after removal or expiry. Omit when editing; an existing start date cannot change. | | `end_date` | string or null | No | Exclusive end date, after the start date and in the future. Omit to retain an existing end date; send `null` for no end date. | For a USD account, this example creates a $100 daily allowance with no end date. Replace the illustrative revision and start date with values from the latest list response, and check for overlaps before submitting. ```bash curl -X POST "https://api.ads.openai.com/v1/ad_account/daily_spend_limit" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "amount_micros": 100000000, "expected_revision": 12, "start_date": "2026-10-01", "end_date": null }' ``` For an amount edit, send `amount_micros` and the latest `expected_revision`; omit `start_date`. Both increases and decreases take effect on save, subject to the [edit rules](#account-spending-limits). The response contains `spend_limits`, with the same fields as the list response. Read `spend_limits.daily_limit` for the saved limit and `spend_limits.revision` for the new revision. ## Remove a daily limit `POST /ad_account/daily_spend_limit/delete` Send `expected_revision` from the latest list response: ```json { "expected_revision": 13 } ``` Removal takes effect on save and retains historical accounting for delayed charges. The response contains the updated list under `spend_limits`, with `daily_limit: null`. ## Handle spending limit errors | Status | Action | | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `400` | Correct missing or malformed request fields, including invalid calendar dates or dates outside the supported range of `2000-01-01` through `2100-01-01`. | | `409` | Refresh the list and reconcile a stale revision or configuration conflict before resubmitting the change. | | `422` | Correct the amount, date ordering, overlapping limits, or a change to a completed date range limit. A decrease below recorded spend returns `budget_window_amount_below_spend`. | | `503` | Refresh before retrying a daily mutation: the change may have succeeded even if its response could not be loaded. | --- # Ad Groups ## List ad groups List ad groups for a campaign. `GET /ad_groups` | Parameter | Type | Required | Notes | | ------------- | ------- | -------- | ---------------------------------- | | `campaign_id` | string | Yes | Parent campaign ID. | | `limit` | integer | No | Between `1` and `500`. Default 20. | | `after` | string | No | Cursor for the next page. | | `before` | string | No | Cursor for the previous page. | | `order` | string | No | `asc` or `desc`. | ```bash curl -X GET "https://api.ads.openai.com/v1/ad_groups?campaign_id=cmpn_101&limit=10" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" ``` ```json { "object": "list", "data": [ { "id": "adgrp_301", "created_at": 1735689700, "updated_at": 1735776100, "name": "US English", "description": "Primary English-speaking audience.", "context_hints": ["productivity", "team collaboration"], "status": "active", "bidding_config": { "billing_event_type": "impression", "max_bid_micros": 60000 } } ], "first_id": "adgrp_301", "last_id": "adgrp_301", "has_more": false } ``` ## Create an ad group Create an ad group for a campaign. `POST /ad_groups` | Field | Type | Required | Notes | | ------------------------------------------------------------------------ | -------- | --------------------- | ----------------------------------------------------------------------------------- | | `campaign_id` | string | Yes | Parent campaign ID. | | `name` | string | Yes | `3` to `1000` chars and must include a non-space character. | | `description` | string | No | Ad group description. | | `context_hints` | string[] | No | Free-form audience or placement hints. | | `status` | string | Yes | `active` or `paused`. | | `bidding_config.billing_event_type` | string | Yes | `impression` for impression campaigns; `click` for click and conversion campaigns. | | `bidding_config.max_bid_micros` | integer | Yes | Minimum `1`; the maximum depends on campaign bidding type and account currency. | | `bidding_config.custom_audience_bid_multipliers` | object[] | No | Bid adjustments for ready audiences eligible for bid multipliers. | | `bidding_config.custom_audience_bid_multipliers[].custom_audience_id` | string | Yes, in each item | An audience returned by `GET /custom_audiences?intended_use=bid_multiplier`. | | `bidding_config.custom_audience_bid_multipliers[].bid_multiplier_micros` | integer | Yes, in each item | `100000` to `10000000`, representing 0.1× to 10×. | | `product_set` | object | No | Inherits the campaign's feed when omitted. Include only to specify product filters. | | `product_set.product_feed_id` | string | Yes, in `product_set` | Must match the campaign's product feed. | | `product_set.filters` | object[] | No | Product filters. Don't repeat the same field within one product set. | | `product_set.filters[].field` | string | Yes, in each filter | Feed attribute to filter. | | `product_set.filters[].operator` | string | Yes, in each filter | `in`, `gt`, `gte`, `lt`, or `lte`. | | `product_set.filters[].values` | string[] | Yes, in each filter | Match values. Send numeric comparison values as strings, such as `"4.5"`. | ### Field notes To adjust bids for matched customers without changing campaign eligibility, see [Custom Audiences](https://developers.openai.com/ads/custom-audiences#adjust-bids-for-an-audience). Small audiences can be eligible for campaign exclusion without meeting the minimum size for bid adjustments. Don't infer bid eligibility from `ready` status or a matched-count range. The server validates eligibility when you save the ad group. Product-set filters support `title`, `body`, `item_id`, `offer_id`, `price`, `target_url`, `image_url`, `product_category`, `brand`, `seller_name`, `external_seller_id`, `star_rating`, `condition`, and `age_group`. Use `gt`, `gte`, `lt`, and `lte` only with `price` or `star_rating`. Context hints provide extra information on when you think your ads might be useful, and help guide when they appear. Provide a list of descriptions or keywords for when the product or service might be useful to show. Micros are millionths of the main currency unit, for example, dollars. The `max_bid_micros` field is per event, so a $60 CPM ($0.06 per impression) is passed as `60000` to the API. Currency fields respect your ad account's default currency. For a conversion-optimized campaign (oCPC), set `bidding_config.billing_event_type` to `click`. `max_bid_micros` is the CPA bid even though the billing event is a click. For example, `100000000` is a $100.00 CPA bid for a USD account. For a product-feed oCPC campaign in open beta, use the same `POST /ad_groups` endpoint. The ad group automatically inherits the campaign's product feed. Include `product_set` only when you want to specify product filters; its `product_feed_id` must match the campaign's feed. For the full setup and reporting workflow, see [Conversion-Optimized Campaigns](https://developers.openai.com/ads/conversion-optimized-campaigns) and [Product Feeds](https://developers.openai.com/ads/product-feeds). ```bash curl -X POST "https://api.ads.openai.com/v1/ad_groups" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "campaign_id": "cmpn_101", "name": "US English", "description": "Primary English-speaking audience.", "context_hints": ["productivity", "team collaboration"], "status": "active", "bidding_config": { "billing_event_type": "impression", "max_bid_micros": 60000 } }' ``` ## Retrieve an ad group Fetch one ad group by ID. `GET /ad_groups/{ad_group_id}` ```bash curl -X GET "https://api.ads.openai.com/v1/ad_groups/adgrp_301" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" ``` ## Update an ad group Update an ad group with `POST`. `POST /ad_groups/{ad_group_id}` All fields are optional on update. `description` can be set to `null` to clear it. If you include `bidding_config`, send the full object. `status` accepts `active`, `paused`, or `archived`. For a product-feed campaign, include the full `product_set` object to change the feed or its filters. Retrieve the ad group after the update if you need the resulting `product_set`; the immediate update response can omit it. ```bash curl -X POST "https://api.ads.openai.com/v1/ad_groups/adgrp_301" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "context_hints": ["productivity", "workflow automation"], "status": "paused", "bidding_config": { "billing_event_type": "impression", "max_bid_micros": 75000 } }' ``` ## Change state with dedicated actions The Ads API also exposes explicit state transitions. Paused ad groups won't deliver ads to customers. Only archive objects you have no further use for, as archiving isn't reversible. - `POST /ad_groups/{ad_group_id}/activate` - `POST /ad_groups/{ad_group_id}/pause` - `POST /ad_groups/{ad_group_id}/archive` ```bash curl -X POST "https://api.ads.openai.com/v1/ad_groups/adgrp_301/archive" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" ``` ```json { "id": "adgrp_301", "created_at": 1735689700, "updated_at": 1735948800, "name": "US English", "description": "Primary English-speaking audience.", "context_hints": ["productivity", "team collaboration"], "status": "archived", "bidding_config": { "billing_event_type": "impression", "max_bid_micros": 60000 } } ``` --- # Ads ## List ads List ads for an ad group. `GET /ads` | Parameter | Type | Required | Notes | | ------------- | ------- | -------- | ---------------------------------- | | `ad_group_id` | string | Yes | Parent ad group ID. | | `limit` | integer | No | Between `1` and `500`. Default 20. | | `after` | string | No | Cursor for the next page. | | `before` | string | No | Cursor for the previous page. | | `order` | string | No | `asc` or `desc`. | ```bash curl -X GET "https://api.ads.openai.com/v1/ads?ad_group_id=adgrp_301&limit=10" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" ``` ```json { "object": "list", "data": [ { "id": "ad_501", "name": "Planner launch card", "created_at": 1735689800, "updated_at": 1735776200, "creative": { "type": "chat_card", "title": "Try the new workspace planner", "body": "Coordinate tasks, docs, and meetings in one place.", "file_id": "file_901", "image_url": "https://cdn.openai.com/ads/file_901.png", "target_url": "https://example.com/workspace-planner" }, "status": "active", "review_status": "approved" } ], "first_id": "ad_501", "last_id": "ad_501", "has_more": false } ``` ## Create an ad Create an ad for an ad group. `POST /ads` | Field | Type | Required | Notes | | --------------------- | ------ | --------------- | ------------------------------------------------------------------------------------------------------------- | | `ad_group_id` | string | Yes | Parent ad group ID. | | `name` | string | Yes | `3` to `1000` chars and must include a non-space character. Used for organization, is not shown to end users. | | `creative.type` | string | Yes | `chat_card` or `product_ad_template`. See [Product Feeds](https://developers.openai.com/ads/product-feeds). | | `creative.title` | string | Yes | `3` to `50` chars. | | `creative.body` | string | Yes | Maximum `100` chars. | | `creative.price` | string | No | Price text or `{{product.price}}` for a product-ad template. | | `creative.target_url` | string | For `chat_card` | Destination URL. A product-ad template receives it from the selected feed item. | | `creative.file_id` | string | For `chat_card` | File returned by `POST /upload`. A product-ad template receives its image from the selected feed item. | | `status` | string | Yes | `active` or `paused`. | ```bash curl -X POST "https://api.ads.openai.com/v1/ads" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "ad_group_id": "adgrp_301", "name": "Planner launch card", "status": "active", "creative": { "type": "chat_card", "title": "Try the new workspace planner", "body": "Coordinate tasks, docs, and meetings in one place.", "target_url": "https://example.com/workspace-planner", "file_id": "file_901" } }' ``` ### Product-ad templates A product-feed ad group can contain at most one non-archived `product_ad_template` ad. Product-ad templates receive their image and destination URL from the selected feed item, so they don't require `creative.file_id` or `creative.target_url`. Follow the [product feeds guide](https://developers.openai.com/ads/product-feeds) for the complete campaign, product-set, and template workflow. ## Retrieve an ad Fetch one ad by ID. `GET /ads/{ad_id}` ```bash curl -X GET "https://api.ads.openai.com/v1/ads/ad_501" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" ``` ## Update an ad Update an ad with `POST`. `POST /ads/{ad_id}` All fields are optional on update. If you include `creative`, send the full creative object. `status` accepts `active`, `paused`, or `archived`. ```bash curl -X POST "https://api.ads.openai.com/v1/ads/ad_501" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Planner launch card v2", "status": "paused", "creative": { "type": "chat_card", "title": "Plan work faster", "body": "Bring tasks, docs, and meetings together.", "target_url": "https://example.com/workspace-planner", "file_id": "file_901" } }' ``` ## Review status Every returned ad object includes `review_status`, which can be: - `in_review` - `rejected` - `approved` If your ad has been rejected, it violates one of our [ads policies](https://openai.com/policies/ad-policies/). Please edit your ad for it to be re-reviewed. ## Preview an ad Preview an existing ad by ID. The preview expires 24 hours after creation. `POST /ads/{ad_id}/preview` ```bash curl -X POST "https://api.ads.openai.com/v1/ads/ad_501/preview" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" ``` ## Change state with dedicated actions The Ads API also exposes explicit state transitions. Paused ads won't deliver to customers. Only archive objects you have no further use for, as archiving isn't reversible. - `POST /ads/{ad_id}/activate` - `POST /ads/{ad_id}/pause` - `POST /ads/{ad_id}/archive` ```bash curl -X POST "https://api.ads.openai.com/v1/ads/ad_501/pause" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" ``` ```json { "id": "ad_501", "name": "Planner launch card", "created_at": 1735689800, "updated_at": 1736035200, "creative": { "type": "chat_card", "title": "Try the new workspace planner", "body": "Coordinate tasks, docs, and meetings in one place.", "file_id": "file_901", "image_url": "https://cdn.openai.com/ads/file_901.png", "target_url": "https://example.com/workspace-planner" }, "status": "paused", "review_status": "approved" } ``` --- # Authentication ## Base URL Send Ads API requests to: ```text https://api.ads.openai.com/v1 ``` ## Authorization The OpenAPI spec defines a bearer security scheme. Pass your Ads API key on every request: | Header | Value | | --------------- | ---------------------------- | | `Authorization` | `Bearer $OPENAI_ADS_API_KEY` | Each Ads API key is scoped to one ad account. API partners should use the key associated with the client account they are configuring. ## Request formats Most Ads API endpoints accept `application/json`. The upload endpoint supports two request formats: - `application/json` with an `image_url` - `multipart/form-data` with a binary `file` ## Example request Use `GET /ad_account` to confirm that your bearer token works. ```bash curl -X GET "https://api.ads.openai.com/v1/ad_account" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" \ -H "Accept: application/json" ``` ```json { "id": "adacct_123", "name": "Acme Ads", "url": "https://www.acme.example", "preview_url": null, "status": "active", "timezone": "UTC", "currency_code": "USD", "review": { "status": "approved" } } ``` --- # Campaigns ## List campaigns List campaigns in the current ad account. `GET /campaigns` | Parameter | Type | Required | Notes | | --------- | ------- | -------- | ---------------------------------- | | `limit` | integer | No | Between `1` and `500`. Default 20. | | `after` | string | No | Cursor for the next page. | | `before` | string | No | Cursor for the previous page. | | `order` | string | No | `asc` or `desc`. | ```bash curl -X GET "https://api.ads.openai.com/v1/campaigns?limit=20&order=desc" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" ``` ```json { "object": "list", "data": [ { "id": "cmpn_101", "created_at": 1735689600, "status": "active", "bidding_type": "impressions", "budget": { "lifetime_spend_limit_micros": 25000000 }, "conversion_event_setting_ids": [], "description": "Promote the new productivity bundle.", "end_time": 1738368000, "mode": null, "name": "Spring launch", "start_time": 1735689600, "targeting": {}, "updated_at": 1735776000 } ], "first_id": "cmpn_101", "last_id": "cmpn_101", "has_more": false } ``` ## Create a campaign Create a campaign for the current ad account. The Ads belonging to a campaign will only show between the defined start and end time, and only in the locations specified in campaign targeting. For location, platform, and custom audience targeting, see [Campaign Targeting](https://developers.openai.com/ads/campaign-targeting). ### Defaults If you omit `start_time`, the campaign will begin delivering immediately. If you omit location targeting, the campaign can target all available locations. Omitting `targeting.platforms` or setting it to `null` adds no platform restriction. Note that time and currency fields will respect your account-set timezone and currency defaults. `POST /campaigns` | Field | Type | Required | Notes | | ----------------------------------------- | -------- | -------- | ----------------------------------------------------------------------------------------- | | `name` | string | Yes | `3` to `1000` chars and must include a non-space character. | | `description` | string | No | Campaign description. | | `start_time` | integer | No | Unix timestamp between `946684800` and `4102444800`. | | `end_time` | integer | No | Unix timestamp between `946684800` and `4102444800`. | | `status` | string | Yes | `active` or `paused`. | | `budget.lifetime_spend_limit_micros` | integer | Yes | Minimum `1000000`. | | `mode` | string | No | Set to `product_feed` to create a [product-feed campaign](https://developers.openai.com/ads/product-feeds). | | `bidding_type` | string | No | `impressions`, `clicks`, or `conversions`. Defaults to `impressions`. | | `conversion_event_setting_ids` | string[] | No | For `conversions`, exactly one active standard event setting ID from this account. | | `targeting.locations.include` | object[] | No | Included location IDs. | | `targeting.platforms.included` | string[] | No | ChatGPT platforms. See [Platform Targeting](https://developers.openai.com/ads/platform-targeting) for accepted values. | | `targeting.custom_audiences.ids` | string[] | No | Ready audience IDs eligible for inclusion. | | `targeting.excluded_custom_audiences.ids` | string[] | No | Ready audience IDs eligible for exclusion, including small audiences. | See [Custom Audiences](https://developers.openai.com/ads/custom-audiences#include-or-exclude-audiences-in-a-campaign) for audience matching, exclusions, and minimum-size requirements. Check audiences with `GET /custom_audiences?intended_use=inclusion` or `intended_use=exclusion` before using them. A ready small or empty audience can be excluded, but it isn't automatically eligible for inclusion. If you include and exclude audiences, the remaining population must still meet the minimum. For an exclusion-only campaign, omit `targeting.custom_audiences`. ```bash curl -X POST "https://api.ads.openai.com/v1/campaigns" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Spring launch", "description": "Promote the new productivity bundle.", "start_time": 1735689600, "end_time": 1738368000, "status": "active", "budget": { "lifetime_spend_limit_micros": 25000000 }, "targeting": { "locations": { "include": [{ "id": "2000043" }, { "id": "3000194" }] } } }' ``` ```json { "id": "cmpn_101", "created_at": 1735689600, "updated_at": 1735689600, "name": "Spring launch", "description": "Promote the new productivity bundle.", "status": "active", "start_time": 1735689600, "end_time": 1738368000, "budget": { "lifetime_spend_limit_micros": 25000000 }, "bidding_type": "impressions", "targeting": { "locations": { "include": [ { "id": "2000043", "type": "region", "country_code": "US", "name": "California", "region_code": "US-CA" }, { "id": "3000194", "type": "market", "country_code": "US", "name": "San Francisco - Oakland - San Jose", "region_code": null } ] } } } ``` ### Create a conversion-optimized campaign To use oCPC, set `bidding_type` to `conversions` and pass exactly one active standard conversion event setting from the current ad account. The event setting must connect to one active conversion source. Custom event settings cannot be optimization goals. ```bash curl -X POST "https://api.ads.openai.com/v1/campaigns" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Acme purchases", "status": "paused", "budget": { "lifetime_spend_limit_micros": 250000000 }, "bidding_type": "conversions", "conversion_event_setting_ids": ["ces_123"] }' ``` Conversion bidding must be enabled for the ad account. Product-feed campaigns can use oCPC during the open beta. Use the same endpoint, set `mode` to `product_feed`, and include the linked `product_feed_id`. You cannot change the campaign objective or selected conversion event after creation. For the complete setup flow, including the required ad-group bid configuration, see [Conversion-Optimized Campaigns](https://developers.openai.com/ads/conversion-optimized-campaigns), [Product Feeds](https://developers.openai.com/ads/product-feeds), and [API Partner Setup](https://developers.openai.com/ads/api-partner-setup). ## Retrieve a campaign Fetch one campaign by ID. `GET /campaigns/{campaign_id}` ```bash curl -X GET "https://api.ads.openai.com/v1/campaigns/cmpn_101" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" ``` ## Update a campaign Update a campaign with `POST`, not `PATCH` or `PUT`. `POST /campaigns/{campaign_id}` All fields are optional on update. If you include `budget`, send the full budget object. `description`, `start_time`, `end_time`, and `targeting` can be set to `null` to clear them. `status` accepts `active`, `paused`, or `archived`. You cannot update `bidding_type`. For a conversion-optimized campaign, you also cannot update `conversion_event_setting_ids`. Omitting `targeting.platforms` preserves the existing platform selection. Provide `targeting.platforms.included` to replace it, or set `targeting.platforms` to `null` to clear only the platform restriction. Empty platform objects and empty `included` arrays return HTTP `400`. See [Update or clear platform targeting](https://developers.openai.com/ads/platform-targeting#update-or-clear-platform-targeting) for an example. Audience eligibility is validated again when you save targeting. A concurrent membership update can return `409 custom_audience_mutation_conflict` without applying the campaign edit. Wait for the audience operation to finish, retrieve the current settings, and retry the intended edit if it is still appropriate. ```bash curl -X POST "https://api.ads.openai.com/v1/campaigns/cmpn_101" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "description": "Updated launch window and budget.", "status": "paused", "budget": { "lifetime_spend_limit_micros": 30000000 } }' ``` ## Change state with dedicated actions The Ads API also exposes explicit state transitions. Each endpoint returns the updated campaign object. Paused campaigns won't deliver ads to customers. Only archive objects you have no further use for, as archiving isn't reversible. - `POST /campaigns/{campaign_id}/activate` - `POST /campaigns/{campaign_id}/pause` - `POST /campaigns/{campaign_id}/archive` ```bash curl -X POST "https://api.ads.openai.com/v1/campaigns/cmpn_101/pause" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" ``` ```json { "id": "cmpn_101", "created_at": 1735689600, "updated_at": 1735862400, "name": "Spring launch", "description": "Promote the new productivity bundle.", "status": "paused", "start_time": 1735689600, "end_time": 1738368000, "budget": { "lifetime_spend_limit_micros": 25000000 }, "bidding_type": "impressions" } ``` --- # Conversion Setup Use the Ads API to configure conversion measurement for the current ad account. These endpoints create the resources that the JavaScript Pixel and Conversions API use to send events. Pixel management and Conversions API key creation must be enabled for the ad account. If `/conversions/pixels` or `/conversions/api_keys` returns `404` with `Not found`, contact your OpenAI partner representative. A `Client data source not found` response from event-setting creation means that `source_ids` references a source that does not exist in the current ad account. ## Create a pixel Create a web conversion source and its Pixel ID. `POST /conversions/pixels` | Field | Type | Required | Notes | | ------------- | ------ | -------- | ---------------------------------------------- | | `name` | string | Yes | A descriptive name from 3 to 1,000 characters. | | `client_type` | string | Yes | Use `web`. | ```bash curl -X POST "https://api.ads.openai.com/v1/conversions/pixels" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Acme website", "client_type": "web" }' ``` ```json { "id": "clidsrc_123", "client_type": "web", "name": "Acme website", "pixel_id": "134534..." } ``` Use `id` as a `source_ids` value when you create an event setting. Use `pixel_id` to initialize the JavaScript Pixel and when you send Conversions API events. Web pixels created by this endpoint automatically use [automatic advanced matching](https://developers.openai.com/ads/measurement-pixel#automatic-advanced-matching). ## Check recent Pixel events Use the conversion event stream while testing a [JavaScript Pixel](https://developers.openai.com/ads/measurement-pixel) integration. It returns up to 50 conversion events received from the Pixel SDK during the previous 15 minutes. > **Note:** The conversion event stream is available only to enabled accounts. > If this endpoint returns `404` with `Not found`, contact your OpenAI partner > representative. `GET /conversions/events` | Parameter | Type | Required | Notes | | --------- | ------ | -------- | ------------------------------------ | | `pid` | string | Yes | Pixel ID returned by pixel creation. | ```bash curl -X GET "https://api.ads.openai.com/v1/conversions/events?pid=" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" ``` ```json { "object": "list", "data": [ { "action_source": "web", "api_channel": "pixel_sdk", "custom_event_name": null, "data_source_id": "cds_123", "event_data_json": "{\"type\":\"customer_action\"}", "event_timestamp_ms": 1787082318902, "event_type": "registration_completed", "received_at_ms": 1787082320225 } ] } ``` Use this endpoint to confirm that recent browser events reached OpenAI, not for attribution or reporting. Use conversion insights for attributed conversion totals. If more than 50 events arrive during the window, the response includes only the 50 most recent events. ## Create a Conversions API key Create a key that can send server-side events for the current ad account. `POST /conversions/api_keys` | Field | Type | Required | Notes | | ------ | ------ | -------- | ---------------------------------------------- | | `name` | string | Yes | A descriptive name from 3 to 1,000 characters. | ```bash curl -X POST "https://api.ads.openai.com/v1/conversions/api_keys" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Acme production conversions" }' ``` ```json { "name": "Acme production conversions", "api_key": "" } ``` > **Caution:** Store the returned key in a server-side secret manager. Never > place it in browser code, client-visible environment variables, logs, or > source control. ## Create an event setting Create a conversion definition and connect it to one conversion source. `POST /conversions/event_settings` | Field | Type | Required | Notes | | ------------------------- | -------- | -------- | ------------------------------------------------------------ | | `name` | string | Yes | Display name for the conversion. | | `event_type` | string | Yes | A [supported event](https://developers.openai.com/ads/supported-events), or `custom`. | | `custom_event_name` | string | Depends | Required when `event_type` is `custom`. | | `attribution_window_days` | integer | Yes | Use `30`. | | `source_ids` | string[] | Yes | Exactly one conversion source ID returned by pixel creation. | ```bash curl -X POST "https://api.ads.openai.com/v1/conversions/event_settings" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Purchases", "event_type": "order_created", "attribution_window_days": 30, "source_ids": ["clidsrc_123"] }' ``` ```json { "id": "ces_123", "name": "Purchases", "event_type": "order_created", "custom_event_name": null, "attribution_window_days": 30, "ad_account_id": "adacct_123", "source_ids": ["clidsrc_123"], "sources": [ { "id": "clidsrc_123", "name": "Acme website" } ], "campaigns": [], "archived": false, "version": 1 } ``` ## List event settings List conversion definitions for the current ad account. `GET /conversions/event_settings` | Parameter | Type | Required | Notes | | --------- | ------- | -------- | ----------------------------- | | `limit` | integer | No | Between `1` and `500`. | | `after` | string | No | Cursor for the next page. | | `before` | string | No | Cursor for the previous page. | | `order` | string | No | `asc` or `desc`. | ```bash curl -X GET "https://api.ads.openai.com/v1/conversions/event_settings?limit=20&order=desc" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" ``` ## Send events After setup, use the returned Pixel ID and Conversions API key to implement [server-side event delivery](https://developers.openai.com/ads/conversions-api). For browser measurement, use the [JavaScript Pixel](https://developers.openai.com/ads/measurement-pixel). If both sources send the same event, use a shared event ID so OpenAI processes it only once. --- # Files ## Upload from an image URL Upload a remote image with JSON and receive a reusable `file_id`. `POST /upload` ```bash curl -X POST "https://api.ads.openai.com/v1/upload" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "image_url": "https://example.com/assets/workspace-planner-card.png" }' ``` ```json { "file_id": "file_901" } ``` ## Upload a binary file The same endpoint also accepts `multipart/form-data` with a binary file. ```bash curl -X POST "https://api.ads.openai.com/v1/upload" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" \ -F "file=@workspace-planner-card.png" ``` ## Upload a custom audience file Upload a UTF-8 CSV or TXT customer list with the dedicated `POST /uploads` endpoint. Set `purpose` to `custom_audience` and save the returned `file_id`: ```bash curl -X POST "https://api.ads.openai.com/v1/uploads" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" \ -F "file=@audience.csv;type=text/csv" \ -F "purpose=custom_audience" ``` Use the file ID, original filename, MIME type, and exact file size to [create a custom audience](https://developers.openai.com/ads/custom-audiences#create-the-custom-audience). You can also use an uploaded file to [add, remove, or replace audience members](https://developers.openai.com/ads/custom-audiences#add-or-remove-using-a-file). CSV files can combine email, phone, GAID, and hashed identifier columns when the audience request sets `identifier_resolution` to `auto`. See [audience file requirements](https://developers.openai.com/ads/custom-audiences#prepare-an-audience-file). ## Upload an account `favicon` Set `purpose` to `account_favicon` when you upload an image for account brand review. The image must be at least 128 × 128 pixels. The API can resolve an icon from the client's website: ```bash curl -X POST "https://api.ads.openai.com/v1/upload" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "image_url": "https://www.acme.example", "purpose": "account_favicon" }' ``` To upload a local image, send `purpose` as a multipart form field: ```bash curl -X POST "https://api.ads.openai.com/v1/upload" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" \ -F "purpose=account_favicon" \ -F "file=@acme-favicon.png" ``` Assign the returned `file_id` with [`POST /ad_account/brand`](https://developers.openai.com/ads/api-reference/ad-account#update-account-brand-metadata). ## Use the uploaded file in an ad Pass the returned `file_id` when you create or update an ad creative. `POST /ads` ```bash curl -X POST "https://api.ads.openai.com/v1/ads" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "ad_group_id": "adgrp_301", "name": "Planner launch card", "status": "active", "creative": { "type": "chat_card", "title": "Try the new workspace planner", "body": "Coordinate tasks, docs, and meetings in one place.", "target_url": "https://example.com/workspace-planner", "file_id": "file_901" } }' ``` --- # Insights ## Endpoints Use one of the four `GET` endpoints for general delivery insights. Each returns the same top-level response shape, with IDs, metadata, and metrics appropriate to its scope. - `GET /ad_account/insights` - `GET /campaigns/{campaign_id}/insights` - `GET /ad_groups/{ad_group_id}/insights` - `GET /ads/{ad_id}/insights` Use `POST /conversions/insights` for attributed conversion totals. ## Conversion insights Authorized `POST /conversions/insights` responses include `conversions`, `click_through_conversions`, and `view_through_conversions`. `conversions` is always equal to `click_through_conversions`; view-through conversions are a separate, supplemental metric and are not added to that total. Click-through attribution follows the applicable configured click window. View-through reporting availability is independent of the advertiser's configured click window, and view-through attribution uses a fixed one-day window after an eligible ad impression. When a conversion is eligible for both, the click takes precedence. View-through conversions are for reporting only. CPA, post-click CVR, bidding, billing, and conversion optimization remain click-through-based. In Ads Manager, view-through conversion reporting is available at the campaign level for accounts with this reporting available. ### Campaign example ```bash curl -sS -X POST "https://api.ads.openai.com/v1/conversions/insights" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" \ -H "Content-Type: application/json" \ --data '{ "aggregation_level": "campaign", "time_ranges": ["{\"type\":\"unix_range\",\"start\":\"1738368000\",\"end\":\"1738454400\"}"], "entity_ids": ["campaign_1"] }' ``` Representative response: ```json { "object": "list", "data": [ { "entity_id": "campaign_1", "conversions": 7, "click_through_conversions": 7, "view_through_conversions": 3 } ], "count": 1 } ``` ## Terminology | Term | Values | Meaning | | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `{aggregation_level}` | `ad_account`, `campaign`, `ad_group`, `ad` | Public row entities. The endpoint sets scope; `aggregation_level` chooses the row entity inside that scope. | | `time_granularity` | `hourly`, `daily`, `monthly`, `none` | Bucket size. `none` returns one bucket for the full requested window. | | `segments[]` | `product`, `country`, `device`, `platform` | Optional extra breakdown dimension. `{segment}` below means the requested segment value. | | `{entity}` | The row `{aggregation_level}` or requested `{segment}` | Entity named in `override_segment_group_order[]`. Use it when requesting grouped metrics in a segmented request. | | `{metric}` | `impressions`, `clicks`, `spend`, `ctr`, `cpc`, `cpm` | Aggregated numeric fields. | | `{aggregation_level}.id` | `ad_account.id`, `campaign.id`, `ad_group.id`, `ad.id` | Canonical aggregation-level ID fields. They are valid when that aggregation level is present in the row. | | `{aggregation_level}.{metric}` | `campaign.impressions`, `ad.clicks`, `ad_group.spend` | Metric for the row aggregation level. For default rows, use `{aggregation_level}.{metric}`. In segmented requests, grouped metrics can name the entity or segment in `override_segment_group_order[]`. | | `{aggregation_level}.{metadata}` | `ad_account.name`, `ad_account.url`, `ad_account.budget.lifetime`, `ad_account.budget.daily`; `campaign.name`, `campaign.description`, `campaign.status`, `campaign.start_time`, `campaign.end_time`, `campaign.budget.lifetime`, `campaign.budget.daily`; `ad_group.name`, `ad_group.description`, `ad_group.status`; `ad.title`, `ad.copy`, `ad.link`, `ad.name`, `ad.status`, `ad.review_status` | Canonical aggregation-level metadata fields. They are valid when that aggregation level is present in the row. | | `{segment}.{metric}` | `product.impressions`, `country.clicks`, `device.spend`, `platform.impressions` | Metric for the requested segment group. Valid only when the matching `segments[]` value is present. | | `{segment}.{metadata}` | `product.feed_id`, `product.item_id`, `product.title`, `product.description`, `product.body`, `product.target_url`, `product.image_url`, `product.brand`, `product.seller_name`, `product.price`, `product.availability`; `country.name`; `device.type` | Canonical segment metadata fields. Valid only when the matching `segments[]` value is present. | | `platform` | See [Platform breakdown](#platform-breakdown). | Canonical platform field. Valid only with `segments[]=platform`. | | `metadata.{field}` | `metadata.readable_time`, `metadata.timezone` | Report metadata. The response returns flat keys such as `readable_time` and `timezone`. | | `{product}.{id}` | `product.feed_id`, `product.item_id`, `product.feed_item_id` | Use `product.feed_id` and `product.item_id` to project identity. Use `product.feed_item_id` only in `filters[]` for an exact feed/item pair. | | `filters[].operator` | `IN`, `GREATER_THAN`, `LESS_THAN` | Filter operators. `IN` is for equality-style filters. `GREATER_THAN` and `LESS_THAN` are for numeric thresholds. | | `sort[].direction` | `asc`, `desc` | Sort order. | | `sort[].field` | `{aggregation_level}.{metric}`; `{entity}.{metric}` for a segmented request; `{aggregation_level}.id`; sortable `{aggregation_level}.{metadata}`; sortable `{segment}.{metadata}` | Canonical sort keys. The field must be valid for the current row shape. | | `includes[]` | `zero_impression_items`, `zero_impression_products` | Optional zero-row expansions. See [Includes](#includes) for when each value works. | | `time_ranges[].type` | `unix_range`, `hour_range`, `date_range` | Time-range object type. `unix_range` uses `start` and `end` Unix seconds. `hour_range` uses local `since` and `until` values in `YYYY-MM-DDTHH`. `date_range` uses local `since` and inclusive `until` values in `YYYY-MM-DD`; `until` normalizes to the following local midnight. `hour_range` and `date_range` can include an IANA time zone in `timezone`; otherwise, they use the ad account time zone. | ## Request parameters All query parameters are optional. | Parameter | Type | Value shape | Rules | | ------------------------------ | ---------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `time_granularity` | `string` | One `time_granularity` value | Default `daily`. See [Terminology](#terminology) for bucket behavior. | | `aggregation_level` | `string` | One public `{aggregation_level}` | Set the row entity inside the endpoint scope. Each endpoint supports its own entity level and lower levels in the hierarchy `ad_account` > `campaign` > `ad_group` > `ad`. | | `time_ranges` | `string[]` | One JSON-encoded time-range object | Restrict the report window. Include at least one bound. Bounds must be within the past 5 years and cannot be in the future. The API normalizes them to valid full-hour boundaries. | | `fields` | `string[]` | Repeated canonical field names | Project selected fields; this changes returned columns, not row grouping. When omitted, the `fields` parameter defaults to `impressions`, includes `readable_time` for bucketed results, and includes the row entity's default name. | | `filters` | `string[]` | JSON-encoded filter objects | Restrict which rows survive. See [Filters](#filters). | | `sort` | `string[]` | JSON-encoded sort objects | Order rows before pagination. See [Sorts](#sorts). | | `segments` | `string[]` | At most one `{segment}` | Add one extra breakdown dimension. See [Segments](#segments). | | `override_segment_group_order` | `string[]` | Row entity plus requested segment | Change grouped metric meaning by reordering groups. See [Segments](#segments). | | `includes` | `string[]` | At most one include value | Expand results with supported zero rows. See [Includes](#includes). | | `limit` | `integer` | `1` through `2000` | Default `20`. Caps rows returned in one page after filters and sorting are applied. | | `before` | `string` | Previous-page cursor | Page backward through the current row order. Send only one cursor at a time; use the previous page's `first_id`. | | `after` | `string` | Next-page cursor | Page forward through the current row order. Send only one cursor at a time; use the previous page's `last_id`. | `fields[]` uses canonical names, but many response fields serialize as flat wire keys, such as `campaign.id` to `campaign_id`, `metadata.readable_time` to `readable_time`, and `product.feed_id` to `product_feed_id`. ### Filters | Parameter | Value shape | Rules | Example | | -------------------- | ---------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | | `filters[]` | JSON-encoded objects with `field`, `operator`, `value` | Repeat `filters[]` to combine filters with `AND`. | `{"field":"campaign.id","operator":"IN","value":["cmpn_101"]}` | | `filters[].field` | One canonical field name from [Terminology](#terminology) | The field must be valid for the current row shape. Use `product.feed_item_id` only for an exact feed/item pair filter with JSON-string `IN` values shaped like `{"feed_id":"feed_1","item_id":"sku_1"}`. | `campaign.id` or `ad.clicks` | | `filters[].operator` | `IN`, `GREATER_THAN`, `LESS_THAN` | Use `IN` for resource, segment, or metadata equality. Use `GREATER_THAN` or `LESS_THAN` for numeric metadata or grouped metric thresholds. | `IN` or `GREATER_THAN` | | `filters[].value` | An array of strings or a number, depending on the operator | The value shape must match the operator. | `["cmpn_101"]` or `10` | ### Sorts | Parameter | Value shape | Rules | Example | | ------------------ | ------------------------------------------------------- | ----------------------------------------------- | ------------------------------------------ | | `sort[]` | JSON-encoded objects | Repeat `sort[]` with `field` and `direction`. | `{"field":"ad.clicks","direction":"desc"}` | | `sort[].field` | One canonical sort key from [Terminology](#terminology) | Use a sort key valid for the current row shape. | `ad.clicks` or `product.title` | | `sort[].direction` | One `sort[].direction` value | Use `asc` or `desc`. | `desc` | ### Segments #### Segment rules | Parameter | Rules | | -------------------------------- | --------------------------------------------------------------------------------------- | | `segments[]` | Add one optional breakdown dimension for enabled ad accounts. | | `time_granularity` | Segmented requests support `none`, `daily`, and `monthly`. | | Segment fields | Request fields only for the selected segment. | | `override_segment_group_order[]` | Include the row's `aggregation_level` and the requested segment exactly once, in order. | #### Product example | Goal | Request shape | | -------------------- | --------------------------------------------------------------------------------------------------------- | | Product breakdown | Add `segments[]=product` to an `ad_account`, `campaign`, `ad_group`, or `ad` aggregation level. | | Product fields | Project `product.*` fields from [Terminology](#terminology). | | Product-first rows | Set `override_segment_group_order[]=product`, then `override_segment_group_order[]=`. | | Zero-impression rows | Add `includes[]=zero_impression_products`; see [Includes](#includes) for required order and availability. | #### Platform breakdown Add `segments[]=platform` to split delivery metrics by ChatGPT app or web browser. Include `fields[]=platform` to return the platform value in each row. Platform is a separate dimension from the `device` breakdown. | Response value | Platform | | -------------- | ----------- | | `android_app` | Android app | | `android_web` | Android web | | `desktop_web` | Desktop web | | `ios_app` | iOS app | | `ios_web` | iOS web | | `web` | Web | Historical `web` rows keep combined web totals. They aren't split retroactively into Android web, Desktop web, or iOS web rows. A `platform` filter with `IN` and `web` includes all web platforms: `web`, `android_web`, `desktop_web`, and `ios_web`. For example: ```json { "field": "platform", "operator": "IN", "value": ["web"] } ``` Use `android_web`, `desktop_web`, or `ios_web` to filter to specific web platforms. These filters don't include historical `web` rows. Platform segments support delivery metrics; conversions aren't supported. To choose where a campaign can deliver, see [Platform Targeting](https://developers.openai.com/ads/platform-targeting). ### Includes `includes[]` expands the result set with supported zero-metric rows. It does not change endpoint scope or `aggregation_level`. | Include | Works when | Adds | | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | | `zero_impression_items` | Default entity grouping only: do not send `segments[]`. | Entity rows that had zero impressions in the requested window. | | `zero_impression_products` | Product reporting only: the ad account has product segments and zero-impression products enabled, `segments[]=product`, `override_segment_group_order[]=product` first, and any `filters[]` use only product fields, entity ID fields, or metrics. | Configured product rows that had zero impressions in the requested window. | ## Examples This request scopes to one ad account, groups rows by campaign, and returns one bucket per day. Because `aggregation_level=campaign`, each data row has a `campaign_id` instead of an `ad_id`. ```bash curl -sS -G "https://api.ads.openai.com/v1/ad_account/insights" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" \ --data-urlencode 'time_granularity=daily' \ --data-urlencode 'aggregation_level=campaign' \ --data-urlencode 'fields[]=metadata.readable_time' \ --data-urlencode 'fields[]=campaign.id' \ --data-urlencode 'fields[]=campaign.name' \ --data-urlencode 'fields[]=campaign.clicks' \ --data-urlencode 'fields[]=campaign.impressions' \ --data-urlencode 'fields[]=campaign.spend' \ --data-urlencode 'time_ranges[]={"type":"unix_range","start":1777075200,"end":1777248000}' ``` Representative response: ```json { "object": "list", "data": [ { "id": "start=1777075200:end=1777161600:entity_id=cmpn_101", "start_time": 1777075200, "end_time": 1777161600, "readable_time": "2026-04-25", "campaign_id": "cmpn_101", "campaign_name": "Spring launch", "impressions": 1200, "clicks": 36, "spend": 18.42 }, { "id": "start=1777161600:end=1777248000:entity_id=cmpn_101", "start_time": 1777161600, "end_time": 1777248000, "readable_time": "2026-04-26", "campaign_id": "cmpn_101", "campaign_name": "Spring launch", "impressions": 980, "clicks": 29, "spend": 14.86 } ], "count": 2, "first_id": "start=1777075200:end=1777161600:entity_id=cmpn_101", "last_id": "start=1777161600:end=1777248000:entity_id=cmpn_101", "has_more": false } ``` This uses the same ad-account scope as the previous example, but changes `aggregation_level` from `campaign` to `ad`. The result now has one row per ad per day, so campaign totals can fan out into multiple ad rows. ```bash curl -sS -G "https://api.ads.openai.com/v1/ad_account/insights" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" \ --data-urlencode 'time_granularity=daily' \ --data-urlencode 'aggregation_level=ad' \ --data-urlencode 'fields[]=metadata.readable_time' \ --data-urlencode 'fields[]=campaign.id' \ --data-urlencode 'fields[]=ad.id' \ --data-urlencode 'fields[]=ad.name' \ --data-urlencode 'fields[]=ad.clicks' \ --data-urlencode 'fields[]=ad.impressions' \ --data-urlencode 'time_ranges[]={"type":"unix_range","start":1777075200,"end":1777161600}' ``` Representative response: ```json { "object": "list", "data": [ { "id": "start=1777075200:end=1777161600:entity_id=ad_501", "start_time": 1777075200, "end_time": 1777161600, "readable_time": "2026-04-25", "campaign_id": "cmpn_101", "ad_id": "ad_501", "ad_name": "Blue shoes", "impressions": 700, "clicks": 22 }, { "id": "start=1777075200:end=1777161600:entity_id=ad_502", "start_time": 1777075200, "end_time": 1777161600, "readable_time": "2026-04-25", "campaign_id": "cmpn_101", "ad_id": "ad_502", "ad_name": "Red shoes", "impressions": 500, "clicks": 14 } ], "count": 2, "first_id": "start=1777075200:end=1777161600:entity_id=ad_501", "last_id": "start=1777075200:end=1777161600:entity_id=ad_502", "has_more": false } ``` `filters[]` removes rows that do not match `campaign.id`. `sort[]` ranks the remaining ads by clicks, and `limit=1` keeps only the top row on the page. ```bash curl -sS -G "https://api.ads.openai.com/v1/ad_account/insights" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" \ --data-urlencode 'time_granularity=none' \ --data-urlencode 'aggregation_level=ad' \ --data-urlencode 'filters[]={"field":"campaign.id","operator":"IN","value":["cmpn_101"]}' \ --data-urlencode 'sort[]={"field":"ad.clicks","direction":"desc"}' \ --data-urlencode 'limit=1' \ --data-urlencode 'fields[]=campaign.id' \ --data-urlencode 'fields[]=ad.id' \ --data-urlencode 'fields[]=ad.name' \ --data-urlencode 'fields[]=ad.clicks' \ --data-urlencode 'fields[]=ad.impressions' \ --data-urlencode 'time_ranges[]={"type":"unix_range","start":1777075200,"end":1777680000}' ``` Representative response: ```json { "object": "list", "data": [ { "id": "start=1777075200:end=1777680000:entity_id=ad_501:sort=clicks.desc:sort_values=126", "start_time": 1777075200, "end_time": 1777680000, "campaign_id": "cmpn_101", "ad_id": "ad_501", "ad_name": "Blue shoes", "impressions": 4200, "clicks": 126 } ], "count": 1, "first_id": "start=1777075200:end=1777680000:entity_id=ad_501:sort=clicks.desc:sort_values=126", "last_id": "start=1777075200:end=1777680000:entity_id=ad_501:sort=clicks.desc:sort_values=126", "has_more": true } ``` For ad accounts with segmented insights and zero-impression product expansion enabled, use a product segment when you need product rows within the selected entity level. This request groups products first, then the ad account, so the response can include one configured product row even when that product had zero impressions. Synthetic zero-product rows omit unavailable metric fields from the response. ```bash curl -sS -G "https://api.ads.openai.com/v1/ad_account/insights" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" \ --data-urlencode 'time_granularity=daily' \ --data-urlencode 'aggregation_level=ad_account' \ --data-urlencode 'segments[]=product' \ --data-urlencode 'override_segment_group_order[]=product' \ --data-urlencode 'override_segment_group_order[]=ad_account' \ --data-urlencode 'includes[]=zero_impression_products' \ --data-urlencode 'fields[]=product.feed_id' \ --data-urlencode 'fields[]=product.item_id' \ --data-urlencode 'fields[]=product.title' \ --data-urlencode 'fields[]=product.impressions' \ --data-urlencode 'fields[]=product.clicks' \ --data-urlencode 'time_ranges[]={"type":"unix_range","start":1777075200,"end":1777161600}' ``` Representative response: ```json { "object": "list", "data": [ { "id": "start=1777075200:end=1777161600:entity_id=v2ad_account_id%3Dadacct_123%7Cproduct_feed_id%3Dfeed_1%7Citem_id%3Dsku_1", "start_time": 1777075200, "end_time": 1777161600, "product_feed_id": "feed_1", "item_id": "sku_1", "product_title": "Blue shoes", "product_impressions": 240, "product_clicks": 9 }, { "id": "start=1777075200:end=1777161600:entity_id=v2ad_account_id%3D%3Cnull%3E%7Cproduct_feed_id%3Dfeed_1%7Citem_id%3Dsku_2", "start_time": 1777075200, "end_time": 1777161600, "product_feed_id": "feed_1", "item_id": "sku_2", "product_title": "Green shoes" } ], "count": 2, "first_id": "start=1777075200:end=1777161600:entity_id=v2ad_account_id%3Dadacct_123%7Cproduct_feed_id%3Dfeed_1%7Citem_id%3Dsku_1", "last_id": "start=1777075200:end=1777161600:entity_id=v2ad_account_id%3D%3Cnull%3E%7Cproduct_feed_id%3Dfeed_1%7Citem_id%3Dsku_2", "has_more": false } ``` --- # Bidding & Budgets Choose your campaign objective and budget, then configure how its ad groups bid. Use the request conventions in [Overview](https://developers.openai.com/ads/api-overview). Examples use an ad account-scoped API key in `${OPENAI_ADS_API_KEY}` and sample IDs such as `cmpn_123` and `adgrp_123`. Replace these IDs with values returned by your requests. If you use a partner key, also include `OpenAI-Ad-Account: ${AD_ACCOUNT_ID}` on requests for the selected account. Budget and bid examples use illustrative USD amounts. Use values appropriate to your account's currency and advertising plan. New resources are created paused so you can finish setup before enabling delivery. ## Choosing a Bid Strategy Choose a strategy based on the control you need over bids and the outcome your campaign optimizes for. | Strategy | Use when | Compatible objectives | Budget | Bid amount | | -------------------- | ------------------------------------------------------------------------ | -------------------------------- | ----------------- | ------------------------ | | Fixed bid | You want to set and adjust the bid yourself | Impressions, clicks, conversions | Daily or lifetime | Provide `max_bid_micros` | | Maximize clicks | You want OpenAI to adjust bids to seek more clicks from your budget | Clicks | Daily | Omit `max_bid_micros` | | Maximize conversions | You want OpenAI to adjust bids to seek more conversions from your budget | Conversions | Daily | Omit `max_bid_micros` | **Maximize Results** is the name for the `maximize_clicks` and `maximize_conversions` strategies. Both are available for standard and product-feed campaigns. ### Campaign Objectives & Billing Your campaign objective determines the outcome to optimize for. The billing event determines what you pay for. | Setting | Configured on | What it controls | | ------------------ | ------------- | ------------------------------------------------------------ | | Campaign objective | Campaign | The outcome to optimize for | | Campaign budget | Campaign | The daily or lifetime spending limit shared by its ad groups | | Billing event | Ad group | Whether you pay for impressions or clicks | | Bid strategy | Ad group | Whether you set bids or let OpenAI adjust them | #### Choose an objective Use the following combinations of campaign objective and ad-group billing and bidding parameters: | Campaign objective (`bidding_type`) | Billing event (`bidding_config.billing_event_type`) | Bid strategy (`bidding_config.strategy`) | | ----------------------------------- | --------------------------------------------------- | ---------------------------------------- | | `impressions` | `impression` | `fixed_bid` | | `clicks` | `click` | `fixed_bid` | | `clicks` | `click` | `maximize_clicks` | | `conversions` | `click` | `fixed_bid` | | `conversions` | `click` | `maximize_conversions` | #### Set up a conversion objective To optimize for actions such as purchases or sign-ups, OpenAI needs conversion events from your site. Set up [conversion tracking](https://developers.openai.com/ads/api-reference/conversion-setup), then select one active standard conversion event setting from the same ad account as the campaign. Custom events cannot be optimization goals. Conversion campaigns optimize for the selected action and bill for valid clicks. The following example creates a paused conversion campaign with an illustrative $200 daily budget for a USD account: ```bash curl -X POST "https://api.ads.openai.com/v1/campaigns" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" \ -H "Idempotency-Key: conversion-campaign-001" \ -H "Content-Type: application/json" \ -d '{ "name": "Trail shoe purchases", "status": "paused", "bidding_type": "conversions", "conversion_event_setting_ids": ["ces_123"], "budget": { "daily_spend_limit_micros": 200000000 }, "targeting": { "locations": { "countries": ["US"] } } }' ``` Replace `ces_123` with your conversion event setting ID and the sample country with your target locations. Save the returned campaign ID to create its ad groups. ### Check compatibility before creating an ad group - Match the strategy to the campaign objective: `maximize_clicks` for `clicks`, or `maximize_conversions` for `conversions`. - Use `billing_event_type: "click"` for both Maximize Results strategies. - Use a daily campaign budget for Maximize Results. - Include `max_bid_micros` for `fixed_bid`; omit it for Maximize Results. - Use audience bid multipliers only with `fixed_bid`. Set `strategy` explicitly so the request expresses your intended behavior. ## Fixed Bids Use `fixed_bid` to supply the bid for an ad group. All ads in the group share its bidding configuration. ### Understand bid amounts `max_bid_micros` uses the same currency scaling as campaign budgets: one major currency unit equals `1000000` micros. Multiply the bid amount by `1,000,000` and send the result as an integer. For example, a $2.50 click bid in a USD account is `2500000`. For impression bidding, the API expects a bid **per impression**. If you work with CPM (cost per thousand impressions), divide by `1,000` first. A $60 CPM bid is $0.06 per impression, so send `60000` micros: `60 ÷ 1,000 × 1,000,000 = 60000`. Bid limits and permitted monetary precision depend on the account currency and campaign objective. ### Create an ad group with a fixed bid First create a campaign with a compatible objective and budget. The following example uses an existing clicks campaign and an illustrative $2 click bid: ```bash curl -X POST "https://api.ads.openai.com/v1/ad_groups" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" \ -H "Idempotency-Key: fixed-bid-ad-group-001" \ -H "Content-Type: application/json" \ -d '{ "campaign_id": "cmpn_123", "name": "Trail running", "status": "paused", "bidding_config": { "billing_event_type": "click", "strategy": "fixed_bid", "max_bid_micros": 2000000 } }' ``` Save the returned ad-group `id`. Confirm that `bidding_config` contains the requested billing event, strategy, and amount. ### Update a fixed bid Send the desired configuration to the ad-group update endpoint. This example changes a clicks ad group's bid to $3 for a USD account: ```bash curl -X POST "https://api.ads.openai.com/v1/ad_groups/adgrp_123" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" \ -H "Content-Type: application/json" \ -d '{ "bidding_config": { "billing_event_type": "click", "strategy": "fixed_bid", "max_bid_micros": 3000000 } }' ``` ### Check delivery guidance Request bidding guidance and serving issues when reviewing delivery: ```bash curl -G "https://api.ads.openai.com/v1/ad_groups/adgrp_123" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" \ --data-urlencode 'include[]=bid_too_low' \ --data-urlencode 'include[]=serving_issues' ``` `bid_too_low: true` indicates that the bid may be too low for reliable delivery. It is guidance, not a serving restriction. Inspect `serving_issues` for reported delivery blockers before deciding whether to change the bid. See the [Ad Groups API reference](https://developers.openai.com/ads/api-reference/ad-groups) for request and response fields. ## Maximize Results Use Maximize Results to let OpenAI adjust bids toward clicks or conversions using your daily campaign budget. Both strategies are available for standard and product-feed campaigns. The API exposes two strategies: | Campaign `bidding_type` | Ad-group `strategy` | Optimization outcome | | ----------------------- | ---------------------- | ---------------------------------------- | | `clicks` | `maximize_clicks` | Clicks | | `conversions` | `maximize_conversions` | The campaign's selected conversion event | Both strategies use `billing_event_type: "click"`. ### Prerequisites - Use a campaign with a daily budget. - For conversions, configure exactly one active standard conversion event setting on the campaign. - Omit `max_bid_micros` and do not configure audience bid multipliers. - Include an `Idempotency-Key` when creating the ad group. It is required for Maximize Results. ### Create an ad group that maximizes clicks Use the ID of a clicks campaign with a daily budget, such as the campaign from Your First Campaign: ```bash curl -X POST "https://api.ads.openai.com/v1/ad_groups" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" \ -H "Idempotency-Key: maximize-clicks-ad-group-001" \ -H "Content-Type: application/json" \ -d '{ "campaign_id": "cmpn_123", "name": "Trail running clicks", "status": "paused", "bidding_config": { "billing_event_type": "click", "strategy": "maximize_clicks" } }' ``` Save the returned ad-group ID and confirm the returned `bidding_config.strategy`. If you retry the same creation, reuse the same `Idempotency-Key` and request body. Use a new key for a different ad group. ### Create an ad group that maximizes conversions Use the same creation endpoint with the ID of your conversion campaign and this `bidding_config` object: ```json { "billing_event_type": "click", "strategy": "maximize_conversions" } ``` Supply a new `Idempotency-Key` for the new ad group. Confirm that the parent campaign has the intended event setting and a daily budget. ## Changing Bid Strategies Update an ad group's `bidding_config` to switch between a fixed bid and Maximize Results. The new strategy must match the parent campaign's objective and budget. ### Inspect the current configuration Retrieve the campaign and ad group before making the change: ```bash curl -G "https://api.ads.openai.com/v1/campaigns/cmpn_123" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" ``` ```bash curl -G "https://api.ads.openai.com/v1/ad_groups/adgrp_123" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" ``` Check the campaign objective, budget type, and the ad group's current bid and audience multipliers. | Change | Required configuration | | --------------------------------- | ---------------------------------------------------------------------------------------------- | | Fixed bid to Maximize clicks | Clicks campaign, daily budget, `strategy: "maximize_clicks"`, no `max_bid_micros` | | Fixed bid to Maximize conversions | Conversions campaign, daily budget, `strategy: "maximize_conversions"`, no `max_bid_micros` | | Maximize Results to fixed bid | `strategy: "fixed_bid"` and an explicit `max_bid_micros` appropriate to the campaign objective | ### Switch to Maximize Results This example switches a clicks ad group to `maximize_clicks` and explicitly clears audience bid multipliers: ```bash curl -X POST "https://api.ads.openai.com/v1/ad_groups/adgrp_123" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" \ -H "Content-Type: application/json" \ -d '{ "bidding_config": { "billing_event_type": "click", "strategy": "maximize_clicks", "custom_audience_bid_multipliers": [] } }' ``` If the campaign uses a lifetime budget, first switch it to a daily budget and confirm the response. Then update the ad group. Switching budget types changes the spending limit that applies to every ad group in the campaign. ### Switch back to a fixed bid Supply the new bid explicitly. This example sets a $2 click bid for a USD clicks campaign: ```bash curl -X POST "https://api.ads.openai.com/v1/ad_groups/adgrp_123" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" \ -H "Content-Type: application/json" \ -d '{ "bidding_config": { "billing_event_type": "click", "strategy": "fixed_bid", "max_bid_micros": 2000000 } }' ``` Previously removed audience multipliers must be included again if you want to restore them. ### Verify the change Inspect the returned `bidding_config` and retrieve the ad group again if needed. Confirm the strategy, billing event, bid amount where applicable, and audience multipliers. The campaign keeps its existing objective and, for conversion campaigns, its selected event. Create a new campaign to change either. Switching between `fixed_bid` and Maximize Results changes how the ad group bids within that objective. ## Campaign Budgets Set a budget on the campaign to control spending across its ad groups. Use separate campaigns when you need separate budgets. ### Choose a budget type | Budget type | Field | Use when | | ----------- | ----------------------------- | ---------------------------------------------------------------------- | | Daily | `daily_spend_limit_micros` | You want a daily spending limit or plan to use Maximize Results | | Lifetime | `lifetime_spend_limit_micros` | You want a total spending limit for the campaign and use fixed bidding | Provide exactly one budget field. A campaign cannot have both a daily and a lifetime budget. ### Understand micros Budget amounts are integers expressed in **micros**, where one micro is one millionth of a major unit of the account's currency. Multiply an amount by `1,000,000` to convert it to micros; divide by `1,000,000` to read it back. For a USD account: - $1.00 = `1000000` micros. - $2.50 = `2500000` micros. - $50.00 = `50000000` micros. The same scaling applies to other account currencies. Send the integer micros value in the request, without a currency symbol or thousands separators. Daily minimums depend on the account currency. Requests below the applicable minimum return an error with the required amount. ### Create a campaign with a daily budget The following example creates a paused clicks campaign with an illustrative $50 daily budget: ```bash curl -X POST "https://api.ads.openai.com/v1/campaigns" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" \ -H "Idempotency-Key: daily-budget-campaign-001" \ -H "Content-Type: application/json" \ -d '{ "name": "Spring launch", "status": "paused", "bidding_type": "clicks", "budget": { "daily_spend_limit_micros": 50000000 }, "targeting": { "locations": { "countries": ["US"] } } }' ``` Save the returned campaign ID. Confirm the budget in the response before adding ad groups. For a lifetime budget, use the following `budget` object in the creation request: ```json { "lifetime_spend_limit_micros": 500000000 } ``` This sets a $500 total campaign budget for a USD account. ### Update a budget amount Send the new amount to the campaign update endpoint. ```bash curl -X POST "https://api.ads.openai.com/v1/campaigns/cmpn_123" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" \ -H "Content-Type: application/json" \ -d '{ "budget": { "daily_spend_limit_micros": 75000000 } }' ``` For a USD account, this sets the daily budget to $75. It does not add $75 to the previous budget. For a lifetime budget, send `lifetime_spend_limit_micros` with the new total. You cannot reduce a lifetime budget below the amount the campaign has already spent. When reducing a daily budget, also review the fixed bids and audience multipliers on its ad groups. The API can reject a budget that is below an ad group's effective bid. ### Switch from a lifetime budget to a daily budget For an existing lifetime-budget campaign, this request sets a $75 daily budget in a USD account: ```bash curl -X POST "https://api.ads.openai.com/v1/campaigns/cmpn_123" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" \ -H "Content-Type: application/json" \ -d '{ "budget": { "daily_spend_limit_micros": 75000000 } }' ``` You cannot switch a daily budget back to a lifetime budget through a campaign update. Create a new campaign if you need a lifetime budget instead. ### Verify the budget and delivery Retrieve the campaign to confirm its budget and inspect reported serving issues: ```bash curl -G "https://api.ads.openai.com/v1/campaigns/cmpn_123" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" \ --data-urlencode 'include[]=serving_issues' ``` Check that `budget` contains the intended budget type and amount. Use Insights to monitor spend after activation. Campaign budgets and ad account spend limits apply independently. Raising a campaign budget does not override an exhausted account spend limit. Delivery also depends on the campaign schedule, resource statuses, reviews, targeting, and available inventory. See the [Campaigns API reference](https://developers.openai.com/ads/api-reference/campaigns) for budget fields and the [Ad Account API reference](https://developers.openai.com/ads/api-reference/ad-account) for account controls. ## Account Budgets An account spend-limit window caps total spending across the account's campaigns during a date range. Campaign budgets continue to apply independently. Spend-limit windows are available only to some accounts. An account limit does not allocate a budget to each campaign. A campaign can have budget remaining while the account's active limit is exhausted. Conversely, removing an account limit does not remove the campaigns' own budgets. See [Spend Limits](https://developers.openai.com/ads/account-management#spend-limits) to create, inspect, update, or delete account spend-limit windows. --- # Bulk API The Bulk API creates or updates campaigns, ad groups, and ads in a single asynchronous job. Submit up to 1,000 operations, poll the job, and inspect the result of each operation. The Bulk API is in limited preview and is enabled per ad account. It isn't included in the downloadable OpenAPI spec. If a bulk endpoint returns `404`, contact your OpenAI account team to confirm access for the account associated with your Ads API key. ## Submit a bulk job Create a campaign, ad group, and ad in one request. The example creates paused resources so you can verify them before delivery starts. `POST /bulk_mutation_jobs` ```bash curl -X POST "https://api.ads.openai.com/v1/bulk_mutation_jobs" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: spring-launch-job-001" \ -d '{ "validate_only": false, "partial_failure": true, "operations": [ { "operation_id": "create-campaign", "type": "campaign.create", "idempotency_key": "campaign-spring-launch", "input": { "name": "Spring launch", "max_budget_micros": 100000000, "billing_event_type": "impression", "budget_type": "lifetime", "status": "paused" } }, { "operation_id": "create-ad-group", "type": "ad_group.create", "idempotency_key": "ad-group-prospecting", "input": { "campaign_idempotency_key": "campaign-spring-launch", "name": "Prospecting", "context_hints": ["shoes", "spring fashion"], "status": "paused" } }, { "operation_id": "create-ad", "type": "ad.create", "idempotency_key": "ad-prospecting-1", "input": { "campaign_idempotency_key": "campaign-spring-launch", "ad_group_idempotency_key": "ad-group-prospecting", "title": "Fresh shoes", "body": "Find your next pair", "target_url": "https://example.com/shoes", "source_image_url": "https://developers.openai.com/showcase/openai-imagegen-demo.png", "status": "paused" } } ] }' ``` The API returns `202 Accepted` with a job ID: ```json { "id": "blkmtnjob_6a2b773d47b481908aa6078025a64ad3", "status": "pending", "operation_count": 3, "created_at": 1784304000, "completed_at": null } ``` Use the Ads API key from the Settings tab in [Ads Manager](https://ads.openai.com). Each key works with one ad account, so don't add an `OpenAI-Ad-Account` header when using an API key. ### Request fields | Field | Type | Required | Description | | ----------------- | -------- | -------- | --------------------------------------------------------------------------------------------------------- | | `operations` | object[] | Yes | Between `1` and `1,000` create or update operations. | | `validate_only` | boolean | No | Validates request fields and dependencies without changing ad resources when `true`. Defaults to `false`. | | `partial_failure` | boolean | No | Continues independent operations after an error when `true`. Defaults to `true`. | Set `partial_failure` to `false` to skip later operations after an operation fails. This setting doesn't roll back operations that already completed. Validation-only jobs don't guarantee that operations can complete successfully. They don't check update-target existence, image fetching, entity limits, or other write-time errors. The optional `Idempotency-Key` header makes it safe to retry an uncertain request with the same body. Reusing the header with a different body returns an error. To rerun a `failed` or `partially_failed` job, submit the same body with a new request-level key. Successful creates are reused. ## Supported operations Each entry in `operations` must include a unique `operation_id`, an operation `type`, and an `input` object. Create operations require a unique `idempotency_key`. Update operations require `target_resource_id` and at least one input field. | Type | Required input | Other supported input | | ----------------- | --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | | `campaign.create` | `name`, `max_budget_micros` | `billing_event_type`, `budget_type`, `status`, `target_countries`, `location_ids` | | `campaign.update` | At least one supported field | `name`, `description`, `status`, `max_budget_micros`, `budget_type`, `start_time`, `end_time`, `location_ids` | | `ad_group.create` | `campaign_idempotency_key`, `name` | `context_hints`, `exclusion_hints`, `max_bid_micros`, `max_cpm_bid_micros`, `status` | | `ad_group.update` | At least one supported field | `name`, `description`, `status`, `context_hints`, `exclusion_hints`, `max_bid_micros`, `max_cpm_bid_micros` | | `ad.create` | `campaign_idempotency_key`, `ad_group_idempotency_key`, `title`, `body`, `target_url`, `source_image_url` | `status` | | `ad.update` | At least one supported field | `name`, `status`, `creative` | Create operations can refer to parents created in the same job. Set `campaign_idempotency_key` to the campaign operation's `idempotency_key`, and set `ad_group_idempotency_key` to the ad group operation's `idempotency_key`. The campaign reference on `ad.create` must match the campaign reference on its parent `ad_group.create` operation. You can mix create and update operations in one job. Updates can target only resources that exist when you submit the job, so you can't update a resource created in the same job. Update each resource only once in a job. Create statuses are `active` or `paused`; update statuses also support `archived`. `campaign.create` defaults to an impression-billed, lifetime, paused campaign. Its budget must be at least `1000000` currency micros. Ad-group bids must match the parent campaign's billing event. Provide only one of `max_bid_micros` for clicks or `max_cpm_bid_micros` for impressions; CPM requires account access. Campaign and ad-group names allow `3` to `1,000` characters. Ad titles allow `3` to `50`, bodies allow up to `100`, and URLs allow up to `2,048` characters. Campaigns support up to `2,500` location IDs, and ad groups support up to `2,000` context hints. See [Location Targeting](https://developers.openai.com/ads/location-targeting) for location IDs. When updating an ad creative, include `title`, `body`, `target_url`, and `file_id`. For example, pause an existing ad: ```json { "operations": [ { "operation_id": "pause-ad", "type": "ad.update", "target_resource_id": "ad_501", "input": { "status": "paused" } } ] } ``` ## Retrieve a job Poll the job ID returned by the create request until the job reaches a terminal status. `GET /bulk_mutation_jobs/{job_id}` ```bash curl -X GET \ "https://api.ads.openai.com/v1/bulk_mutation_jobs/blkmtnjob_6a2b773d47b481908aa6078025a64ad3" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" ``` | Status | Meaning | | ------------------ | ---------------------------------------------------------------------------- | | `pending` | The job is waiting to run. | | `in_progress` | The job is processing operations. | | `completed` | All operations completed successfully. | | `partially_failed` | At least one operation succeeded and another returned `failed` or `skipped`. | | `failed` | No operations succeeded. Inspect the operation results for details. | `completed`, `partially_failed`, and `failed` are terminal statuses. ## List operation results Retrieve the result of each operation after submitting a job. `GET /bulk_mutation_jobs/{job_id}/operations` Set `limit` to between `1` and `100` results per page. It defaults to `100`. ```bash curl -X GET \ "https://api.ads.openai.com/v1/bulk_mutation_jobs/blkmtnjob_6a2b773d47b481908aa6078025a64ad3/operations?limit=100" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" ``` ```json { "object": "list", "data": [ { "operation_id": "create-ad", "type": "ad.create", "status": "created", "resource_id": "ad_501", "submitted_version_id": "adver_501", "error_code": null, "error": null, "retryable": null, "retry_after_seconds": null } ], "has_more": false, "complete": true, "error": null } ``` Use `has_more` and the last returned `operation_id` to request the next page with `after`. Pagination cursors are only available after `complete` is `true`. While a job is running, the endpoint can return an incomplete snapshot of the results collected so far. Each operation result includes its `operation_id`, `type`, status, and error fields that can be `null`. `failed` results can populate `error_code`, `error`, `retryable`, and `retry_after_seconds`. The top-level `error` field describes a job-level error when present. `submitted_version_id` is `null` for campaign and ad-group creates. | Status | Meaning | | ----------- | ------------------------------------------------------------------------------------- | | `created` | The create operation succeeded. | | `updated` | The update operation succeeded. | | `validated` | The operation passed request and dependency validation in a validation-only job. | | `failed` | The operation returned an error. Use the retry fields for next steps. | | `skipped` | The operation didn't run because a dependency or earlier operation returned an error. | ## Limits and retries Bulk jobs have the following default limits: | Limit | Value | | ----------------------------------- | ------------------------------ | | Operations per job | `1,000` | | Request body size | `16 MiB` | | Serialized operation size | `512 KiB` | | Create requests per ad account | `10` requests per `10` seconds | | Operation results per page | `100` | | Self-serve campaigns per ad account | `5,000` non-archived campaigns | | Self-serve ad groups per ad account | `5,000` non-archived ad groups | | Self-serve ads per ad account | `5,000` active or paused ads | Keep `operation_id` and create-operation `idempotency_key` values unique within a job. Each value can contain up to `255` characters. If a result's `retryable` field is `true`, wait for `retry_after_seconds` when provided before submitting the same body in a new job. Reuse the original create-operation `idempotency_key` values when retrying that request. --- # Campaign Management ## Campaigns A campaign controls the objective, budget, schedule, and targeting shared by its ad groups. Create the campaign first, then add ad groups and ads. ### Create a campaign ```bash curl -X POST "https://api.ads.openai.com/v1/campaigns" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" \ -H "Idempotency-Key: campaign-create-001" \ -H "Content-Type: application/json" \ -d '{ "name": "Spring launch", "status": "paused", "bidding_type": "clicks", "budget": { "daily_spend_limit_micros": 50000000 }, "targeting": { "locations": { "countries": [ "US" ] } }, "landing_page_configuration": { "query_string_template": "utm_source=openai&utm_medium=paid&utm_campaign={campaign_id}&utm_content={ad_id}" } }' ``` Save the returned campaign `id`. Set `bidding_type` explicitly: `impressions`, `clicks`, or `conversions`. A conversions campaign also needs an eligible conversion event setting. ### Retrieve and list ```bash curl -G "https://api.ads.openai.com/v1/campaigns/cmpn_123" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" ``` Name lookup is exact and case-insensitive and can return multiple matches. Store IDs as the durable identifiers for your integration. ```bash curl -G "https://api.ads.openai.com/v1/campaigns" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" \ --data-urlencode 'name=Spring launch' ``` ### Update a campaign ```bash curl -X POST "https://api.ads.openai.com/v1/campaigns/cmpn_123" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" \ -H "Content-Type: application/json" \ -d '{ "name": "Spring launch — updated", "budget": { "daily_spend_limit_micros": 75000000 } }' ``` Retrieve the current resource before editing nested settings or arrays. Build the complete desired targeting or conversion-event list when changing those fields so you preserve values you intend to keep. ### Change campaign status Use `POST /v1/campaigns/{campaign_id}/activate`, `/pause`, or `/archive`. For example: ```bash curl -X POST "https://api.ads.openai.com/v1/campaigns/cmpn_123/pause" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" ``` A campaign pause prevents its child ads from serving. ## Ad Groups Ad groups apply a shared bid configuration and context hints to a group of ads. ### Create an ad group ```bash curl -X POST "https://api.ads.openai.com/v1/ad_groups" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" \ -H "Idempotency-Key: ad-group-create-001" \ -H "Content-Type: application/json" \ -d '{ "campaign_id": "cmpn_123", "name": "Trail running", "status": "paused", "context_hints": [ "Lightweight trail shoes for rocky terrain" ], "bidding_config": { "billing_event_type": "click", "strategy": "fixed_bid", "max_bid_micros": 2000000 } }' ``` The campaign must belong to the selected ad account. Save the returned `id` for ad creation and future updates. Set the strategy explicitly. Fixed bidding requires `max_bid_micros`; Maximize Results requires omitting it. ### Retrieve and list ```bash curl -G "https://api.ads.openai.com/v1/ad_groups/adgrp_123" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" ``` ```bash curl -G "https://api.ads.openai.com/v1/ad_groups" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" \ --data-urlencode 'campaign_id=cmpn_123' \ --data-urlencode 'name=Trail running' ``` ### Update an ad group ```bash curl -X POST "https://api.ads.openai.com/v1/ad_groups/adgrp_123" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" \ -H "Content-Type: application/json" \ -d '{ "name": "Trail running — updated", "context_hints": [ "Lightweight trail shoes for rocky terrain", "Water-resistant shoes for wet trails" ] }' ``` `context_hints` replaces the current list. Include every hint you want to keep. Omitting `bidding_config` preserves the current bid configuration; when supplying it, include its required fields. Use `/activate`, `/pause`, and `/archive` on the ad-group URL to change its state. An active ad group still depends on an active parent campaign and eligible ads. ## Ads & Creative An ad supplies the content and destination shown to the user. A standard `chat_card` uses your title, body, image, and landing-page URL. ### Upload an image ```bash curl -X POST "https://api.ads.openai.com/v1/upload" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" \ -F "file=@/path/to/product-image.png" ``` Alternatively, send JSON containing `image_url` to the same endpoint. Use JPEG, PNG, or WebP and provide an image at least 640 × 640 pixels. Save the returned `file_id`. ### Create a chat-card ad ```bash curl -X POST "https://api.ads.openai.com/v1/ads" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" \ -H "Idempotency-Key: ad-create-001" \ -H "Content-Type: application/json" \ -d '{ "ad_group_id": "adgrp_123", "name": "Trail shoe launch", "status": "paused", "creative": { "type": "chat_card", "title": "Find your next trail shoe", "body": "Explore shoes made for your next outdoor run.", "target_url": "https://example.com/trail-shoes", "file_id": "file_123" } }' ``` Use a title of 3–50 characters and a body no longer than 100 characters. The destination must be an HTTP or HTTPS URL no longer than 2,048 characters and accessible to OpenAI's ad crawlers. The file and ad group must belong to the selected account. ### Review and update ```bash curl -G "https://api.ads.openai.com/v1/ads/ad_123" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" ``` Inspect `status`, `review_status`, and `review`. Updating creative creates a new submitted version and starts another review: ```bash curl -X POST "https://api.ads.openai.com/v1/ads/ad_123" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" \ -H "Content-Type: application/json" \ -d '{ "creative": { "type": "chat_card", "title": "Ready for your next trail?", "body": "Explore our latest trail running collection.", "target_url": "https://example.com/trail-shoes", "file_id": "file_123" } }' ``` The example sends the complete intended creative. The ad cannot be moved to another ad group. Use `/activate`, `/pause`, and `/archive` on the ad to change its state. An active ad still depends on its campaign and ad group. ### Tracking parameters Add `landing_page_configuration.query_string_template` at the campaign, ad-group, or ad level. For example: ```json { "landing_page_configuration": { "query_string_template": "utm_source=openai&utm_campaign={campaign_id}&utm_content={ad_id}&click_id={oppref}" } } ``` Parameters from different levels combine. For a duplicate parameter, precedence is: existing destination URL, ad, ad group, campaign, then ad account. ## Ad Previews Check the creative, destination, review status, account reviews, targeting, and budget. A preview shows appearance; it does not confirm serving eligibility. ```bash curl -X POST "https://api.ads.openai.com/v1/ads/ad_123/preview" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" ``` ```bash curl -G "https://api.ads.openai.com/v1/ads/ad_123" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" \ --data-urlencode 'include[]=serving_issues' ``` ## Bulk Operations Use a bulk mutation job to create or update many campaigns, ad groups, and ads asynchronously. A job can contain up to 1,000 operations and a request body up to 16 MiB. ### Submit updates This example validates a campaign budget change and an ad-group pause without applying them. Change `validate_only` to `false` and use a new job key when you are ready to apply the changes. ```bash curl -X POST "https://api.ads.openai.com/v1/bulk_mutation_jobs" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" \ -H "Idempotency-Key: bulk-validation-001" \ -H "Content-Type: application/json" \ -d '{ "validate_only": true, "partial_failure": true, "operations": [ { "operation_id": "update-budget", "type": "campaign.update", "target_resource_id": "cmpn_123", "input": { "max_budget_micros": 150000000 } }, { "operation_id": "pause-ad-group", "type": "ad_group.update", "target_resource_id": "adgrp_123", "input": { "status": "paused" } } ] }' ``` A successful submission returns HTTP 202. Save the job ID. The bulk `input` format differs from the single-resource API: for example, the bulk campaign input uses `max_budget_micros`. Do not paste a single-resource body into a bulk operation without checking its schema. ### Create a hierarchy Supported operation types are `campaign.create`, `campaign.update`, `ad_group.create`, `ad_group.update`, `ad.create`, and `ad.update`. Each create operation has its own `idempotency_key`. Create an ad group's parent campaign in the same job and reference its key through `campaign_idempotency_key`. An ad also references the same-job parent ad group through `ad_group_idempotency_key`. Each operation needs a unique `operation_id`. The following validates a complete hierarchy. Substitute your real destination and publicly accessible image URL before applying it: ```bash curl -X POST "https://api.ads.openai.com/v1/bulk_mutation_jobs" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" \ -H "Idempotency-Key: bulk-hierarchy-validation-001" \ -H "Content-Type: application/json" \ -d '{ "validate_only": true, "partial_failure": true, "operations": [ { "operation_id": "create-campaign", "type": "campaign.create", "idempotency_key": "bulk-campaign-001", "input": { "name": "Catalog launch", "max_budget_micros": 100000000, "billing_event_type": "click", "budget_type": "lifetime", "status": "paused", "target_countries": [ "US" ] } }, { "operation_id": "create-ad-group", "type": "ad_group.create", "idempotency_key": "bulk-ad-group-001", "input": { "campaign_idempotency_key": "bulk-campaign-001", "name": "Trail running", "max_bid_micros": 2000000, "status": "paused" } }, { "operation_id": "create-ad", "type": "ad.create", "idempotency_key": "bulk-ad-001", "input": { "campaign_idempotency_key": "bulk-campaign-001", "ad_group_idempotency_key": "bulk-ad-group-001", "title": "Explore trail running shoes", "body": "Find your next pair for the trail.", "target_url": "https://example.com/trail-shoes", "source_image_url": "https://example.com/images/trail-shoes.jpg", "status": "paused" } } ] }' ``` ### Poll the job and read all results ```bash curl -G "https://api.ads.openai.com/v1/bulk_mutation_jobs/JOB_ID" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" ``` Wait for `completed`, `partially_failed`, or `failed`. Then retrieve operation outcomes: ```bash curl -G "https://api.ads.openai.com/v1/bulk_mutation_jobs/JOB_ID/operations" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" \ --data-urlencode 'limit=100' ``` Results distinguish `created`, `updated`, `validated`, `failed`, and `skipped`. Read every page. After the job finishes, use the last operation's `operation_id` as `after` when `has_more` is true. ### Handle partial failure and retries With `partial_failure: true`, unrelated operations can continue after a failure. With `false`, remaining operations are skipped after a failure. Neither mode rolls back successful operations. Failed dependencies cause dependent operations to be skipped. Retry a network-uncertain submission with the same job key and body. The same key with a different body returns a conflict. To retry a finished failed job, follow the operation results, retain create-operation keys, and use a new job key. Respect `retryable` and `retry_after_seconds` when returned. See the [Bulk API guide](https://developers.openai.com/ads/bulk-api) for additional operation schemas, limits, and examples. --- # Targeting ## Inclusion & Exclusion Campaign targeting determines who is eligible to receive the campaign's ads. Geographic, platform, and audience settings work together; matching one setting does not override the others. You may configure both inclusion and exclusion targeting on the same campaign. Exclusions take precedence when someone belongs to both an included audience and an excluded audience. The same audience ID cannot appear in both lists in one campaign. ### Configure inclusion and exclusion This example targets US users in one audience while excluding a second audience: ```bash curl -X POST "https://api.ads.openai.com/v1/campaigns/cmpn_123" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" \ -H "Content-Type: application/json" \ -d '{ "targeting": { "locations": { "countries": [ "US" ] }, "custom_audiences": { "ids": [ "caud_123" ] }, "excluded_custom_audiences": { "ids": [ "caud_456" ] } } }' ``` Before sending the update, retrieve the campaign and preserve any other geographic, audience, or platform settings you want to keep. In particular, geographic and audience updates can replace existing targeting criteria. ### Inclusion is different from a bid multiplier Including an audience restricts eligibility to matching users. An audience bid multiplier adjusts a fixed bid for matching users while leaving nonmembers eligible under the campaign's targeting. For example, a campaign with a multiplier for existing customers can still reach new customers. A campaign that includes only the existing-customer audience cannot. ## Geographic Targeting Set geographic targeting on the campaign. You can use country codes or supported location IDs returned by the geographic lookup endpoint. See [Location Targeting](https://developers.openai.com/ads/location-targeting) for more examples. ### Find a location ID ```bash curl -G "https://api.ads.openai.com/v1/geo_lookup/search" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" \ --data-urlencode 'q=California' \ --data-urlencode 'limit=10' ``` Use the returned `id` when targeting the location. The name and other metadata help you choose the correct result, but the ID identifies the target. ### Target a country and exclude a region ```bash curl -X POST "https://api.ads.openai.com/v1/campaigns/cmpn_123" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" \ -H "Content-Type: application/json" \ -d '{ "targeting": { "locations": { "countries": [ "US" ] }, "excluded_locations": { "include": [ { "id": "2000043" } ] } } }' ``` The location ID here is the California example. Look up the locations you intend to use. Check the [geographic target catalog](https://ads.openai.com/assets/openai-geotargets.csv) for supported IDs. ### Country codes Country codes use ISO 3166-1 alpha-2 format, such as `US`. A code's ISO validity does not mean it is available for advertising in your account; use supported advertising locations. For a specific geographic inclusion, use `targeting.locations.include` with the desired location IDs. For exclusions, use `targeting.excluded_locations.countries` or `.include`. ### Preserve existing targeting Read the campaign before changing geography. Include the complete intended geographic and audience configuration so an update does not remove restrictions you want to keep. Explicitly set platform targeting when changing it. You can include up to 2,500 IDs in geographic inclusion and exclusion lists. Account availability and campaign mode can impose additional restrictions. ### Product-feed campaigns Use country-level geographic inclusion and exclusion for product-feed campaigns. Verify account support before relying on more granular locations. If the API accepts the configuration but delivery is low, consider the combined effect of geography, audiences, platform, bids, and available products. ## Platform Targeting Use platform targeting to choose the ChatGPT surfaces where a campaign can deliver. Configure it on the campaign through `targeting.platforms.included`. See [Platform Targeting](https://developers.openai.com/ads/platform-targeting) for more examples. Use these broad platform groups: | Value | Inventory | | ------------- | --------------------------------------------- | | `ios_app` | ChatGPT iOS app | | `android_app` | ChatGPT Android app | | `web` | ChatGPT web, including desktop and mobile web | ### Target web ```bash curl -X POST "https://api.ads.openai.com/v1/campaigns/cmpn_123" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" \ -H "Content-Type: application/json" \ -d '{ "targeting": { "platforms": { "included": [ "web" ] } } }' ``` This selects the web group. It does not mean desktop-only traffic. An iPhone user visiting ChatGPT in a browser belongs to web inventory rather than the iOS app group. ### Target both mobile apps ```bash curl -X POST "https://api.ads.openai.com/v1/campaigns/cmpn_123" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" \ -H "Content-Type: application/json" \ -d '{ "targeting": { "platforms": { "included": [ "ios_app", "android_app" ] } } }' ``` Use a nonempty list. To express all three broad groups explicitly, include `ios_app`, `android_app`, and `web`. At creation, omitting platform targeting defaults to all platforms. ### Update behavior The API handles a platform-only update separately from geographic and audience changes. The examples above change only the platform selection. If you also send geographic or audience fields, build the complete desired configuration for those fields. Retrieve the campaign after updating and check the returned platform configuration. ## Custom Audiences Custom audiences match your customer or prospect identifiers to users. Use ready audiences for campaign inclusion, exclusion, or fixed-bid multipliers. See the [Custom Audiences guide](https://developers.openai.com/ads/custom-audiences) for identifier normalization, file formats, and additional operations. ### Create an audience from a file Prepare a UTF-8 CSV with a header row. For example: ```text email,phone_number customer@example.com,+14155552671 ``` Audience uploads support email, phone, their supported SHA-256 variants, and GAID identifiers. Use the audience-specific normalization rules before hashing; do not assume another API uses the same phone normalization. Files must be no larger than 500,000,000 bytes. Upload the file using the plural `/v1/uploads` endpoint: ```bash curl -X POST "https://api.ads.openai.com/v1/uploads" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" \ -F "file=@/path/to/audience.csv;type=text/csv" \ -F "purpose=custom_audience" ``` Save the file ID, filename, MIME type, and exact size. Substitute those values below: ```bash curl -X POST "https://api.ads.openai.com/v1/custom_audiences" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" \ -H "Content-Type: application/json" \ -d '{ "name": "High-value customers", "file_id": "oaisdmntci_123", "identifier_resolution": "auto", "filename": "audience.csv", "mimetype": "text/csv", "file_size": 123456 }' ``` `identifier_resolution: "auto"` processes supported columns in a CSV. For a single-type TXT file, provide `identifier_type` and one identifier per line. ### Wait for readiness ```bash curl -G "https://api.ads.openai.com/v1/custom_audiences/caud_123" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" ``` Wait for `status: "ready"`. Uploaded row counts do not equal matched-user counts. Inclusion and bid multipliers require at least 25,000 matched users; ready exclusion audiences have no minimum. Review any processing failure before using the audience. You can create an empty audience with just `name` and optional `description`, then add members. Even an empty audience is processed asynchronously. ### Add or remove members Update an existing audience without changing its ID or the campaigns and ad groups that reference it. | Action | Endpoint | Input | | ------------------------------------------ | ---------------------------------------- | ---------------------------------------- | | Add members while keeping existing members | `POST /v1/custom_audiences/{id}/add` | Inline identifiers or an uploaded file | | Remove selected members | `POST /v1/custom_audiences/{id}/remove` | Inline identifiers or an uploaded file | | Replace the complete membership list | `POST /v1/custom_audiences/{id}/replace` | An uploaded file; see Replace membership | Use a new `Idempotency-Key` for each new add, remove, replace, or merge request. When retrying the same submission, reuse its original key and request body. Resuming or canceling an accepted operation by ID does not require a key or request body. #### Add members inline For a small update, send identifiers directly in the request. Inline request bodies must not exceed 16 MiB. ```bash curl -X POST "https://api.ads.openai.com/v1/custom_audiences/caud_123/add" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" \ -H "Idempotency-Key: audience-add-inline-001" \ -H "Content-Type: application/json" \ -d '{ "identifiers": [ { "identifier_type": "email", "identifier": "customer@example.com" } ] }' ``` To remove members inline, send the same input structure to `/v1/custom_audiences/caud_123/remove`, with the identifiers to remove and a new `Idempotency-Key`. #### Add members from a file Use a file when you have a batch of members to add. This adds members to the existing audience; it does not replace the audience's complete membership list. **1. Prepare the file.** Create a UTF-8 CSV with a header row and the identifiers you want to add. For example, save this as `audience-additions.csv`: ```text email new-customer-1@example.com new-customer-2@example.com ``` Use the same supported identifier formats and file-size limit described in “Create an audience from a file.” **2. Upload the file.** Replace the local path with your file's location: ```bash curl -X POST "https://api.ads.openai.com/v1/uploads" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" \ -F "file=@/path/to/audience-additions.csv;type=text/csv" \ -F "purpose=custom_audience" ``` Save the `file_id` returned by the upload. Uploading the file alone does not update the audience. **3. Add the uploaded members.** Replace `caud_123` with your existing audience ID and `oaisdmntci_456` with the returned file ID: ```bash curl -X POST "https://api.ads.openai.com/v1/custom_audiences/caud_123/add" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" \ -H "Idempotency-Key: audience-add-file-001" \ -H "Content-Type: application/json" \ -d '{ "file_id": "oaisdmntci_456", "identifier_resolution": "auto" }' ``` For this file-based request, use `file_id` instead of an inline `identifiers` array. You do not need to resend `filename`, `mimetype`, or `file_size` in the membership-update request. `identifier_resolution: "auto"` processes supported identifier columns in a CSV. For a single-type TXT file, use `identifier_type` instead, such as `"identifier_type": "email"`, and put one identifier on each line. Example response: ```json { "operation_id": "caudop_123", "custom_audience_id": "caud_123", "operation": "add", "status": "processing" } ``` Save the returned `operation_id` and check completion as shown below. #### Remove members from a file Prepare and upload a file containing the identifiers you want to remove, using the same upload process. Then send its returned `file_id` to `/remove`: ```bash curl -X POST "https://api.ads.openai.com/v1/custom_audiences/caud_123/remove" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" \ -H "Idempotency-Key: audience-remove-file-001" \ -H "Content-Type: application/json" \ -d '{ "file_id": "oaisdmntci_789", "identifier_resolution": "auto" }' ``` Replace `oaisdmntci_789` with the ID of your removal file. This removes the specified members; other members remain in the audience. #### Check update completion Add, remove, and replace requests return an operation rather than the updated audience. Use the returned `operation_id` to check its status: ```bash curl -G "https://api.ads.openai.com/v1/custom_audiences/caud_123/operations/caudop_123" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" ``` Example completed response: ```json { "operation_id": "caudop_123", "custom_audience_id": "caud_123", "operation": "add", "status": "succeeded" } ``` While the operation is `processing`, check again until it completes. An accepted request does not mean the membership update has finished, even if the audience itself still has a status of `ready`. Review any operation failure before treating the update as complete. You can optionally include the audience's current `membership_revision` as `expected_revision` in an add or remove request to prevent applying the update against a different membership revision. Replacement requires this field. ### Manage audience operations List accepted operations to find an operation ID, then use that ID to check progress, resume an interrupted add or remove, or cancel it before membership changes begin. For OAuth, listing and polling require `ads.admin.all.read`; resuming and canceling require both `ads.admin.all.read` and `ads.admin.all.write`. #### Find an operation ID The operation list includes retained add, remove, replace, and merge operations. It does not include the initial audience creation request. For a merge, use the new audience ID: ```bash curl -G "https://api.ads.openai.com/v1/custom_audiences/caud_123/operations" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" \ --data-urlencode 'limit=20' ``` Set `limit` from 1 to 100. When `has_more` is `true`, pass `next_cursor` unchanged as `cursor` on the next request for the same audience and ad account. Continue until `has_more` is `false`, even if a page has an empty `data` array. The list returns operation IDs, audience IDs, operation types, and statuses; it does not return original inputs or idempotency keys. #### Resume an add or remove If polling returns `409 custom_audience_operation_recovery_required`, resume the interrupted operation using its original inputs and saved progress. Confirm its ID from the saved response or operation list, then send no request body or `Idempotency-Key` header: ```bash curl -X POST "https://api.ads.openai.com/v1/custom_audiences/caud_123/operations/caudop_123/resume" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" ``` Poll the same operation afterward. Resume preserves its ID and accepted input. An operation that already succeeded or failed keeps that status; resuming it can finish cleanup without applying membership changes again. Resume by ID supports add and remove only. Retry replace and merge requests with their original endpoint, body, and idempotency key. An interrupted add or remove may have partially applied changes, so do not submit a new operation or an inverse update to guess at recovery. See [Poll and recover membership operations](https://developers.openai.com/ads/custom-audiences#poll-and-recover-membership-operations). #### Cancel an add or remove Cancel an accepted add or remove before it starts applying membership changes. Send no request body or `Idempotency-Key` header: ```bash curl -X POST "https://api.ads.openai.com/v1/custom_audiences/caud_123/operations/caudop_123/cancel" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" ``` A successful cancellation returns `status: "failed"`; there is no separate `canceled` status. Cleanup can continue asynchronously. Canceling again is safe, and resuming a canceled operation can finish cleanup without applying membership changes. Cancellation returns `409 custom_audience_mutation_conflict` if an active operation has started applying changes or the operation already succeeded. A `processing` status alone does not guarantee that cancellation is possible. Failed operations stay failed, and cancellation does not undo changes already applied. Replace and merge operations cannot be canceled. If cancellation returns `503 custom_audience_operation_unavailable`, retry cancellation for the same operation ID. The operation may have stopped even though scheduling cleanup failed. See [Cancel an Add or Remove](https://developers.openai.com/ads/custom-audiences#cancel-an-add-or-remove). ### Replace membership Retrieve the audience's `membership_revision`, upload the replacement file, and submit: ```bash curl -X POST "https://api.ads.openai.com/v1/custom_audiences/caud_123/replace" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" \ -H "Idempotency-Key: audience-replace-001" \ -H "Content-Type: application/json" \ -d '{ "file_id": "oaisdmntci_456", "identifier_resolution": "auto", "expected_revision": 2 }' ``` Use the actual current revision. Replacement keeps the audience ID and the old membership remains in use while processing. Changes must preserve size requirements for campaigns or ad groups using the audience. Use a new `Idempotency-Key` for each replacement and reuse its original key and request body for retries. To archive an unused audience, call `/v1/custom_audiences/{id}/archive`; audience archival cannot be undone. ### Audience readiness Use ready audiences. Inclusion and bid multipliers require at least 25,000 matched users; exclusion audiences have no minimum matched count. When combining inclusion and exclusion, the remaining included audience must satisfy the required size. Matched users are different from uploaded rows. Check the processed audience before applying it. See [Custom Audiences](#custom-audiences). ## Context Hints Context hints provide additional information about the ads in an ad group. Use them to describe relevant products, use cases, or needs that the creative and landing page may not fully cover. Hints are not exact-match keywords. They also do not replace explicit geographic, platform, or audience targeting. ### Write useful hints Prefer specific context that helps explain the offering: | Less useful | More useful | | -------------------- | --------------------------------------------------------------------------------- | | Shoes | Lightweight trail running shoes for rocky terrain | | Outdoors | Water-resistant footwear for wet-weather hiking | | California customers | Use geographic targeting for California; describe the product's use case in hints | These examples illustrate writing style, not a guarantee that a particular conversation will trigger an ad. ### Set hints on an ad group ```bash curl -X POST "https://api.ads.openai.com/v1/ad_groups/adgrp_123" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" \ -H "Content-Type: application/json" \ -d '{ "context_hints": [ "Lightweight trail running shoes for rocky terrain", "Water-resistant footwear for wet-weather hiking" ] }' ``` An ad group can contain up to 2,000 hints. You do not need to fill the limit; include information that helps describe the ads accurately. ### Preserve or replace the list Updating `context_hints` replaces the current list. To add one hint, retrieve the ad group, append the new hint to the existing list, and submit the complete intended list. Send an empty array to clear the list. Omit the field when you want to leave it unchanged. Retrieve the ad group after the update and confirm the saved list. If you need to restrict delivery to a geographic area or audience, configure that restriction on the campaign separately. --- # Conversion-Optimized Campaigns 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. In the Ads API, oCPC uses `bidding_type: "conversions"`. oCPC is in open beta for both standard and product-feed campaigns. Use the same campaign and ad-group endpoints to optimize for conversions while continuing to pay per valid click. Choose the campaign goal that matches the outcome you want: | Goal | Best for | How you pay | What delivery optimizes for | | -------------------- | ------------------------------ | ----------------------------------- | ------------------------------------------------------------ | | `impressions` (CPM) | Reach and awareness | Per 1,000 impressions | Broad delivery at scale | | `clicks` (CPC) | Engagement and traffic | Per valid click | Clicks from people likely to engage | | `conversions` (oCPC) | A tracked action after a click | Per valid click, not per conversion | Clicks more likely to lead to your selected conversion event | ## Before you begin Before you create an oCPC campaign, make sure: - The ad account supports conversion bidding. If campaign creation returns `403` with `Conversion bidding is not enabled`, contact your OpenAI partner representative. - You have set up conversion tracking with the [JavaScript Pixel](https://developers.openai.com/ads/measurement-pixel), the [Conversions API](https://developers.openai.com/ads/conversions-api), or both. The Conversions API is a more reliable tracking source than the pixel alone. - You have exactly one active [standard conversion event](https://developers.openai.com/ads/supported-events) to use as the optimization goal. Custom events cannot be oCPC optimization goals. - The conversion event setting belongs to the current ad account and connects to one active conversion source. Product-feed conversion bidding is available in open beta. Each oCPC campaign uses one selected conversion event, and you cannot change the goal or event after campaign creation. ## Create a conversion-optimized campaign Create the campaign as `paused` while you add and check its ad groups and ads. ```bash curl -X POST "https://api.ads.openai.com/v1/campaigns" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Acme purchases", "status": "paused", "budget": { "lifetime_spend_limit_micros": 250000000 }, "bidding_type": "conversions", "conversion_event_setting_ids": ["ces_123"] }' ``` Replace `ces_123` with the active conversion event setting ID that represents your goal, such as `order_created`, `lead_created`, or `registration_completed`. See [Conversion Setup](https://developers.openai.com/ads/api-reference/conversion-setup) to create and manage event settings. For a product-feed campaign, use the same `POST /campaigns` endpoint. Include `mode: "product_feed"`, the ID of a product feed linked to the ad account, and the same conversion bidding fields: ```bash curl -X POST "https://api.ads.openai.com/v1/campaigns" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Running shoes catalog purchases", "status": "paused", "mode": "product_feed", "product_feed_id": "product_feed_123", "budget": { "lifetime_spend_limit_micros": 250000000 }, "bidding_type": "conversions", "conversion_event_setting_ids": ["ces_123"] }' ``` Create each child ad group with `billing_event_type` set to `click`. For an oCPC campaign, `max_bid_micros` is the CPA bid even though billing uses valid clicks. For example, `100000000` is a $100.00 CPA bid for a USD account. ```bash curl -X POST "https://api.ads.openai.com/v1/ad_groups" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "campaign_id": "cmpn_101", "name": "US English", "status": "active", "bidding_config": { "billing_event_type": "click", "max_bid_micros": 100000000 } }' ``` For a product-feed campaign, the ad group automatically inherits the campaign's product feed. Include `product_set` only when you want to specify product filters; its `product_feed_id` must match the campaign's feed. See [Product Feeds](https://developers.openai.com/ads/product-feeds) for the complete product-feed campaign, ad-group, and product-ad template workflow. Create ads as you normally would, then activate the campaign after all child resources are ready. For the complete resource-creation sequence, see the [Quickstart](https://developers.openai.com/ads/api-quickstart). For all campaign and ad-group fields, see [Campaigns](https://developers.openai.com/ads/api-reference/campaigns) and [Ad Groups](https://developers.openai.com/ads/api-reference/ad-groups). ## Understand delivery and billing oCPC uses your selected conversion event together with ad quality, relevance, click likelihood, and conversion likelihood to favor clicks that are more likely to lead to that event. The CPA bid controls how the campaign competes for those outcomes. Billing does not change to pay per conversion. OpenAI charges you only when a valid click occurs, and the auction determines the actual CPC. Treat the CPA bid as an optimization input, not as a conversion charge. ## Review and improve performance In Ads Manager, review impressions, clicks, conversions, spend, click-through rate (CTR), and average CPC together. Because oCPC optimizes toward the selected event, conversions are the primary outcome to review. You can calculate cost per conversion by dividing spend by conversions. Use the [Insights endpoints](https://developers.openai.com/ads/api-reference/insights) to retrieve delivery metrics such as impressions, clicks, spend, CTR, and CPC. To improve performance: - Choose the standard conversion event that best represents your campaign goal. - Keep conversion tracking healthy. Incomplete or incorrectly configured tracking can make reporting and optimization less effective. - Use a conversion event with enough volume to evaluate performance. - Align ad copy with user intent and the selected conversion goal. - Review enough volume before making large changes to bids, budgets, or creative. ## Common questions ### Supported events An oCPC campaign supports exactly one active standard conversion event setting. Custom event settings are not supported as optimization goals. See [Supported Events](https://developers.openai.com/ads/supported-events) for the standard event names. ### Changing an existing campaign No. You cannot change an existing CPM or CPC campaign to oCPC. Create a new campaign with `bidding_type: "conversions"`. ### Changing the selected conversion event No. You cannot change the campaign goal or selected conversion event after creation. Create a new campaign to optimize toward a different event. ### Billing No. oCPC optimizes delivery toward the selected conversion event, but billing still uses valid clicks. ### Product-feed campaigns Yes. Product-feed oCPC is available in open beta. Set `mode` to `product_feed`, include the linked `product_feed_id`, and set `bidding_type` to `conversions` when creating the campaign. Create its ad group with `billing_event_type: "click"`. The ad group inherits the campaign's feed; include `product_set` only when you want to specify product filters. ## Next steps - [Set up conversion measurement](https://developers.openai.com/ads/api-reference/conversion-setup) - [Send browser events with the JavaScript Pixel](https://developers.openai.com/ads/measurement-pixel) - [Send server-side events with the Conversions API](https://developers.openai.com/ads/conversions-api) - [Create campaigns](https://developers.openai.com/ads/api-reference/campaigns) - [Create ad groups](https://developers.openai.com/ads/api-reference/ad-groups) - [Create product-feed campaigns](https://developers.openai.com/ads/product-feeds) - [Query insights](https://developers.openai.com/ads/api-reference/insights) --- # Conversion Tracking Use conversion tracking to measure actions people take after interacting with your ads, such as purchases, registrations, or lead submissions. Send events from your website or server, define which actions count as conversions, and connect them to your campaigns. Management requests use an ad account-scoped Advertiser API key, `${OPENAI_ADS_API_KEY}`. Server-side event requests use a separate Conversions API key, `${OPENAI_CONVERSIONS_API_KEY}`. Keep both keys on your server. See [Authentication](https://developers.openai.com/ads/api-reference/authentication) for Advertiser API request conventions. Examples use a website purchase in USD and sample IDs such as `cds_123`, `ces_123`, and `cmpn_123`. Replace sample IDs with those returned by your requests and use the currency appropriate to the transaction. Conversion tracking connects activity on your site to your advertising: - **A data source** receives events from your website or server. - **A conversion event setting** defines which event from that source counts as a conversion, such as a completed purchase. - **A campaign** uses the attached event setting for conversion reporting and, when configured, optimization. For example, your website sends an `order_created` event when a customer completes a purchase. You create a conversion event setting for purchases and attach it to the campaigns you want to measure. **Choose how to send events** | Integration | Where events are sent | When to use it | | ----------------- | ---------------------- | ------------------------------------------------------------------------------------------------ | | Measurement Pixel | The customer's browser | Measure actions that happen on your website. | | Conversions API | Your server | Send actions recorded by your server, such as confirmed orders. | | Both | Browser and server | Measure the same actions through both integrations, with shared event IDs to prevent duplicates. | Both integrations use a **Pixel ID** to identify the data source. A server-only integration still needs a Pixel ID, but does not require installing the browser Pixel. When using both integrations for the same website, reuse the data source. For the same conversion, send the same event name and event ID through both integrations so OpenAI can recognize the duplicate. ## Pixel Setup Use the Measurement Pixel to send events from your website. Create a data source, install the Pixel with its Pixel ID, and send an event when the action occurs. ### 1. Create a data source ```bash curl -X POST "https://api.ads.openai.com/v1/conversions/pixels" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" \ -H "Content-Type: application/json" \ -d '{ "name": "Acme website", "client_type": "web" }' ``` Save the returned `pixel_id` for sending events and `id` for creating conversion event settings. These identify the same source for different operations: | Value | Used for | | ---------- | ------------------------------------------------------------------------------------------------------------------- | | `pixel_id` | Initializing the Pixel, sending server events, and checking recent events. Use it as `${PIXEL_ID}` in the examples. | | `id` | Selecting the source when creating a conversion event setting. Use it in place of `cds_123`. | If you already have a source for this website, retrieve and reuse it: ```bash curl "https://api.ads.openai.com/v1/conversions/pixels" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" ``` ### 2. Install the Pixel Add the installation snippet from the [Measurement Pixel guide](https://developers.openai.com/ads/measurement-pixel#install-the-measurement-pixel) to the pages where you want to measure events. Set `pixelId` to the value returned when you created the source. No API key is needed in the browser. Where measurement requires consent, configure the [Pixel's consent controls](https://developers.openai.com/ads/measurement-pixel#control-measurement-consent) before initialization. ### 3. Send a purchase event After installing and initializing the Pixel, call `measure` when a purchase completes: ```javascript oaiq( "measure", "order_created", { type: "contents", amount: 8900, currency: "USD", }, { event_id: "order_12345" } ); ``` This example records an $89.00 purchase. Event amounts use the currency's standard minor unit: `8900` for USD 89.00, or `8900` for JPY 8,900. Use the actual order ID or another unique event ID. Keep it the same if you also send this purchase from your server. Confirm receipt using Monitoring Events. For other actions and their event data, see [Supported Events](https://developers.openai.com/ads/supported-events). ## Conversions API Setup Use the Conversions API to send events from your server. You need a data source's Pixel ID and a Conversions API key for the same ad account. Create or retrieve a source using the endpoints in Pixel Setup. If you only send server events, you can skip browser installation. ### 1. Create a Conversions API key Use your Advertiser API key to provision a key for sending events: ```bash curl -X POST "https://api.ads.openai.com/v1/conversions/api_keys" \ -H "Authorization: Bearer ${OPENAI_ADS_API_KEY}" \ -H "Content-Type: application/json" \ -d '{ "name": "Acme server events" }' ``` Store the returned `api_key` securely and use it as `${OPENAI_CONVERSIONS_API_KEY}`. This key authenticates event requests to `bzr.openai.com`; continue using your Advertiser API key for setup and management requests to `api.ads.openai.com`. ### 2. Send an event The following example sends a test purchase with the current timestamp. Set `${PIXEL_ID}` and `${OPENAI_CONVERSIONS_API_KEY}` before running it. In your integration, use the actual time the action occurred, in Unix milliseconds. ```bash EVENT_TIMESTAMP_MS="$(date +%s)000" curl -X POST "https://bzr.openai.com/v1/events?pid=${PIXEL_ID}" \ -H "Authorization: Bearer ${OPENAI_CONVERSIONS_API_KEY}" \ -H "Content-Type: application/json" \ -d @- <" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ --data '{ "validate_only": false, "events": [] }' ``` You can provision a Pixel ID and Conversions API key from the conversions tab in Ads Manager. Approved API partners can use the Ads API key associated with a client account to provision both resources with the [conversion setup endpoints](https://developers.openai.com/ads/api-reference/conversion-setup). | Value | Required | Description | | -------------------- | -------- | -------------------------------------------------------- | | `pid` | Yes | Your Pixel ID. | | `validate_only` | No | Validates events without saving them when `true`. | | `integration_source` | No | Stable identifier for the integration sending the batch. | | `events` | Yes | The events to send. | The API accepts batches of up to 1,000 events. If one event in the batch fails, the full batch fails. For app lifecycle events, use the Pixel ID from an existing web data source. Send `app_installed` and `app_opened` from your server with `action_source` set to `mobile_app`. Native mobile SDK setup and mobile data sources are not currently supported. ## Web-event attribution reporting Web events support click-through attribution and, when available for your account, view-through attribution. Click-through attribution uses the applicable configured click window. View-through conversions use a fixed one-day window after an eligible ad impression. Whether view-through reporting is available does not depend on your configured click window. If a conversion is eligible for both, the click takes precedence. View-through attribution does not use a separate request or event field. Ads Manager reports view-through conversions as a separate, campaign-level metric. They are not included in `Conversions`, which remains the click-through conversion total. CPA, post-click CVR, bidding, billing, and conversion optimization also remain click-through-based. App lifecycle events and mobile measurement integrations remain click-through-based. ## Identify partner integrations If you send events on behalf of advertisers, include `integration_source` at the top level of every Conversions API request. Mobile measurement partners and other integrations should use the same stable identifier on every request, such as `acme_measurement` or `example_analytics`. The value applies to every event in the batch. For example, a measurement partner can identify itself when sending an app install event: ```json { "integration_source": "acme_measurement", "events": [ { "id": "app_installed_123", "type": "app_installed", "timestamp_ms": , "action_source": "mobile_app", "data": { "type": "customer_action" } } ] } ``` Replace `` with the event timestamp in milliseconds. Use 1–64 ASCII characters. Start with a letter or digit, and use only letters, digits, periods (`.`), underscores (`_`), or hyphens (`-`). The API trims whitespace and converts the value to lowercase before validation. Use `integration_source` to identify the integration sending the request. This field does not affect authentication or authorization. ## Event structure Each event includes the event metadata and a `data` object. ```json { "id": "order_12345", "type": "order_created", "timestamp_ms": 1773892800000, "oppref": "oppref_abc", "source_url": "https://shop.example.com/checkout/confirmation", "action_source": "web", "user": { "obref": "123e4567-e89b-42d3-a456-426614174000", "emails_sha256": [ "b4c9a289323b21a01c3e940f150eb9b8c542587f1abfd8f0e1cc1ffc5e475514" ], "external_ids_sha256": [ "18f69bcd2f9cc9c38195e722b2a5590429840ea5090971d2256e026926e55fa1" ], "countries": ["US"], "cities": ["San Francisco"], "postal_codes": ["94107"], "ip_address": "203.0.113.1", "user_agent": "Mozilla/5.0" }, "data": { "type": "contents" } } ``` | Field | Required | Description | | ------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | Yes | A non-empty string that identifies the event. Reuse the same ID when retrying or sending the same conversion through another integration. | | `type` | Yes | Use `appointment_scheduled`, `checkout_started`, `contents_viewed`, `custom`, `items_added`, `lead_created`, `order_created`, `page_viewed`, `registration_completed`, `subscription_created`, or `trial_started`. Native app events also support `app_installed` and `app_opened`. | | `timestamp_ms` | Yes | Event time as an integer Unix timestamp in milliseconds. The timestamp must be within the last 7 days and no more than 10 minutes in the future. | | `custom_event_name` | Depends | Required when `type` is `custom`. Use 1–64 letters, digits, underscores, or hyphens; start and end with a letter or digit. The name cannot match a standard event name. The API converts it to lowercase. | | `oppref` | No | An opaque, OpenAI-provided attribution identifier. Pass the original string without modification. | | `source_url` | Depends | Required for web events when `action_source` is `web`; optional for native app events. Use a URL with a scheme and host, such as `https://shop.example.com/checkout`. | | `action_source` | Depends | Use `web`, `mobile_app`, `offline`, `physical_store`, `phone_call`, `email`, or `other`. The value must be `mobile_app` for `app_installed` and `app_opened` events. | | `user` | No | An object containing optional conversion-matching fields. See [Send user data](#send-user-data). | | `opt_out` | No | Use `true` to opt the event out of future user-level personalization, or `false` for the default behavior. | | `data` | Yes | An object describing the conversion. Its `type` field must match the data shape required for the event name (see [Supported Events](https://developers.openai.com/ads/supported-events)) and use one of the event data shapes below. | See [Supported Events](https://developers.openai.com/ads/supported-events) for event names and data shapes. Unlike the pixel, the API does not capture `oppref` for you. Capture the value yourself and pass it with the server event when it is available to support click matching. View-through attribution does not require a separate request or event field. ### Event data Each `events[].data` object supports the following fields. The available fields depend on its `type`. | Field | Required | Description | | ------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `type` | Yes | Use `contents`, `customer_action`, `plan_enrollment`, or `custom`, as required by the event type. The `app_installed` and `app_opened` event types require `customer_action`. | | `amount` | No | The event-level monetary value as an integer in the currency's standard minor unit. For example, use `4200` for $42.00 with `currency: "USD"`. | | `currency` | Depends | Required when `amount` is present. Use a valid three-letter ISO 4217 currency code, such as `USD`, `EUR`, or `JPY`; the API converts values to uppercase. | | `contents` | No | An array of item objects. Available when `data.type` is `contents`, `plan_enrollment`, or `custom`; not available for `customer_action`. | | `contents[].id` | No | A string containing your internal product, item, or content identifier. | | `contents[].group_id` | No | A string identifying the product group or parent item. | | `contents[].name` | No | A string containing the item's display name. | | `contents[].content_type` | No | A string describing the item category, such as `product`, `plan`, or `page`. | | `contents[].quantity` | No | The item quantity as an integer. | | `contents[].amount` | No | The item-level monetary value as an integer in the currency's standard minor unit. | | `contents[].currency` | No | A valid three-letter ISO 4217 currency code for the item, such as `USD`, `EUR`, or `JPY`. | | `contents[].variant_dict` | No | An object whose keys and values are strings, such as `{"size": "medium", "color": "blue"}`. | | `plan_id` | No | A string identifying your subscription or trial plan. Available when `data.type` is `plan_enrollment` or `custom`. | | `` | No | A custom property available only when `data.type` is `custom`. Values can be strings, numbers, boolean values, objects, arrays, or `null`. | ## Send user data Add an optional `user` object to each event to improve conversion matching. The object is event-scoped, so put it inside each entry in `events`, not at the request root. Every field in the `user` object is optional. Include only the fields you have for the user. ### Normalize identifiers before hashing Normalize each identifier as follows: - Email address: trim leading and trailing whitespace and convert the value to lowercase. - Phone number: keep the country calling code. Remove all whitespace, parentheses, periods, and hyphens, then remove a leading `+` and any leading zeroes. Hash the resulting 8–15 digits. For example, `+1 (415) 555-2671` becomes `14155552671`. - External ID: trim leading and trailing whitespace. Preserve case and all other characters. - First and last name: convert the value to lowercase and remove all whitespace and ASCII punctuation. Apart from converting to lowercase, preserve non-ASCII characters; don't strip accents or transliterate. For example, `O'Connor` becomes `oconnor`, and `José` becomes `josé`. The normalized value is the exact string to encode and hash: | Identifier | Input | Normalized value | | ------------ | ------------------- | ---------------- | | Phone number | `+1 (415) 555-2671` | `14155552671` | | First name | `Mary Jane` | `maryjane` | | Last name | `O'Connor` | `oconnor` | | First name | `José` | `josé` | Encode each normalized value as UTF-8, compute its SHA-256 digest, and send the digest as a lowercase, 64-character hexadecimal string. Don't send raw email addresses, phone numbers, external IDs, first names, or last names. Send geographic values as raw strings. ### User object example Place this object inside an event at `events[].user`: ```json { "obref": "123e4567-e89b-42d3-a456-426614174000", "phone_numbers_sha256": [ "758fbf68945f21c416814c539ab578876c8d98fb69e6da692def92cd52417fe0" ], "emails_sha256": [ "b4c9a289323b21a01c3e940f150eb9b8c542587f1abfd8f0e1cc1ffc5e475514" ], "external_ids_sha256": [ "18f69bcd2f9cc9c38195e722b2a5590429840ea5090971d2256e026926e55fa1" ], "first_names_sha256": [ "fdee430d40bd57deeac186cd9790033d0f06f909a8806e7ce6e717ab7c7d5029" ], "last_names_sha256": [ "fb1e7ec987523d2cb9e022cec1d6ae7c99dc46edfae4fe51254025fe4bea571f" ], "regions": ["California"], "postal_codes": ["94107"], "cities": ["San Francisco"], "countries": ["US"], "android_advertising_id": "38400000-8cf0-11bd-b23e-10b96e40000d", "ip_address": "203.0.113.1", "user_agent": "Mozilla/5.0" } ``` Use the plural list fields below. For each list, the API uses the first three valid, unique values in the order provided. It ignores additional values without rejecting the event or request. | Field | Type | Description | | ------------------------ | -------------- | ------------------------------------------------------------------------------------------------------------------------------ | | `phone_numbers_sha256` | `list[string]` | SHA-256 hashes of 8–15 digits after removing a leading `+`, leading zeroes, whitespace, parentheses, periods, and hyphens. | | `emails_sha256` | `list[string]` | SHA-256 hashes of normalized email addresses. | | `external_ids_sha256` | `list[string]` | SHA-256 hashes of stable, pseudonymous customer identifiers from your system. | | `first_names_sha256` | `list[string]` | SHA-256 hashes of lowercase first names after removing whitespace and ASCII punctuation; non-ASCII characters are preserved. | | `last_names_sha256` | `list[string]` | SHA-256 hashes of lowercase last names after removing whitespace and ASCII punctuation; non-ASCII characters are preserved. | | `regions` | `list[string]` | Raw region values. The API trims whitespace, converts values to lowercase, and limits each normalized value to 128 characters. | | `postal_codes` | `list[string]` | Raw postal or ZIP codes. Use letters, numbers, spaces, or hyphens; each normalized value can contain up to 32 characters. | | `cities` | `list[string]` | Raw city names. The API trims whitespace, converts values to lowercase, and limits each normalized value to 128 characters. | | `countries` | `list[string]` | Raw two-letter country codes, such as `US`. | | `android_advertising_id` | `string` | Raw Android Google Advertising ID (GAID) in UUID format. Available only through the Conversions API. | | `obref` | `string` | Opaque browser reference from the Pixel's `__obref` cookie. Pass it without hashing. | | `ip_address` | `string` | Valid IPv4 or IPv6 address. | | `user_agent` | `string` | Non-empty user agent string from the client that generated the event. | `android_advertising_id` supports Android GAID only; IDFA is not supported. You can send a GAID with any `action_source`. The API ignores all-zero advertising IDs without rejecting the event. For hybrid Pixel and Conversions API integrations, read the `__obref` first-party cookie in the browser, send it to your server, and include it unchanged as `events[].user.obref` when available. Send a non-blank string. Before collecting or forwarding the cookie, follow your site's measurement consent requirements. If the user revokes consent, stop sending it. Unlike `oppref`, which is an event-level field, `obref` belongs inside `user`. ## Example event ```bash curl -X POST "https://bzr.openai.com/v1/events?pid=" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ --data '{ "validate_only": false, "events": [ { "id": "order_12345", "type": "order_created", "timestamp_ms": 1773892800000, "oppref": "oppref_abc", "source_url": "https://shop.example.com/checkout/confirmation", "action_source": "web", "user": { "obref": "123e4567-e89b-42d3-a456-426614174000", "emails_sha256": [ "b4c9a289323b21a01c3e940f150eb9b8c542587f1abfd8f0e1cc1ffc5e475514" ], "external_ids_sha256": [ "18f69bcd2f9cc9c38195e722b2a5590429840ea5090971d2256e026926e55fa1" ], "countries": ["US"], "cities": ["San Francisco"], "postal_codes": ["94107"], "ip_address": "203.0.113.1", "user_agent": "Mozilla/5.0" }, "data": { "type": "contents", "amount": 2599, "currency": "USD", "contents": [ { "id": "sku_123", "name": "Starter bundle", "content_type": "product", "quantity": 1 } ] } } ] }' ``` ## App lifecycle events App lifecycle events use the `customer_action` data shape and require `action_source` to be `mobile_app`. ### App installed ```json { "id": "app_installed_123", "type": "app_installed", "timestamp_ms": , "action_source": "mobile_app", "data": { "type": "customer_action" } } ``` ### App opened ```json { "id": "app_opened_123", "type": "app_opened", "timestamp_ms": , "action_source": "mobile_app", "data": { "type": "customer_action" } } ``` ## Deduplicate browser and server events If you send the same conversion from the pixel and the Conversions API, reuse the same value as the API `id` and pixel `event_id`. Send both events with the same Pixel ID. For custom events, use the same `custom_event_name` on both sides as well. Deduplication uses your Pixel ID, `event_name`, and `id`. OpenAI uses the first event it receives for a matching key and ignores later duplicates. --- # Custom Audiences Custom audiences let you use customer or prospect lists to control who can see your ads. Create an audience from a file or start with an empty audience, then add or remove customers as your list changes. You can also replace the full list or merge existing audiences into a new audience. Custom audiences are not supported for campaigns targeting the European Economic Area (EEA) or Switzerland, where personalized ads are not yet available. Before you begin, create an Ads API key in the **Settings** tab of [Ads Manager](https://ads.openai.com). Store the key as `OPENAI_ADS_API_KEY` and send it as a bearer token. Each key can access only the audiences associated with its ad account. Only upload first-party audience data that you have the right to use for ads. Don't upload broker-sourced data. Before uploading, confirm that your use complies with required rights, notices, consents, permissions, legal bases, and the [Ad Tools Terms](https://openai.com/policies/ad-tools-terms/), and get privacy or legal approval for your use case. All examples use the `https://api.ads.openai.com/v1` base URL. Replace the example resource IDs with IDs from your account. For each new membership operation, generate a unique `Idempotency-Key` and keep it with the original request so you can retry safely. ## Choose an operation Choose the operation that matches the change in your customer list: | Goal | Input | Result | | ------- | ---------------------------------------------- | ------------------------------------------------- | | Create | Uploaded file, or a name without a file | A new audience ID. | | Add | Inline identifiers or an uploaded file | Add matched users to the same audience. | | Remove | Inline identifiers or an uploaded file | Remove matched users from the same audience. | | Replace | Uploaded file containing the full desired list | Replace membership while keeping the audience ID. | | Merge | 2 to 64 existing audience IDs | A new, independent union audience. | Use inline requests for small updates and files for bulk changes. Both are asynchronous: accepting a request doesn't mean processing has finished. Small audiences, including empty audiences, can be used for **exclusion** once they are ready. Inclusion and bid adjustments still require enough matched users. Check [eligibility for the intended use](#check-eligibility-for-the-intended-use) before attaching an audience to a campaign or ad group. ## Prepare an audience file Create a UTF-8 CSV or TXT file no larger than 500 MB (500,000,000 bytes). A UTF-8 BOM is accepted. Use `text/csv` for CSV files and `text/plain` for TXT files. A TXT file contains one identifier per line, without a header, and uses the `identifier_type` you specify in the request. A CSV file must include an identifier header. These identifier formats are supported: | Identifier type | CSV header | Format | | --------------------- | --------------------- | ------------------------------------------------------------------------------------- | | `email` | `email` | An email address containing one `@`. The API trims it and converts it to lowercase. | | `phone` | `phone_number` | A phone number in E.164 format, including `+` and the country code. | | `email_sha256` | `email_sha256` | The 64-character SHA-256 hexadecimal digest of the normalized email address. | | `phone_number_sha256` | `phone_number_sha256` | The 64-character SHA-256 hexadecimal digest of the normalized E.164 telephone number. | | `gaid` | `gaid` | A nonzero, hyphenated Google Advertising ID. The API normalizes and hashes it. | For example, an email audience CSV can contain: ```text email alex@example.com jamie@example.com sam@example.com ``` Before hashing an email, trim surrounding whitespace and convert it to lowercase. Before hashing a phone number, normalize it to E.164, including the leading `+` and country code. Hash the UTF-8 value without a trailing newline and send the 64-character hexadecimal digest, not Base64. Don't remove email dots or plus tags. GAID values must be raw, nonzero, hyphenated UUID values, such as `38400000-8cf0-11bd-b23e-10b96e40000d`. The API trims surrounding whitespace, converts them to lowercase, and hashes them internally. Don't hash GAID values before submitting them. ### Combine identifier types in one CSV Set `identifier_resolution` to `auto` when creating or updating an audience from a CSV that combines email, phone, GAID, and hashed identifiers: ```csv email,phone_number,email_sha256,phone_number_sha256,gaid alex@example.com,+12025550123,,, ,,057a0fff4c78ae3e14236c36b611061cbdd54ccd72a34b23f77d7a8c4bca4963,, ,,,1a2d415d4fef1dfafe57e0d98af15bbad8cc4bd8ca8ac66e89f2e0ef3941d500, ,,,,38400000-8cf0-11bd-b23e-10b96e40000d ``` Leave unused cells empty. Each populated identifier cell is a matching candidate; a row doesn't require every identifier to match the same user. OpenAI counts each matched user once, so different identifiers can represent one audience member. Don't repeat a consumed identifier column in the header. Use `identifier_resolution: "auto"` with file-based Create, Add, Remove, or Replace. Without it, use a single identifier type and specify that type in `identifier_type`; this single-type processing path accepts up to 5,000,000 identifiers. Automatic resolution supports larger files within the same 500 MB upload limit. ## Upload the audience file Upload the CSV or TXT file to `POST /uploads`. Set the multipart `purpose` field to `custom_audience`: ```bash curl -X POST "https://api.ads.openai.com/v1/uploads" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" \ -F "file=@audience.csv;type=text/csv" \ -F "purpose=custom_audience" ``` The response contains the file ID: ```json { "file_id": "oaisdmntci_123" } ``` Save the `file_id`, the original filename, the file's MIME type, and the exact file size in bytes. You must provide these values when you create an audience from a file. Use the upload promptly; don't treat its file ID as permanent storage. ## Create the custom audience Send the uploaded file details to `POST /custom_audiences`: ```bash curl -X POST "https://api.ads.openai.com/v1/custom_audiences" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "High-value customers", "description": "Customers eligible for the summer campaign", "file_id": "oaisdmntci_123", "identifier_type": "email", "filename": "audience.csv", "mimetype": "text/csv", "file_size": 123456 }' ``` | Field | Required | Description | | ----------------------- | ----------------- | ------------------------------------------------------------------ | | `name` | Yes | Audience name containing at least three characters. | | `description` | No | A description of the audience. | | `file_id` | For file creation | The file ID returned by `POST /uploads`. | | `identifier_type` | No | `email`, `phone`, `email_sha256`, `phone_number_sha256`, `gaid`. | | `identifier_resolution` | No | Set to `auto` for automatic resolution of CSV identifier columns. | | `filename` | With `file_id` | The uploaded filename, including its `.csv` or `.txt` extension. | | `mimetype` | With `file_id` | The uploaded file's MIME type, such as `text/csv` or `text/plain`. | | `file_size` | With `file_id` | The exact file size in bytes, from `1` through `500000000`. | If you omit `identifier_type`, the API defaults to `email`. For a single-type file, set the type explicitly. For a mixed-column CSV, add `"identifier_resolution": "auto"` to the request. The API returns the new audience and starts processing the uploaded file: ```json { "id": "caud_123", "created_at": 1783962000, "updated_at": 1783962000, "name": "High-value customers", "description": "Customers eligible for the summer campaign", "status": "processing", "hash_spec_version": "custom_audience_join_hash_v1", "uploaded_identifier_count_range": "none", "matched_identifier_count_range": "none", "matched_user_count_range": "none", "invalid_identifier_count_range": "none", "membership_revision": 0 } ``` ### Create an empty audience To build a list incrementally, create an empty audience without uploading a placeholder file: ```bash curl -X POST "https://api.ads.openai.com/v1/custom_audiences" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Recent purchasers", "description": "Exclude recent purchasers from acquisition campaigns" }' ``` Omit `file_id`, `filename`, `mimetype`, `file_size`, and `identifier_resolution`. Don't upload a zero-byte file. Save the returned audience ID and wait for it to be `ready` before adding members or using it for exclusion. An empty exclusion audience doesn't exclude anyone until members are added. Create returns an audience object, not a membership operation. Don't assume it has the Add/Remove replay contract. If the creation response is lost, check the account's audience list before creating another audience. ## Check processing status Retrieve the audience with `GET /custom_audiences/{custom_audience_id}`: ```bash curl "https://api.ads.openai.com/v1/custom_audiences/caud_123" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" ``` Check the audience periodically until preparation finishes. Processing time depends on input size and the operation. `ready` means preparation succeeded; it doesn't mean the audience is eligible for every use. | Status | Meaning | | ------------------------ | ------------------------------------------------------------------------------------- | | `upload_pending` | The uploaded file is waiting for processing to begin. | | `processing` | The file is being processed and the audience isn't ready to use. | | `rockset_ingest_pending` | Processed identifiers are waiting to be ingested. | | `publishing` | The audience is being prepared for targeting and bidding. | | `ready` | Processing succeeded. Check eligibility for exclusion, inclusion, or bid adjustments. | | `too_small` | The audience didn't meet the size policy applied when it was processed. | | `failed` | Processing failed. Check the file format, identifier type, and file limits. | | `archived` | The audience is archived and can no longer be used. | The response returns identifier and matched-user counts as privacy-preserving ranges, such as `under_25k`, `25k_100k`, `100k_500k`, `500k_1m`, `1m_5m`, and `5m_plus`. `under_25k` includes empty matched audiences. For matched counts, `none` means a count isn't available, not that it is zero. Exact matched counts and individual membership results aren't exposed. For finer reporting at or above 100,000 matched users, pass `matched_count_granularity=granular` to a list or retrieve request. Counts remain privacy-preserving ranges above 5,000,000, with wider ranges used for larger audiences. Don't use a count range to decide targeting eligibility. ## Update audience membership List and retrieve responses include `membership_revision`. Read the audience before a change and pass that value as `expected_revision`. It is optional for Add and Remove, and required for Replace. The revision values in the examples are illustrative; use the value you just read. Wait for each operation to finish before submitting the next dependent change. After success, retrieve the audience again for its current revision. Don't assume every request changes membership or increments the revision. ### Add or remove inline identifiers Add and remove operations accept exactly one uploaded `file_id` or an `identifiers` array. Each inline identifier includes its own `identifier_type`: ```bash curl -X POST "https://api.ads.openai.com/v1/custom_audiences/caud_123/add" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: custom-audience-add-001" \ -d '{ "expected_revision": 0, "identifiers": [ { "identifier_type": "email", "identifier": "new.customer@example.com" }, { "identifier_type": "gaid", "identifier": "38400000-8cf0-11bd-b23e-10b96e40000d" } ] }' ``` Inline entries can mix all five supported identifier types. Each entry has its own type and value. Don't also send `file_id`. Use the same request shape with `/remove` to remove customers, with a fresh revision and a new key for the Remove operation: ```bash curl -X POST "https://api.ads.openai.com/v1/custom_audiences/caud_123/remove" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: custom-audience-remove-001" \ -d '{ "expected_revision": 1, "identifiers": [ {"identifier_type": "email", "identifier": "new.customer@example.com"} ] }' ``` Add doesn't duplicate a user who is already a member. Remove doesn't change membership for an absent user or permanently prevent a later Add. Unmatched identifiers can leave membership unchanged. Batches of up to 10,000 inline identifiers use the small-update path. Larger inline batches use file-based processing; 10,000 is not a hard item-count limit. The entire Add/Remove request body must fit within 16 MiB (16,777,216 bytes), or the API returns `413`. Prefer files for bulk updates. ### Add or remove using a file Upload the delta file with `purpose=custom_audience`, then call `/add` or `/remove` with its `file_id` instead of `identifiers`: ```bash curl -X POST "https://api.ads.openai.com/v1/custom_audiences/caud_123/add" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: custom-audience-file-add-001" \ -d '{ "file_id": "oaisdmntci_456", "identifier_resolution": "auto", "expected_revision": 2 }' ``` This example accepts a CSV with mixed identifier columns. For a single-type file, you can send `identifier_type` instead of `identifier_resolution`. Add/Remove use the filename, MIME type, and size saved with the upload; don't send those metadata fields in the mutation body. An Add file contains only customers to add. A Remove file contains only customers to remove; members omitted from the file remain in the audience. Use Replace when the file is a full snapshot of the desired membership. ### Replace the full list Upload the complete desired audience, read the current membership revision, then submit the file and revision to `/replace`: ```bash curl -X POST "https://api.ads.openai.com/v1/custom_audiences/caud_123/replace" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: custom-audience-replace-001" \ -d '{ "file_id": "oaisdmntci_789", "identifier_resolution": "auto", "expected_revision": 3 }' ``` Replace requires `file_id` and a nonnegative `expected_revision`; it doesn't accept inline identifiers. The audience keeps its ID and existing campaign and ad-group references. The current membership stays available while the replacement is prepared, then the new membership is published. The audience can still show `ready` during replacement. Poll the returned operation to determine when the replacement finishes, rather than relying on audience status alone. Don't emulate replacement by removing every member and adding them back. ### Merge audiences into a new list Merge combines 2 to 64 distinct, ready audiences in the same ad account, counting each matched user once. Wait for pending source updates to finish before merging: ```bash curl -X POST "https://api.ads.openai.com/v1/custom_audiences/merge" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: custom-audience-merge-001" \ -d '{ "name": "All qualified customers", "custom_audience_ids": ["caud_source_1", "caud_source_2"] }' ``` The new audience is independent. The sources don't change, future source updates don't propagate to the merged audience, and existing campaigns don't automatically switch to the new ID. Poll the operation using the `custom_audience_id` returned by Merge, not a source audience ID. ## Poll and recover membership operations Use these endpoints for each membership change: | Goal | Endpoint | Required body fields | | ------------------------- | ----------------------------------------------------- | ------------------------------ | | Add identifiers | `POST /custom_audiences/{custom_audience_id}/add` | `file_id` or `identifiers` | | Remove identifiers | `POST /custom_audiences/{custom_audience_id}/remove` | `file_id` or `identifiers` | | Replace all identifiers | `POST /custom_audiences/{custom_audience_id}/replace` | `file_id`, `expected_revision` | | Merge into a new audience | `POST /custom_audiences/merge` | `name`, `custom_audience_ids` | Each new Add, Remove, Replace, or Merge request requires an `Idempotency-Key` header. Reuse the key only to retry the same operation; retries return or resume the first accepted input. Don't change the file, identifiers, or revision under an existing key: a repeated key can return the original operation without checking the new body. Save the original request, key, audience ID, and operation ID securely. The response contains a privacy-safe operation object: ```json { "operation_id": "caudop_123", "custom_audience_id": "caud_123", "operation": "add", "status": "processing" } ``` Poll `GET /custom_audiences/{custom_audience_id}/operations/{operation_id}` until `status` is `succeeded` or `failed`. The response exposes only the operation ID, audience ID, operation type, and status; it doesn't return raw identifiers, matching counts, or individual membership outcomes. ```bash curl \ "https://api.ads.openai.com/v1/custom_audiences/caud_123/operations/caudop_123" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" ``` Handle these responses without starting duplicate work: | Response | What to do | | --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | `processing` | Continue polling with increasing delays. | | `succeeded` | Retrieve the audience and its current revision before another change. | | `failed` | Stop polling and reconcile the result before submitting another operation. Contact support with the operation ID if needed. | | `409 custom_audience_operation_recovery_required` | The Add/Remove was interrupted and may be partially applied. [Resume by operation ID](#resume-an-add-or-remove), then poll the same operation. | | `503 custom_audience_operation_unavailable` | Status is temporarily unavailable. Retry the status request with increasing delays; don't assume failure. | | `409 custom_audience_mutation_conflict` | Wait for competing work, retrieve the current state, and reconsider the intended change. | | `409 custom_audience_replacement_revision_conflict` | Refresh the audience revision before submitting a new replacement. | | `429` | Back off and retry, retaining the original key for an accepted mutation. | For a lost submission response, retry the original endpoint, body, and key. An interrupted Add/Remove may have changed some membership already. Don't use a new key, replay the entire job as new work, or submit an inverse update to guess at recovery. If recovery can't continue, contact support with the operation ID and request ID, without sending raw identifiers. ### List operations for an audience Use `GET /custom_audiences/{custom_audience_id}/operations` to find operation IDs when you no longer have the original submission response. The list includes retained Add, Remove, Replace, and Merge operations for that audience in your authenticated ad account. It doesn't include the initial Create request. For Merge, use the new audience ID, not a source audience ID. ```bash curl --get \ "https://api.ads.openai.com/v1/custom_audiences/caud_123/operations" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" \ --data-urlencode "limit=20" ``` ```json { "object": "list", "data": [ { "operation_id": "caudop_123", "custom_audience_id": "caud_123", "operation": "add", "status": "processing" } ], "has_more": false, "next_cursor": null } ``` Set `limit` from 1 to 100. When `has_more` is `true`, pass `next_cursor` unchanged as the `cursor` query parameter on the next request, using the same ad account and audience. Continue until `has_more` is `false`, even if a page has an empty `data` array. Each item has the same four fields as the status response; the list doesn't return the original input or idempotency key. ### Resume an Add or Remove Use `POST /custom_audiences/{custom_audience_id}/operations/{operation_id}/resume` to recover an accepted Add or Remove using its original inputs and saved progress. You don't need the original idempotency key or a request body. Confirm the operation ID from your saved response or the operation list before resuming it. ```bash curl -X POST \ "https://api.ads.openai.com/v1/custom_audiences/caud_123/operations/caudop_123/resume" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" ``` The response is the same operation object shown above. Poll that operation until it reaches a terminal status. Resume preserves the operation ID and accepted input; it doesn't create a new operation or replace the uploaded file or identifiers. An operation that has already succeeded or failed keeps its terminal status. Resuming it can finish pending cleanup, but doesn't apply its membership changes again. Resume by ID supports Add and Remove only. Replace and Merge retries still use the original submission endpoint, body, and idempotency key. ### Cancel an Add or Remove Use `POST /custom_audiences/{custom_audience_id}/operations/{operation_id}/cancel` to stop an accepted Add or Remove before it starts applying membership changes. Send no request body or `Idempotency-Key` header. ```bash curl -X POST \ "https://api.ads.openai.com/v1/custom_audiences/caud_123/operations/caudop_123/cancel" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" ``` A successful cancellation returns the operation with `status: "failed"`; there is no separate `canceled` status. Cleanup can continue asynchronously. Canceling again is safe, and resuming a canceled operation can finish cleanup without applying its membership changes. Cancellation returns `409 custom_audience_mutation_conflict` if an active operation has started applying changes or the operation already succeeded. A failed operation keeps its existing `failed` status; cancellation doesn't undo any changes it made before failing. A `processing` status alone doesn't guarantee that cancellation is still possible. Cancellation doesn't support Replace or Merge. If cancellation returns `503 custom_audience_operation_unavailable`, it may have stopped the operation but failed to schedule cleanup. Retry cancellation for the same operation ID; don't start a new submission to retry cancellation. All operation requests require access to the audience's ad account. For OAuth, listing and polling require `ads.admin.all.read`; resuming and canceling require both `ads.admin.all.read` and `ads.admin.all.write`. ## Check eligibility for the intended use Use-specific eligibility is separate from `status: "ready"`: | Intended use | Size requirement | | ---------------- | -------------------------------------------------------------------------------- | | `exclusion` | Ready small or empty audiences can be used. No minimum matched size is required. | | `inclusion` | The audience must meet the matched-user minimum. | | `bid_multiplier` | The audience must meet the matched-user minimum for bid adjustments. | For inclusion and bid adjustments, use 25,000 matched users as the public planning threshold. Privacy safeguards can affect the exact boundary, and uploading 25,000 identifiers doesn't guarantee enough matched users. Don't send an `exclusion_only` creation field: eligibility depends on how you use the audience. Request audiences eligible for the intended use: ```bash curl -G "https://api.ads.openai.com/v1/custom_audiences" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" \ --data-urlencode "intended_use=exclusion" \ --data-urlencode "custom_audience_ids[]=caud_123" ``` Use `inclusion` or `bid_multiplier` to check those uses. Repeat `custom_audience_ids[]` for multiple IDs, or omit it to list eligible audiences in the account. The response contains only eligible audiences and a `policy_revision` token. To recheck the selection, send that `policy_revision` with the intended use and the selected IDs. If the API returns `409 custom_audience_policy_revision_mismatch`, refresh without the old token and review the selection again. `policy_revision` is not `membership_revision` or a campaign-write parameter. The server validates eligibility again when you save campaign or ad-group settings. ## Include or exclude audiences in a campaign Use ready audiences eligible for the intended use in the campaign's `targeting` object. Add audience IDs to `custom_audiences.ids` to include matched users or `excluded_custom_audiences.ids` to exclude them: ```bash curl -X POST "https://api.ads.openai.com/v1/campaigns" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: custom-audience-campaign-001" \ -d '{ "name": "High-value customer campaign", "description": "Campaign targeting selected customer audiences", "status": "paused", "bidding_type": "clicks", "budget": { "lifetime_spend_limit_micros": 300000000 }, "targeting": { "locations": { "countries": ["US"] }, "custom_audiences": { "ids": ["caud_123"] }, "excluded_custom_audiences": { "ids": ["caud_456"] } } }' ``` Audience inclusion and exclusion work as follows: - Include audiences to deliver only to users who belong to at least one included audience. - Exclude audiences to prevent delivery to users who belong to an excluded audience. - If you use both, exclusions take precedence and the remaining audience must still meet the minimum size requirement. - Don't include and exclude the same audience in a campaign. For an exclusion-only campaign, omit `custom_audiences` and provide only `excluded_custom_audiences`. A small exclusion audience doesn't need to meet the inclusion minimum. If you combine inclusion and exclusion, the remaining eligible audience must still meet the minimum. The same fields apply to `POST /campaigns/{campaign_id}`. Preserve the other targeting settings you want to keep when updating the targeting object. For the remaining campaign parameters, see [Campaigns](https://developers.openai.com/ads/api-reference/campaigns). ## Adjust bids for an audience Add `custom_audience_bid_multipliers` to an ad group's `bidding_config` to raise or lower the maximum bid for a ready audience eligible for `intended_use=bid_multiplier`. Small exclusion audiences aren't automatically eligible for bid adjustments. Bid multipliers don't change which users are eligible to see a campaign. ```bash curl -X POST "https://api.ads.openai.com/v1/ad_groups" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: custom-audience-ad-group-001" \ -d '{ "campaign_id": "cmpn_123", "name": "High-value customers", "description": "Higher bid for selected customers", "status": "paused", "bidding_config": { "billing_event_type": "click", "max_bid_micros": 7500000, "custom_audience_bid_multipliers": [ { "custom_audience_id": "caud_123", "bid_multiplier_micros": 2000000 } ] } }' ``` Multipliers are expressed in millionths: | `bid_multiplier_micros` | Bid multiplier | | ----------------------- | -------------- | | `100000` | 0.1× | | `1000000` | 1× | | `2000000` | 2× | | `10000000` | 10× | The supported range is `100000` through `10000000`. If a user matches multiple configured audiences, the highest matching multiplier applies. For the remaining ad group parameters, see [Ad Groups](https://developers.openai.com/ads/api-reference/ad-groups). ## Handle targeting safeguards and conflicts Membership changes must preserve the size requirements of campaigns and bid adjustments that reference the audience. For example, removing users from an inclusion audience or adding users to an exclusion audience can make a campaign's remaining eligible population too small. The API can reject the change; using a file instead of inline identifiers doesn't bypass this check. A campaign or ad-group update can conflict with an in-progress membership change. `409 custom_audience_mutation_conflict` means that targeting update wasn't applied. Wait for the membership operation to finish, retrieve the current settings, and retry the intended edit if it is still appropriate. ## List and archive audiences List the custom audiences associated with your API key's ad account: ```bash curl -G "https://api.ads.openai.com/v1/custom_audiences" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" \ --data-urlencode "limit=20" ``` Use the membership operations above to update an audience. Archive an audience only when you no longer need it: ```bash curl -X POST \ "https://api.ads.openai.com/v1/custom_audiences/caud_123/archive" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" ``` Archiving is permanent. An archived audience can't be restored or used in campaign targeting or bid adjustments. --- # Delta Feeds API The Delta Feeds API updates availability, titles, and prices for existing variants in a linked product feed. Send only the products that changed instead of uploading your entire catalog again. Access to the Delta Feeds API is enabled per ad account. If a request returns `403` with `product_feed_api_disabled` or `product_feed_delta_api_disabled`, contact your OpenAI account team to confirm access. ## Before you begin You need: - An Ads API key from the ad account's **Settings** tab in [Ads Manager](https://ads.openai.com). - A product feed linked to that ad account and its feed ID. - An initial catalog already uploaded to the feed. - The existing parent product and variant identifiers from that catalog. The endpoint updates existing feed variants. It doesn't create feeds, upload full catalogs, or add products that aren't already in the feed. See [Product Feeds](https://developers.openai.com/ads/product-feeds) to set up the initial catalog. ## Update product variants Send a `PATCH` request with each changed product and its affected variants. You can update a variant's price, availability, or both. `PATCH /feeds/{feed_id}/products` ```bash curl -X PATCH \ "https://api.ads.openai.com/v1/feeds/product_feed_123/products" \ -H "Authorization: Bearer $OPENAI_ADS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "products": [ { "id": "running-shoe-001", "variants": [ { "id": "running-shoe-001-black-9", "availability": { "available": false } }, { "id": "running-shoe-001-white-9", "title": "Running shoe - white, size 9", "price": { "amount": 8999, "currency": "USD" }, "availability": { "status": "in_stock" } } ] } ] }' ``` The API returns `200 OK` with the feed ID and an acceptance result: ```json { "id": "product_feed_123", "accepted": true } ``` `accepted: true` means the update was accepted by feed processing. It doesn't mean downstream indexing, ad eligibility, or serving has already updated. Check the `accepted` value before treating the request as successful. ### Request fields | Field | Type | Required | Description | | ------------------------------------ | -------- | -------- | ----------------------------------------------------------------------------------------------------- | | `products` | object[] | Yes | One or more existing products to update. | | `products[].id` | string | Yes | The parent product identifier from the existing catalog. | | `products[].variants` | object[] | Yes | One or more existing variants of the parent product. | | `products[].variants[].id` | string | Yes | The existing variant or item identifier from the catalog. | | `products[].variants[].title` | string | No | Updated title for this variant. | | `products[].variants[].price` | object | No | Updated price for this variant. | | `price.amount` | integer | Yes | Nonnegative price in minor units (`8999` means `$89.99` in `USD`). | | `price.currency` | string | Yes | Supported three-letter currency code, such as `USD`. | | `products[].variants[].availability` | object | No | Updated availability for this variant. | | `availability.available` | boolean | No | `true` maps to `in_stock`; `false` maps to `out_of_stock`. | | `availability.status` | string | No | Explicit availability, such as `in_stock` or `out_of_stock`. Overrides `available` when both are set. | Both `products` and every `variants` array must contain at least one item. Product and variant IDs must be nonempty. Don't include the same variant more than once in a request. Send the feed ID in the URL, the parent product ID in `products[].id`, and the variant ID in `products[].variants[].id`. Don't send `shop_id`, `scoped_offer_id`, or `target_country`; feed ownership, product identity, and supported countries are resolved from the linked feed. ## Understand how changes are applied The initial feed upload supplies the full product record. A delta request changes only the specified fields on existing variants and preserves the remaining catalog data. After feed processing accepts an update, downstream systems apply the change asynchronously. An out-of-stock product stops qualifying for delivery after the change propagates. Marking a product in stock doesn't guarantee that it will serve: the product, campaign, ad group, and ad must still meet normal [serving requirements](https://developers.openai.com/ads/product-feeds#understand-serving-eligibility). There is no completion timestamp or downstream processing result in the acceptance response. Use your normal feed and campaign monitoring to verify the result. ## Handle common errors | Status | Cause | Action | | ------ | ---------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | | `400` | A required field is missing, a product or variant list is empty, or the request includes an unknown field. | Verify the request body and send only supported fields. | | `401` | The Ads API key is missing or invalid. | Use an active Ads API key in the `Authorization: Bearer` header. | | `403` | Feed API access is disabled or the account lacks permission to manage feed data. | Check account permissions and ask your OpenAI account team to confirm Delta Feeds API access. | | `404` | The feed doesn't exist or isn't linked to the ad account associated with the API key. | Confirm the feed ID and use the API key for the account that owns the feed. | If the error code is `product_feed_api_disabled` or `product_feed_delta_api_disabled`, access hasn't been enabled for the account. Don't retry unchanged requests until access or the underlying request issue is resolved. ## Next steps - [Set up a product feed and product-feed campaign](https://developers.openai.com/ads/product-feeds). - [Review Advertiser API authentication](https://developers.openai.com/ads/api-reference/authentication). - [Understand Advertiser API access and limits](https://developers.openai.com/ads/api-overview). --- # Image Tag Use an image tag to send a website conversion when a page loads without running JavaScript. Each image request sends one event. Use the [JavaScript Pixel](https://developers.openai.com/ads/measurement-pixel) for events triggered by clicks, form submissions, or other interactions after the page loads. Use the [Conversions API](https://developers.openai.com/ads/conversions-api) when you can send the event from your server. ## Install an image tag Add a hidden 1 × 1 image to the `` of the page where the event happens: ```html ``` Replace `` with the Pixel ID from the conversions tab in Ads Manager. Render the tag only after collecting any consent required for measurement. To use the image as a fallback for the JavaScript Pixel, put it inside a `