Skip to main content

ChatGPT Ads

Create and manage ads inside ChatGPT with the same API key you already use for publishing. Upload-Post talks to the OpenAI Advertiser API on your behalf, so you connect your ad account once and then drive campaigns, ad groups, ads, creatives and reporting through api.upload-post.com.

What you need

An OpenAI Ads Manager account with API access. Issue a key at ads.openai.com → Settings → API keys. Each OpenAI key is scoped to exactly one ad account; connect one key per ad account you want to manage.

Spend runs on your own OpenAI billing. Upload-Post never charges for ad spend and never bills through your ad account — it only relays the calls.


How ChatGPT Ads are structured​

Campaign        budget, schedule, location and audience targeting
└── Ad group bid (max CPM or max CPC)
└── Ad creative: title, body, image, destination URL

An ad only serves when all three objects are active and the ad has passed OpenAI's review (review_status: "approved", usually a few minutes).

Everything is created paused

Every create endpoint defaults to status: "paused" so nothing starts spending by accident. Send "status": "active" explicitly, or flip the objects later with the /activate actions.


Connect an ad account​

Endpoint​

POST /api/uploadposts/ads/chatgpt/connect

Body Parameters (JSON)​

NameTypeRequiredDescription
api_keyStringYesYour OpenAI Ads API key. Validated live against OpenAI before it is stored.
profile_usernameStringNoLink the ad account to this profile. See Profiles.
nameStringNoFriendly label. Defaults to the ad account name reported by OpenAI.
defaultBooleanNoMake this the account used when a request omits ad_account_id.

Example Request​

curl -X POST https://api.upload-post.com/api/uploadposts/ads/chatgpt/connect \
-H 'Authorization: Apikey your-api-key-here' \
-H 'Content-Type: application/json' \
-d '{"api_key": "your-openai-ads-api-key"}'

Successful Response (200 OK)​

{
"success": true,
"message": "ChatGPT Ads account connected successfully",
"account": {
"ad_account_id": "adacct_123",
"name": "Acme Ads",
"currency_code": "USD",
"timezone": "UTC",
"status": "active",
"review_status": "approved",
"default": true,
"connected_at": "2026-09-06T10:12:00+00:00"
}
}

The key is encrypted at rest and is never returned by any endpoint. Re-posting the same account replaces the stored key — that is how you rotate it.

Managing connected accounts​

MethodEndpointDescription
GET/api/uploadposts/ads/chatgpt/accountsConnected ad accounts (no credentials), each with the profiles it is linked to.
GET/api/uploadposts/ads/chatgpt/accountLive metadata for the selected account, straight from OpenAI.
DELETE/api/uploadposts/ads/chatgpt/accounts/{ad_account_id}Forget the stored key and unlink every profile. Campaigns keep running in Ads Manager.
DELETE/api/uploadposts/ads/chatgpt/accounts/{ad_account_id}?profile_username=...Unlink just that profile; the key stays for the others.

Profiles and ad accounts​

An ad account can be linked to a profile, which is how an agency ties each client's advertising to that client's profile — the same place you connect their social accounts. Pass profile_username when connecting, and then on any request:

curl -G https://api.upload-post.com/api/uploadposts/ads/chatgpt/campaigns \
-H 'Authorization: Apikey your-api-key-here' \
--data-urlencode 'profile_username=client-a'
No silent fallback

When a request names a profile, only that profile's linked ad account is used. If the profile has no ad account linked the call returns 404 — it never falls back to your default account, because spending on the wrong client's budget is worse than an error.

The link lives in the profile's own ads_accounts field, never in social_accounts: an ad account is not a publishing destination and never appears as an upload target.

Choosing the ad account​

Without a profile_username, pick one per request in any of these ways (first match wins):

  1. ?ad_account_id=adacct_123 query parameter
  2. X-Ads-Account-Id: adacct_123 header
  3. "ad_account_id": "adacct_123" in the JSON body

Otherwise the account flagged default is used; if there is no default and more than one account, the request returns 400.


Launch an ad in one call​

promote creates the campaign, ad group and ad — and uploads the creative image — in a single request. Use it to promote a post you just published: pass the post's image as image_url and its permalink (or your landing page) as target_url.

Endpoint​

POST /api/uploadposts/ads/chatgpt/promote

Body Parameters (JSON)​

NameTypeRequiredDescription
titleStringYesAd headline, 3–50 characters.
bodyStringYesAd copy, up to 100 characters.
target_urlStringYesDestination URL (http(s)).
image_urlStringYes*Creative image URL. OpenAI fetches it.
file_idStringYes*Already-uploaded creative id. Alternative to image_url.
budgetNumberYes**Lifetime budget in your ad account currency. Minimum 1.
budget_microsIntegerYes**Same value in micros (millionths). 25000000 = 25.00. Alternative to budget.
max_bidNumberYes***Max bid per billing event in account currency (e.g. 0.06 for a $60 CPM).
max_bid_microsIntegerYes***Same value in micros. Alternative to max_bid.
nameStringNoBase name for the three objects. Defaults to title.
statusStringNopaused (default) or active.
bidding_typeStringNoimpressions (default), clicks or conversions.
start_timeInteger/StringNoUnix timestamp or ISO-8601 datetime. Starts immediately when omitted.
end_timeInteger/StringNoUnix timestamp or ISO-8601 datetime.
locationsString[]NoOpenAI location ids (country, region or DMA). All locations when omitted.
context_hintsString[]NoFree-form hints about when your ad is useful, e.g. ["productivity", "team collaboration"].
ad_group_nameStringNoOverride the ad group name.
ad_nameStringNoOverride the ad name.

* One of image_url or file_id. ** One of budget or budget_micros. *** One of max_bid or max_bid_micros.

Example Request​

curl -X POST https://api.upload-post.com/api/uploadposts/ads/chatgpt/promote \
-H 'Authorization: Apikey your-api-key-here' \
-H 'Content-Type: application/json' \
-d '{
"name": "Spring launch",
"title": "Try the workspace planner",
"body": "Coordinate tasks, docs and meetings in one place.",
"target_url": "https://acme.example/planner",
"image_url": "https://acme.example/assets/planner-card.png",
"budget": 25,
"max_bid": 0.06,
"context_hints": ["productivity", "team collaboration"],
"status": "active"
}'

Successful Response (201 Created)​

{
"success": true,
"campaign": { "id": "cmpn_101", "status": "active" },
"ad_group": { "id": "adgrp_301", "status": "active" },
"ad": { "id": "ad_501", "status": "active", "review_status": "in_review" },
"status": "active",
"message": "Ad created. It starts serving once OpenAI's review approves it (check 'review_status')."
}

Everything is built paused and only activated once all three objects exist. If a later step fails, the response carries what was already created so you can finish or archive it:

{
"success": false,
"message": "bid above the account maximum",
"details": { "created": { "campaign_id": "cmpn_101" } }
}

Campaigns​

MethodEndpointDescription
GET/api/uploadposts/ads/chatgpt/campaignsList campaigns.
POST/api/uploadposts/ads/chatgpt/campaignsCreate a campaign.
GET/api/uploadposts/ads/chatgpt/campaigns/{campaign_id}Retrieve one campaign.
POST/api/uploadposts/ads/chatgpt/campaigns/{campaign_id}Update a campaign.
POST/api/uploadposts/ads/chatgpt/campaigns/{campaign_id}/activate | /pause | /archiveChange state.

List endpoints accept limit (1–500, default 20), after, before and order (asc/desc).

Create Body Parameters (JSON)​

NameTypeRequiredDescription
nameStringYes3–1000 characters.
budget / budget_microsNumber/IntegerYesLifetime spend limit. Minimum 1 (1000000 micros).
statusStringNopaused (default) or active.
descriptionStringNoFree text.
start_time / end_timeInteger/StringNoUnix timestamp or ISO-8601 datetime.
bidding_typeStringNoimpressions (default), clicks or conversions. Cannot be changed later.
conversion_event_setting_idsString[]NoExactly one active standard event setting, required for conversions bidding.
locationsString[]NoShorthand for targeting.locations.include.
custom_audience_idsString[]NoShorthand for targeting.custom_audiences.ids.
excluded_custom_audience_idsString[]NoShorthand for targeting.excluded_custom_audiences.ids.
targetingObjectNoFull OpenAI targeting object. Overrides the shorthands above.
curl -X POST https://api.upload-post.com/api/uploadposts/ads/chatgpt/campaigns \
-H 'Authorization: Apikey your-api-key-here' \
-H 'Content-Type: application/json' \
-d '{
"name": "Spring launch",
"budget": 25,
"locations": ["2000043"],
"status": "paused"
}'
{
"success": true,
"campaign": {
"id": "cmpn_101",
"name": "Spring launch",
"status": "paused",
"bidding_type": "impressions",
"budget": { "lifetime_spend_limit_micros": 25000000 }
}
}

On update, every field is optional and status also accepts archived. Archiving is irreversible.


Ad groups​

MethodEndpointDescription
GET/api/uploadposts/ads/chatgpt/adgroups?campaign_id=...List ad groups (campaign_id is required).
POST/api/uploadposts/ads/chatgpt/adgroupsCreate an ad group.
GET/api/uploadposts/ads/chatgpt/adgroups/{ad_group_id}Retrieve one ad group.
POST/api/uploadposts/ads/chatgpt/adgroups/{ad_group_id}Update an ad group.
POST/api/uploadposts/ads/chatgpt/adgroups/{ad_group_id}/activate | /pause | /archiveChange state.

Create Body Parameters (JSON)​

NameTypeRequiredDescription
campaign_idStringYesParent campaign.
nameStringYes3–1000 characters.
max_bid / max_bid_microsNumber/IntegerYesMax bid per billing event. 0.06 = a $60 CPM on impression billing.
billing_event_typeStringNoimpression (default) for impression campaigns, click for click/conversion.
statusStringNopaused (default) or active.
context_hintsString[]NoWhen your product or service is useful, in your own words.
descriptionStringNoFree text.
bidding_configObjectNoFull OpenAI bidding config. Overrides max_bid / billing_event_type.
product_setObjectNoProduct filters for product-feed campaigns.
curl -X POST https://api.upload-post.com/api/uploadposts/ads/chatgpt/adgroups \
-H 'Authorization: Apikey your-api-key-here' \
-H 'Content-Type: application/json' \
-d '{
"campaign_id": "cmpn_101",
"name": "US English",
"max_bid": 0.06,
"context_hints": ["productivity", "team collaboration"]
}'

Creatives​

Upload the image your ad uses and reuse the returned file_id.

POST /api/uploadposts/ads/chatgpt/creatives

Send either JSON with an image_url, or multipart/form-data with a binary file (up to 10 MB).

curl -X POST https://api.upload-post.com/api/uploadposts/ads/chatgpt/creatives \
-H 'Authorization: Apikey your-api-key-here' \
-H 'Content-Type: application/json' \
-d '{"image_url": "https://acme.example/assets/planner-card.png"}'
{ "success": true, "creative": { "file_id": "file_901" } }

Ads​

MethodEndpointDescription
GET/api/uploadposts/ads/chatgpt/ads?ad_group_id=...List ads (ad_group_id is required).
POST/api/uploadposts/ads/chatgpt/adsCreate an ad.
GET/api/uploadposts/ads/chatgpt/ads/{ad_id}Retrieve one ad.
POST/api/uploadposts/ads/chatgpt/ads/{ad_id}Update an ad.
POST/api/uploadposts/ads/chatgpt/ads/{ad_id}/previewTemporary iframe preview (expires in 24 h).
POST/api/uploadposts/ads/chatgpt/ads/{ad_id}/activate | /pause | /archiveChange state.

Create Body Parameters (JSON)​

NameTypeRequiredDescription
ad_group_idStringYesParent ad group.
nameStringYesInternal name, 3–1000 characters. Not shown to users.
titleStringYesHeadline, 3–50 characters.
bodyStringYesCopy, up to 100 characters.
target_urlStringYesDestination URL (http(s)).
file_idStringYesCreative id from /creatives.
statusStringNopaused (default) or active.
creativeObjectNoFull OpenAI creative object. Use it for product_ad_template ads.
curl -X POST https://api.upload-post.com/api/uploadposts/ads/chatgpt/ads \
-H 'Authorization: Apikey your-api-key-here' \
-H 'Content-Type: application/json' \
-d '{
"ad_group_id": "adgrp_301",
"name": "Planner launch card",
"title": "Try the workspace planner",
"body": "Coordinate tasks, docs and meetings in one place.",
"target_url": "https://acme.example/planner",
"file_id": "file_901",
"status": "active"
}'
{
"success": true,
"ad": {
"id": "ad_501",
"name": "Planner launch card",
"status": "active",
"review_status": "in_review",
"creative": {
"type": "chat_card",
"title": "Try the workspace planner",
"body": "Coordinate tasks, docs and meetings in one place.",
"image_url": "https://cdn.openai.com/ads/file_901.png",
"target_url": "https://acme.example/planner"
}
}
}

review_status is in_review, approved or rejected. A rejected ad breaks OpenAI's ads policies; edit it and it goes back into review.


Insights​

EndpointScope
GET /api/uploadposts/ads/chatgpt/insightsAd account
GET /api/uploadposts/ads/chatgpt/campaigns/{campaign_id}/insightsCampaign
GET /api/uploadposts/ads/chatgpt/adgroups/{ad_group_id}/insightsAd group
GET /api/uploadposts/ads/chatgpt/ads/{ad_id}/insightsAd

Query Parameters​

NameDescription
since / untilYYYY-MM-DD shorthand for a date range in the ad account timezone.
time_granularityhourly, daily (default), monthly or none.
aggregation_levelad_account, campaign, ad_group or ad — the row entity inside the endpoint's scope.
fieldsRepeatable. Metrics and metadata to project: impressions, clicks, spend, ctr, cpc, cpm, campaign.name, …
segmentsOne of product, country, device.
filters / sortRepeatable JSON-encoded objects, passed through to OpenAI.
limit1–2000, default 20. Page with after / before.
time_rangesRepeatable JSON-encoded range objects, for windows since/until can't express.

Repeatable parameters accept either spelling — fields=clicks&fields=spend or fields[]=clicks&fields[]=spend — and are always forwarded to OpenAI in the [] form it expects.

curl -G https://api.upload-post.com/api/uploadposts/ads/chatgpt/insights \
-H 'Authorization: Apikey your-api-key-here' \
--data-urlencode 'aggregation_level=campaign' \
--data-urlencode 'time_granularity=daily' \
--data-urlencode 'since=2026-09-01' \
--data-urlencode 'until=2026-09-05' \
--data-urlencode 'fields=impressions' \
--data-urlencode 'fields=clicks' \
--data-urlencode 'fields=spend'
{
"success": true,
"insights": [
{
"campaign_id": "cmpn_101",
"readable_time": "2026-09-01",
"timezone": "UTC",
"impressions": 15548,
"clicks": 312,
"spend": 42.75
}
],
"count": 1,
"has_more": false,
"first_id": "start=1756684800:end=1756771200:entity_id=cmpn_101",
"last_id": "start=1756684800:end=1756771200:entity_id=cmpn_101"
}

Page forward by sending the previous response's last_id as after.


Amounts and micros​

OpenAI expresses money in micros — millionths of your ad account's currency unit. Upload-Post accepts either form:

You sendOpenAI receivesMeaning
"budget": 252500000025.00 lifetime budget
"budget_micros": 2500000025000000same
"max_bid": 0.0660000$60 CPM (per impression)
"max_bid_micros": 6000060000same

All amounts are in the ad account's own currency (currency_code on the connect response).


Errors​

StatusMeaning
400Validation failed, or OpenAI rejected the request. message carries the upstream reason.
401Missing Upload-Post credentials, or the stored OpenAI key was revoked — reconnect the account.
404No ChatGPT Ads account connected, or the campaign/ad group/ad does not exist.
413Creative file larger than 10 MB.
429Rate limited (by Upload-Post or by OpenAI). Retry after a short pause.
502OpenAI Ads is unreachable or returned an unexpected response.
504OpenAI Ads did not respond in time.
{
"success": false,
"message": "ChatGPT Ads rejected the stored API key. Issue a new one in Ads Manager (https://ads.openai.com → Settings → API keys) and reconnect the account."
}