Create a Campaign
Create a ChatGPT chat card campaign with POST /v1/ads/create, and set its budget, status, goal, bidding, conversion event, click tracking and targeting.
A chat card carries a title, a body, an image and a destination URL. So headline, body, imageUrl and linkUrl are all required.
The call creates the campaign, its ad group and the ad in one request.
The sample below runs a lifetime budget until endDate with a fixed bid. Budgets and Goals and bidding cover the daily and automatic options.
const { data: created } = await zernio.adcampaigns.createStandaloneAd({
body: {
accountId: '66b2e19d8c3f5a7e9d0b1c2d',
adAccountId: 'acct_abc123',
name: 'Planner launch',
goal: 'traffic',
budgetAmount: 25,
budgetType: 'lifetime',
endDate: '2027-02-28T23:59:00Z',
bidStrategy: 'LOWEST_COST_WITH_BID_CAP',
bidAmount: 2,
headline: 'Try the new planner',
body: 'Coordinate tasks, docs and meetings in one place.',
imageUrl: 'https://cdn.example.com/planner.png',
linkUrl: 'https://example.com/planner',
countries: ['US']
}
});
console.log(created.ad._id);Response (201):
{
"ad": {
"_id": "66f0a1b2c3d4e5f6a7b8c9d0",
"name": "Planner launch",
"platform": "openai",
"status": "pending_review",
"adType": "standalone",
"goal": "traffic",
"budget": { "amount": 25, "type": "lifetime" }
},
"message": "Ad created"
}budgetAmount and bidAmount are in whole units of the ad account's currency, so 25 is $25.00 on a USD account. currency on the ad and on GET /v1/ads/tree names that currency.
Every sample below reuses the zernio and client constructors from this one.
Budgets
An OpenAI campaign carries exactly one spend cap, set by budgetType:
budgetType | OpenAI field | Notes |
|---|---|---|
daily | daily_spend_limit_micros | Required for automatic bidding (Maximize Results) |
lifetime | lifetime_spend_limit_micros | Send endDate to give the cap its spend window |
The minimum is 1 in the ad account currency, and a lower amount returns 422.
OpenAI can require a higher daily minimum for some currencies. That check runs only on a real create, and the error names the minimum.
Status
status: "PAUSED" creates the ad paused, so you can review it before it spends.
The pause is held on the campaign the call creates, with the ad group and ad switched on. One PUT /v1/ads/campaigns/{campaignId}/status with active brings everything live.
campaignStatus overrides status for the campaign alone. campaignStatus: "ACTIVE" with status: "PAUSED" creates the campaign live, and holds the pause on the new ad group instead, with its ad switched on.
Omitted, or ACTIVE, the ad goes live after OpenAI's review.
Goals and bidding
traffic bids on clicks, awareness on impressions and conversions on conversion events. Any other goal returns 400.
The conversions goal needs an optimizable conversion event on the ad account before the create call. Without one, the request returns 400.
The event comes from a tracking tag with defaultEventType, or from ChatGPT Ads Manager. Choose the conversion event covers picking one per campaign.
bidStrategy picks how the ad group bids:
bidStrategy | OpenAI bidding | Requires |
|---|---|---|
Omitted or LOWEST_COST_WITHOUT_CAP | Maximize Results: maximize_clicks on traffic, maximize_conversions on conversions | budgetType: "daily" |
LOWEST_COST_WITH_BID_CAP or COST_CAP | A fixed bid (fixed_bid), the CPA bid under conversions | bidAmount |
bidAmount in whole currency units is converted to micros.
Maximize Results on a lifetime budget, or on the awareness goal, returns 400 with code missing_required_field. Send a fixed bid there.
LOWEST_COST_WITH_MIN_ROAS returns 422, because OpenAI has no ROAS-based bidding.
This body bids automatically on a daily budget:
{
"accountId": "66b2e19d8c3f5a7e9d0b1c2d",
"adAccountId": "acct_abc123",
"name": "Planner always-on",
"goal": "traffic",
"budgetAmount": 40,
"budgetType": "daily",
"headline": "Try the new planner",
"body": "Coordinate tasks, docs and meetings in one place.",
"imageUrl": "https://cdn.example.com/planner.png",
"linkUrl": "https://example.com/planner",
"countries": ["US"]
}Choose the conversion event
On goal: "conversions", promotedObject.customEventType picks the event the campaign optimizes for.
Read the candidates from conversionEvents[] on each pixel returned by GET /v1/accounts/{accountId}/conversion-destinations (List conversion destinations):
{
"conversionEvents": [
{ "id": "evs_123", "name": "Purchase", "eventType": "order_created", "customEventName": null, "archived": false, "optimizable": true }
]
}Pass the event's id (exact match), its eventType such as order_created, or its name such as Purchase (both case-insensitive):
{
"goal": "conversions",
"promotedObject": { "customEventType": "order_created" }
}Only optimizable events qualify, meaning active and standard rather than custom.
- An unknown, archived or custom event returns
400listing the account's optimizable events. - A type or name shared by several of them returns
400asking for theid.
Omit customEventType and the campaign optimizes for the account's most recently created optimizable event.
customEventType is accepted only with the conversions goal. Every other promotedObject field returns 400 on OpenAI.
Click tracking
tracking.urlTags becomes the ad's landing_page_configuration.query_string_template. OpenAI appends it to linkUrl on click.
OpenAI's macros {campaign_id}, {ad_group_id}, {ad_id} and {oppref} (the click id) pass through raw, so OpenAI expands them. Every other character is percent-encoded.
OpenAI expands macros here, not inside linkUrl.
{
"tracking": {
"urlTags": [
{ "key": "utm_source", "value": "chatgpt" },
{ "key": "utm_campaign", "value": "{campaign_id}" },
{ "key": "click_id", "value": "{oppref}" }
]
}
}Validate before creating
validateOnly: true checks the request and creates nothing.
OpenAI has no dry-run of its own. Zernio runs every check it knows (creative lengths, budget, bid strategy, targeting), plus live lookups of the conversion event and the target countries.
The response is 200 with a validated result per node. On the conversions goal, the message names the event the campaign would optimize for:
{
"validateOnly": true,
"results": [
{ "node": "campaign", "status": "validated" },
{ "node": "adSet", "status": "validated" },
{ "node": "ad", "status": "validated" }
],
"message": "Validated by Zernio against OpenAI's documented limits, plus live lookups of the conversion event and target countries; nothing was created. ..."
}OpenAI's own write-time checks (image fetch, currency-specific budget and bid minimums, ad review) still run only on a real create.
POST /v1/ads/campaigns accepts validateOnly: true for a campaign-only check as well.
Targeting
Targeting is by country.
Every other targeting field returns 400 rather than being dropped silently. That includes ages, genders, interests, audienceInclude and audienceExclude.