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.comSettings → 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.
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).
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. Campaigns keep running in Ads Manager.

Choosing the ad account

With one connected account everything just works. With several, 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."
}