Zernio
APIFor you
Zernio
APIFor you
API Reference

Campaigns and Ads

Overview

Get campaign treeGETGet daily account metricsGET

Campaigns

List campaignsGETCreate a standalone campaignPOSTGet live campaign detailsGETUpdate a campaignPUTDuplicate a campaignPOSTDelete a campaignDELETEPause or resume a campaignPUTPause or resume many campaignsPOSTList campaign assetsGETAttach campaign assetsPOSTUpdate campaign assetsPUTRemove campaign assetsDELETERead a Google campaign's device, location, excluded location, and language targetingGETEdit a Google campaign's device, location, excluded location, or language targetingPUTRead a campaign's ad schedule (dayparting)GETReplace a campaign's ad schedule (dayparting)PUTGet campaign conversion goalsGETUpdate campaign conversion goalsPATCH

Asset Groups

List Performance Max asset groupsGETCreate a Performance Max asset groupPOSTGet a Performance Max asset groupGETUpdate a Performance Max asset groupPATCHRemove a Performance Max asset groupDELETELink or unlink asset group assetsPOSTReplace an asset group's listing-group treePUT

Ad Sets

List ad setsGETCreate a standalone ad groupPOSTGet live ad-set detailsGETUpdate an ad setPUTDuplicate an ad setPOSTDelete an ad setDELETEPause or resume a single ad setPUTList ad-group assetsGETAttach ad-group assetsPOSTUpdate ad-group assetsPUTRemove ad-group assetsDELETE

Ads

List adsGETGet ad detailsGETCreate standalone adPOSTBoost post as adPOSTUpdate adPUTDuplicate an adPOSTCancel an adDELETEPause or resume a single adPUT

Keywords

List Search keywordsGETAdd Search ad-group keywordsPOSTPause or enable a Search keywordPATCHRemove a Search keywordDELETEList campaign-level negative keywordsGETReplace campaign-level negative keywordsPUTList campaign negative listsGETReplace campaign negative listsPUT

Bidding

Read a campaign's current biddingGETList portfolio bid strategiesGETCreate portfolio bid strategyPOSTUpdate portfolio bid strategyPATCH

Recommendations

List Google Ads recommendationsGETApply Google Ads recommendationsPOSTDismiss Google Ads recommendationsPOST

Other

Create a shared budgetPOSTRead the platform's review verdict for an adGETReview status of each creative in a TikTok Smart+ adGETList shared budgetsGETReview status of every TikTok Smart+ ad of an advertiserGET
Dashboard
OverviewPlatformsAPI ReferenceResources
Campaigns and Ads

Create standalone ad

Creates a paid ad with your own creative, building the campaign and ad set it needs or adding it to existing ones.

Create a paid ad with custom creative across Meta, Google Ads, Pinterest, TikTok, X, LinkedIn, OpenAI Ads (ChatGPT Ads) and Reddit Ads.

Google Performance Max: set campaignType: "pmax" and supply assetGroup with text, images by role, business name and finalUrl. Creates a daily budget, PAUSED campaign and asset group atomically. validateOnly: true validates the complete request with Google without creating or persisting resources. Read assets with GET /v1/ads/campaigns/{campaignId}/asset-groups. The logo is required; video is optional via assetGroup.youtubeVideoId. Brand guidelines are disabled at creation. All supplied asset links are validated together against Google's minimum asset requirements. PMax rejects ACTIVE creation, portfolio bidding, bid caps, legacy creative fields and attach shapes. Geo and language targeting are supported; omitted geo targets all locations. PMax does not require top-level goal, headline, body or linkUrl. Supported bidding: omitted or LOWEST_COST_WITHOUT_CAP for Maximize Conversions, COST_CAP plus bidAmount for target CPA, LOWEST_COST_WITH_MIN_ROAS plus roasAverageFloor for Maximize Conversion Value with target ROAS.

Google Demand Gen: set campaignType: "demand_gen" and supply demandGen. Creates a daily budget, PAUSED campaign, one ad group and one ad in a single atomic request: a multi-asset image ad, a video responsive ad when demandGen.youtubeVideoIds is sent, or a carousel ad (2 to 10 cards) when demandGen.carouselCards is sent. Geo (countries, regions, cities, zips, metros) and languages go on the ad group, as Demand Gen requires. demandGen.channels sets the ad group's channel controls and demandGen.audience builds a Google Audience (user lists, interests, custom audiences, age ranges, genders) attached to the ad group, or demandGen.audienceId attaches an existing one. validateOnly: true validates everything with Google without creating resources. Supported bidding: omitted or LOWEST_COST_WITHOUT_CAP for Maximize Conversions, COST_CAP plus bidAmount for target CPA, LOWEST_COST_WITH_MIN_ROAS plus roasAverageFloor for target ROAS, LOWEST_COST_WITH_BID_CAP plus bidAmount for target CPC. The created ad carries the native platformCampaignId, platformAdSetId (ad group) and platformAdId. Activate with PUT /v1/ads/campaigns/{campaignId}/status; budget, bidding and name are edited with the regular campaign endpoints; creative, channels, audience and ad group targeting with PUT /v1/ads/{adId} (demandGen, targeting). To grow an existing Demand Gen campaign, send existingCampaignId (adds a PAUSED ad group, with its own geo, languages, channels and audience, plus its ad) or adSetId (adds a PAUSED ad to that ad group); neither takes budget, bidding or schedule fields, and both support validateOnly.

Other mutually-exclusive request shapes are selected by the body:

  • Legacy single-creative shape (all platforms, the default).
  • Meta-only multi-creative shape via the creatives array: one ad set with N ads sharing budget and targeting.
  • Attach shape via adSetId: adds one new ad to an existing ad set, inheriting its budget, targeting, and schedule (Meta, Google Ads, TikTok, and LinkedIn). On LinkedIn adSetId is the existing Campaign id, and the budget, schedule, targeting and bidding fields must be omitted. On LinkedIn goal may be omitted too (taken from the Campaign's objective), and the created ad's targeting echoes the Campaign's audience.

Meta accepts creativeFeatures on the single and attach shapes and as defaults for creatives[]; an item replaces the whole feature map. promotion is not supported on any shape and any object is rejected with 400. Reusing existingCreativeId uses the existing creative settings instead of new settings. Requested settings are persisted for lists, exports, and default ad-detail reads.

Per-platform required fields, budget minimums, and video-ad rules are documented on each property below.

Meta required fields per shape (a missing one is a 400 naming it in param):

  • Every shape that builds a new ad set (no adSetId, existingCampaignId or buyingType: "RESERVED"): budgetAmount and budgetType, plus endDate for a lifetime budget.
  • Single image or video (the default), also with adSetId: headline (max 255), body, callToAction, exactly one of imageUrl / video (none for a catalog template creative, which ignores and does not upload any sent), and linkUrl unless the goal is lead_generation or page_visits or leadGenFormId is set.
  • bodies[] / headlines[] text variations: body, one list with 2+ items, callToAction, imageUrl or video, and linkUrl unless the goal is lead_generation or leadGenFormId is set. headline is not required.
  • placementAssets: headline, body, callToAction, and linkUrl with the same exemptions as the single shape; the media comes from placementAssets.
  • carouselCards: body, callToAction, and linkUrl unless the goal is lead_generation or leadGenFormId is set. headline is not required.
  • creatives[]: per entry headline, body, callToAction, imageUrl or video, and linkUrl unless the goal is lead_generation. Top-level body is not required.
  • dynamicCreative: imageUrls or videoUrls, callToActionTypes, and linkUrls unless the goal is lead_generation. Top-level body is not required.
  • existingCreativeId: no creative fields. Goal page_likes: body only.
  • Goal lead_generation always needs leadGenFormId, except with existingCreativeId.

LinkedIn creates a Single Image or Single Video Ad backed by a Direct Sponsored Content "dark post" authored by a Company Page (see organizationId). Supported goals are engagement, traffic, awareness, and video_views (video ads use the video field; video_views requires a video), and traffic ads require linkUrl.

Reddit Ads (accountId of a redditads connection, adAccountId like a2_abc123) creates a campaign, an ad group carrying the budget, schedule, targeting and pixel, and an ad running a new promoted post authored by the ad account's first profile (422 when it has none). The creative format follows the body: carouselCards builds a carousel, video a video ad, imageUrl an image ad, and none of them a text ad, which cannot link out (linkUrl is a 400 there; body is required). Only the legacy single-creative shape is available: creatives[], adSetId, existingCampaignId and validateOnly are refused. Reddit throttling surfaces as 429 rate_limited with platform: reddit; retry after a delay. To promote an existing Reddit post use POST /v1/ads/boost.

Idempotency: this endpoint is not idempotent at the platform level (a blind retry creates a second campaign/ad set/ad). Send an Idempotency-Key header to make retries safe: the first request with a given key creates the ad and we store the response; a retry with the same key replays that exact response (with Idempotent-Replayed: true) instead of creating duplicates. Reusing a key with a different body returns 422; a key whose first request is still in flight returns 409 (retry after a short backoff). If the first request is cut off mid-create (for example by a function timeout), its key can stay in progress and keep returning 409 until it expires; use a new key to retry sooner, after checking whether the ad was created. Keys are scoped to your credential and expire after 24h.


PlatformsMetaGoogleTikTokLinkedInPinterestXOpenAI
POST
/v1/ads/create

Authorization

bearerAuth
AuthorizationBearer <token>

API key authentication: send your Zernio API key in the Authorization header, prefixed with Bearer.

In: header

Header Parameters

Idempotency-Key?string

Optional client-generated unique key (e.g. a UUID) that makes retries safe. Same key + same body replays the original response; same key + different body → 422; key still processing → 409. A request cut off mid-handler (for example by a function timeout) can leave its key in progress, answering 409 until the key expires after 24 hours.

Lengthlength <= 255

Request Body

application/json

TypeScript Definitions

Use the request body type in TypeScript.

Response Body

application/json

application/json

application/json

application/json

application/json

application/json

application/json

import Zernio from '@zernio/node';const zernio = new Zernio({ apiKey: process.env.ZERNIO_API_KEY });const { data } = await zernio.adcampaigns.createStandaloneAd({  body: {    accountId: 'account_abc123',    adAccountId: 'adaccount_abc123',    name: 'Example',  },});console.log(data);
{  "validateOnly": true,  "results": [    {      "node": "campaign",      "status": "validated",      "reason": "string"    }  ],  "message": "string"}
Was this page helpful?

Get ad details

Shows one ad with its creative, targeting, status and performance metrics. Google Search ads include current creative.headlines, creative.descriptions and creative.finalUrls, preserving pinnedField. Top-level cachedAt and stale report cache freshness. Google mutations invalidate this read. RSA enrichment requires a stored advertisingChannelType of SEARCH. Ads with an unknown or other channel return their stored details without a Google read. If RSA enrichment fails, the stored ad is returned with HTTP 200 and without cache metadata. The `{adId}` path segment accepts any identifier dialect Zernio indexes for the ad: - the Zernio internal `_id` (24-char hex) - Meta's numeric `platformAdId` (the value shipped in `comment.received` webhooks as `comment.ad.id`) - the creative's `effective_object_story_id` (`{pageId}_{postId}` shape, Facebook side) - the creative's `effective_instagram_media_id` (Instagram side) - an Instagram media id an ad comment arrived on (`comment.platformPostId` of a `comment.received` carrying `comment.ad.id`), including the extra media Meta creates for placement variants Any of these resolve to the same ad. Caller doesn't need a translation step. `creative.creativeFeatures` holds the stored requested settings, which do not confirm platform application. **Facebook ad dark posts.** Meta gives an ad with per-placement assets one extra unpublished post per placement, and comments land on it. Meta exposes no link from that post to its ad. When exactly one ad on the Page, created before the dark post, carries the same copy as the post, the dark post id resolves to that ad. Otherwise, when the id is a dark post Zernio has seen comments on and its Page is connected, the response is `ad: null` plus `darkPost`, which carries the Meta Ads account and the ad accounts running ads for that Page. Read its comments with GET /v1/ads/{adId}/comments using the same id. **Status freshness.** `status`, `configuredStatus`, `platformStatus`, `platformAdSetStatus` and `platformCampaignStatus` are the values Zernio last stored. Background sync refreshes them, typically within 15 to 60 minutes (Google up to about 3 hours), and ended or long-paused objects may be refreshed less often. Zernio's own status writes re-read the switches they change. A change made in the platform's own ads manager therefore shows up here only after the next sync. Controllers that act on a switch should pass `live=true`, which reads the switches from the platform now, stores them, and returns `statusReadAt` (null when the read failed and the stored values were returned). With `live=true` the ad's own switch (`configuredStatus`), its delivery status (`status`, `platformStatus`) and its ad set and campaign switches (`platformAdSetStatus`, `platformCampaignStatus`) are read live.

Boost post as ad

Turns an already published post into a paid ad that keeps the post's likes and comments, creating the campaign around it. By default it provisions the whole hierarchy (campaign, ad set, ad). **Attach shape (Meta).** Send `adSetId` to put the ad under an EXISTING ad set instead, so that ad set keeps its learning phase. It then owns `budget`, `schedule` and `targeting`, and sending any of those alongside `adSetId` is a 400 rather than a silent drop. `budget` is required only without `adSetId`. `instagramAccountId`, `destinationType`, `whatsappPhoneNumber` and `adSetId` are Meta-only and return 400 on other platforms. `accountId` may be a Facebook, Instagram or Meta ads (business login) connection. A business-login connection has no posting account, so pass the post as `platformPostId` (Facebook `pageId_postId` or an Instagram media id); a Zernio `postId` is a 400 there. For X, `accountId` may be the posting X account or its X Ads connection: either resolves the other on the same profile. A Zernio `postId` on an X Ads connection resolves through its X account, and is a 400 when the profile has none (pass `platformPostId` instead). **Reddit.** `accountId` is the Reddit Ads connection (platform `redditads`). Pass the Reddit post as `platformPostId` (`t3_abc123` or the bare id), or a Zernio `postId`, which resolves through the Reddit account connected on the same profile (400 when there is none). Reddit promotes only posts made by a profile of the ad account: a post by any other author returns 422. Goals are traffic, awareness, conversions, video_views (the post must be a video, 422 otherwise) and lead_generation; conversions and lead_generation need a pixel that already received the event (see `promotedObject`). `leadGenFormId` returns 400: Reddit retired its native lead forms, so `lead_generation` sends people to `linkUrl` and optimizes toward the pixel's Lead event. Each boost creates a campaign, an ad group carrying the budget, the targeting and the bid, and the ad. `bidStrategy` sets the ad group's bid: LOWEST_COST_WITHOUT_CAP (or omitted) is automatic bidding (BIDLESS), LOWEST_COST_WITH_BID_CAP is MANUAL_BIDDING with `bidAmount` as the bid, COST_CAP is MAXIMIZE_VOLUME with `bidAmount` as the cap; any other strategy returns 422 before anything is created. **Messaging boosts (Meta).** Use `goal: engagement` with `callToAction: WHATSAPP_MESSAGE`, `MESSAGE_PAGE`, or `INSTAGRAM_MESSAGE`. The CTA implies WHATSAPP, MESSENGER, or INSTAGRAM_DIRECT respectively; `destinationType` alone does not select a messaging CTA. Omit `linkUrl` only for messaging CTAs. Plain link CTAs keep their goal and link behavior when combined with an independent `destinationType`. The campaign uses OUTCOME_ENGAGEMENT and the ad set uses CONVERSATIONS with the promoted Page. Optional `whatsappPhoneNumber` selects a number already paired with that Page. Conflicting CTA/destination, instant form, goal, or optimizationGoal inputs return 400. Attach requires the target ad set destination to match. Existing post references preserve social proof; an Instagram reel rejected by Meta is not re-uploaded as a new post for a messaging boost. **Retries.** Boosts are NOT idempotent and can take minutes when Meta requires re-hosting an Instagram video, so do not retry on client timeout. Send an Idempotency-Key header to make retries safe: same key and body replays the original 201, and distinct keys always create distinct ads. Without the header, an identical request is treated as a retry: while one is in flight it returns 409, and within 10 minutes of a completed boost it returns the already-created ad instead of creating another. To intentionally duplicate an ad, send distinct Idempotency-Keys (or vary the body, e.g. the name).

accountId*string
adAccountId*string
name*string
Lengthlength <= 255
campaignName?string

Meta and Reddit. Exact campaign name. Overrides the default <name> - Campaign.

Lengthlength <= 255
adSetName?string

Meta and Reddit (the ad group). Exact ad set name. Overrides the default <name> - Ad Set. (For per-ad names on the multi-creative shape, set name on each creatives[] entry.)

Lengthlength <= 255
adName?string

Meta only. Exact ad name (the single-creative ad object's name). Overrides the default, which is name. (For per-ad names on the multi-creative shape, set name on each creatives[] entry instead.)

Lengthlength <= 255
tracking?

Meta, plus urlTags on ChatGPT (OpenAI) and pixelId on Reddit. Meta: attaches pixel measurement to the ad regardless of the optimization goal (the "Website events" tracking row in Ads Manager). pixelId becomes the ad's tracking_specs (offsite_conversion + fb_pixel); urlTags is stored on the new creative as url_tags and retained on the ad for compatibility. Applied on the legacy single-creative shape, every ad of the multi-creative shape, and the attach shape. NOTE: tracking lives on the AD object and is not inherited from the ad set, so pass it on EVERY attach call that should carry the pixel. ChatGPT (OpenAI): urlTags becomes the ad's landing_page_configuration.query_string_template, which OpenAI appends to linkUrl on click. Reddit: pixelId picks the pixel of the new ad group, like promotedObject.pixelId (which wins when both are sent).

goal?string

Required on legacy and multi-creative shapes; the attach shape inherits it from the ad set. Available goals vary by platform.

Meta

  • conversions: OUTCOME_SALES. Requires promotedObject.pixelId and promotedObject.customEventType with a commerce event such as PURCHASE or START_TRIAL, or promotedObject.customConversionId to optimise against a Custom Conversion, or customEventType: OTHER + customEventStr to optimise against a pixel custom event.
  • lead_conversion: OUTCOME_LEADS optimizing website pixel leads. Same pixel and event fields, but with a leads-class event such as LEAD, SUBMIT_APPLICATION, SCHEDULE or CONTACT (or promotedObject.customConversionId to optimise against a Custom Conversion instead). Meta gates conversion events by objective, so leads-class events are rejected under conversions.
  • lead_generation: OUTCOME_LEADS with instant forms. Requires leadGenFormId. promotedObject.pageId is optional and auto-filled from the connected Page.
  • app_promotion: requires promotedObject.applicationId and promotedObject.objectStoreUrl.
  • catalog_sales: Advantage+ catalog ads, for example vehicle inventory. Requires promotedObject.productSetId, promotedObject.pixelId and promotedObject.customEventType. Builds a catalog TEMPLATE creative from the copy fields, which may carry template tags like {{product.name}} or {{vehicle.make}}. No imageUrl or video is sent; Meta renders the visuals per catalog item. Discover catalogs via GET /v1/ads/catalogs and product sets via GET /v1/ads/catalogs/{catalogId}/product-sets. Single shape only, no creatives[], dynamicCreative, placementAssets, existingCreativeId or carouselCards; an imageUrl or video sent with it is ignored and not uploaded. Works with existingCampaignId, and with adSetId to add a catalog ad to an existing ad set (then only promotedObject.productSetId is required; pixel and event stay on the ad set). Pick the format with catalogFormat.
  • traffic with promotedObject.productSetId: a catalog ad without a pixel (OUTCOME_TRAFFIC, optimizationGoal LINK_CLICKS by default or LANDING_PAGE_VIEWS). The product set goes on the template creative only, never on the ad set's promoted_object, and no pixelId or customEventType is needed. Same creative rules as catalog_sales.
  • page_likes: Page Likes conversion location under OUTCOME_ENGAGEMENT (destination_type ON_PAGE, optimization PAGE_LIKES). promotedObject.pageId is optional and auto-filled from the connected Page. The creative CTA is fixed to LIKE_PAGE targeting that Page; headline / body / linkUrl / callToAction / imageUrl / video are all optional (Meta derives the link and the Like button from the Page).
  • page_visits: Meta's "Page visits and followers" under OUTCOME_TRAFFIC (destination_type FACEBOOK_PAGE, optimization PROFILE_VISIT). promotedObject.pageId is optional and auto-filled from the connected Page, and linkUrl defaults to the Page. Meta enables Profile Visit ads per ad account: one it has not enabled is refused with Meta's "This account isn't eligible to use Profile Visit ads yet".

TikTok

  • conversions: website-conversion ad group. Requires promotedObject.pixelId, your TikTok Pixel ID. Accepts an optional promotedObject.customEventType with a TikTok optimization_event code your pixel tracks (newer pixels use e.g. SHOPPING for purchase events; legacy pixels use ON_WEB_ORDER, INITIATE_ORDER, ON_WEB_REGISTER or FORM). To inherit pixel and event from an existing ad group, pass adSetId instead.

LinkedIn

  • engagement, traffic, awareness and video_views create standalone Direct Sponsored Content ads. traffic requires linkUrl; video_views requires video.
  • lead_generation: requires leadGenFormId (an adForm ID from POST /v1/ads/lead-forms). The campaign objective is set to MAX_LEAD and the creative's leadgenCallToAction destination is set to urn:li:adForm:{id}.
  • job_applicants requires a platformSpecificData.jobs creative.
  • For conversions on LinkedIn, or to promote an existing post, use POST /v1/ads/boost.

OpenAI Ads

  • Only traffic, awareness, and conversions are supported (other goals return 400). Maps to OpenAI's bidding_type (clicks, impressions, conversions respectively). conversions requires an active conversion event setting on the account; create a tracking tag with defaultEventType via the tracking-tags API (POST /v1/accounts/{accountId}/tracking-tags), or configure a conversion event in OpenAI Ads Manager, or the request returns 400. Pick the event with promotedObject.customEventType (see AdPromotedObject); without it the most recently created optimizable event is used.

Reddit Ads

  • traffic (CLICKS), awareness (BRAND_AWARENESS), conversions, video_views (VIDEO_VIEWABLE_IMPRESSIONS) and lead_generation (LEAD_GENERATION); any other goal returns 400.
  • conversions requires promotedObject.customEventType; the pixel must already have received that event (see AdPromotedObject).
  • lead_generation sends people to linkUrl and optimizes toward the pixel's Lead event, which the pixel must already have received. Reddit retired its native lead forms on September 30, 2026, so leadGenFormId returns 400.
  • video_views needs a video (422 otherwise); optimizationGoal picks VIDEO_VIEW_6S (default) or VIDEO_VIEW_15S.

Value in

  • "engagement"
  • "traffic"
  • "awareness"
  • "video_views"
  • "lead_generation"
  • "lead_conversion"
  • "conversions"
  • "app_promotion"
  • "catalog_sales"
  • "page_likes"
  • "page_visits"
  • "job_applicants"
smartTargeting?

TikTok only (a 400 elsewhere). TikTok Smart Targeting on the ad group: audience is TikTok's smart_audience_enabled, interestsBehaviors its smart_interest_behavior_enabled. When on, TikTok may deliver beyond the selected audiences or interests. Available on Video views, Traffic, Lead generation, App install, Web conversion and Community interaction. Only the flags you send are written; an unwritten flag reads back null in nativeSettings, so send false explicitly to be able to verify it is off. Not available with smartPlus or when attaching to an existing ad set (adSetId).

optimizationGoal?string

Meta, TikTok with goal video_views (ENGAGED_VIEW, the 6-second default, or ENGAGED_VIEW_FIFTEEN; both bill per view), or Reddit with goal video_views (VIDEO_VIEW_6S, the default, or VIDEO_VIEW_15S; other values return 400). Meta: Explicit ad-set optimization_goal (e.g. LANDING_PAGE_VIEWS, LINK_CLICKS, REACH, IMPRESSIONS, OFFSITE_CONVERSIONS, THRUPLAY, LEAD_GENERATION). Overrides the default derived from goal (e.g. traffic defaults to LINK_CLICKS). Forwarded verbatim to Meta, which validates compatibility with the campaign objective and rejects incompatible combinations.

billingEvent?string

Meta only. Explicit ad-set billing_event. Defaults to IMPRESSIONS. Forwarded verbatim to Meta, which validates compatibility with the optimization goal.

buyingType?string

Meta only. Defaults to AUCTION and is explicitly sent on new campaigns, including validateOnly. Reusing existingCampaignId does not change the campaign. RESERVED = Reach & Frequency: requires rfPredictionId (a RESERVED prediction from /v1/ads/rf-predictions + /reserve). Budget, schedule and pricing come from the reservation, so budgetAmount/budgetType are not required and bid fields are ignored. Only the plain single-ad shape (no creatives[], adSetId, existingCampaignId or dynamicCreative).

Default"AUCTION"

Value in

  • "AUCTION"
  • "RESERVED"
rfPredictionId?string

Meta only. The RESERVED prediction id the R&F ad set runs on (reserving mints a new id, so pass that one). Requires buyingType RESERVED.

promotion?|

Not supported. Meta validates creative_sourcing_spec.promotion_metadata_spec on the create call and then discards it, so a Promotion set through the Marketing API never reaches the creative. Any object is rejected with 400 invalid_field_value. Send null or omit the field, and set the Promotion on the ad in Ads Manager. Verified on 2026-09-11 across Graph v19.0 to v25.0 and every write path.

creativeFeatures?

Meta only. Applied to each new creative, including standalone, attach and catalog template shapes. With creatives[], these are defaults; an item replaces the whole feature map, including an empty map. auto_promotion_tag is an Advantage+ enhancement, not the Ads Manager Promotion setting. On catalog ads, the catalog enhancements are keys of this same map, for example { "product_extensions": "OPT_OUT", "media_type_automation": "OPT_OUT", "standard_enhancements_catalog": "OPT_OUT", "multi_creative_post_carousel": "OPT_OUT" }.

catalogFormat?string

Meta catalog template creatives only (goal catalog_sales, goal traffic with promotedObject.productSetId, or adSetId with promotedObject.productSetId); a 400 otherwise. Sent as object_story_spec.template_data.format_option. force_single_link is never sent with it (Meta rejects the pair, subcode 1443051). single_image also sends format_transformation_spec with carousel and da_collection set to data_source: ["none"], so Meta keeps the single image instead of turning it into a carousel or collection. carousel_images_single_item also sends template_data.multi_share_end_card: false, which Meta requires for that format (subcode 1443167). Omit it to let Meta pick the format.

Value in

  • "single_image"
  • "carousel_images_multi_items"
  • "carousel_images_single_item"
  • "carousel_slideshows"
multiAdvertiser?string

Meta only. Multi-advertiser ads: whether Meta may show this ad alongside other advertisers' in one unit. Meta auto-enrols since Aug 2024, so send OPT_OUT to leave. It is a top-level creative field, NOT a creativeFeatures key, and Meta rejects it there.

Value in

  • "OPT_IN"
  • "OPT_OUT"
aiDisclosure?string

Meta only. Meta's "Ad includes media created or edited with AI" disclosure, the checkbox in Ads Manager, stored on the creative as generative_asset_spec.transparency_metadata.self_disclosure. OPT_IN checks it, OPT_OUT explicitly declares no AI media, omitted leaves Meta's default. Applied to each new creative, including standalone, creatives[] and attach shapes, and preserved when a creative is rebuilt. This sets the disclosure on the ad; whether and when the viewer-facing label renders is Meta's decision.

Value in

  • "OPT_IN"
  • "OPT_OUT"
validateOnly?boolean

Google Performance Max validates the complete atomic campaign and asset group with no resource creation or local persistence. Google validation still downloads image URLs and consumes quota. On Meta, validates the complete inline campaign, ad set, creative and ad with execution_options validate_only. Nothing is uploaded or created, and validation bypasses Idempotency-Key storage. Supports a single image or existing video.id (each also with bodies[] / headlines[] Multiple Text Options), all-image placementAssets with per-rule copy and text pools, or existingCreativeId; dynamicCreative, translations, carouselCards, placement videos, new video uploads, creatives[], adSetId and RESERVED buying return 400. Placement validation uses existing Instagram identities only. Existing campaign or creative nodes are marked skipped. When Meta cannot dry-run the nested tree (code 2, or an unknown-error code 1 on an OFFSITE_CONVERSIONS ad set nested in a new campaign, which Meta returns for some website-conversion ad sets that it then creates normally), the campaign and creative are validated on their own and the adSet and ad nodes come back unverified with the reason. Success returns 200 with per-node results; Meta rejection returns an error. ChatGPT (OpenAI) has no platform dry-run: Zernio runs every check it knows (creative lengths, budget, bid strategy, targeting) plus live lookups of the conversion event and target countries, and uploads or creates nothing. OpenAI's own write-time checks (image fetch, currency-specific minimums, ad review) still run only on a real create. Any other platform, or a Google campaignType other than pmax or demand_gen, returns 501 feature_not_available.

budgetAmount?number

Budget in WHOLE currency units (USD: 50 = $50.00), NOT cents. Meta's own Marketing API takes this same number in minor units, so it is an easy and expensive mix-up. Required on legacy, multi-creative and Performance Max shapes. Inherited on attach. OpenAI Ads: in the ad account currency, minimum 1; OpenAI can require a higher daily minimum for some currencies and names it in the error.

budgetType?string

Required on legacy, multi-creative and Performance Max shapes. Inherited on attach. OpenAI Ads accepts both as the campaign's single spend cap. A lifetime cap can later switch to daily with PUT /v1/ads/campaigns/{campaignId}, but OpenAI never switches a daily cap back to lifetime (422). Automatic bidding (Maximize Results) needs a daily budget. Reddit Ads: the ad group's daily or lifetime spend goal; lifetime needs endDate.

Value in

  • "daily"
  • "lifetime"
status?string

Publish state of the created entities, on every platform. Omitted or ACTIVE publishes live (default, back-compat); PAUSED pauses only the TOP-MOST object this call creates and switches everything below it on, so one resume of that object brings the new tree live and nothing that already existed is touched: a new campaign is held paused with its ad set and ad on; with existingCampaignId the new ad set is held paused with its ad on; with adSetId the new ad itself is paused. campaignStatus: ACTIVE with status: PAUSED moves the pause down to the new ad set. campaignStatus, adSetStatus and adStatus set one level each and always win for that level; status: PAUSED adds a hold of its own only when none of them is PAUSED. To create every object paused, send all three as PAUSED. LinkedIn: a held campaign group is PAUSED with its campaign and creative ACTIVE; a new campaign in an existing group stays DRAFT; a held creative (with adSetId) is created DRAFT, as LinkedIn refuses PAUSED until its review is APPROVED. X has no per-ad switch, so its lowest level is the line item. Google Performance Max and Demand Gen accept PAUSED only (the campaign is created paused).

Value in

  • "ACTIVE"
  • "PAUSED"
campaignStatus?string

Every platform. Sets the switch of the new campaign alone (LinkedIn: the campaign group) and overrides status for it. PAUSED holds the campaign off with its ad set and ad on unless adSetStatus or adStatus say otherwise; ACTIVE with status: PAUSED switches the campaign on and holds the new ad set paused (its ad on). Omitted, it follows status. Ignored when the request creates no campaign (existingCampaignId or adSetId).

Value in

  • "ACTIVE"
  • "PAUSED"
adSetStatus?string

Every platform. Sets the switch of the new ad set alone (Google, TikTok, Pinterest and ChatGPT: the ad group; LinkedIn: the campaign, which stays DRAFT when held; X: the line item) and overrides status for it. Omitted, it follows status.

Precedence: a level status (campaignStatus, adSetStatus, adStatus) always wins for its level. status: PAUSED then holds the top-most new object that has no level status, and only when no level status is PAUSED; every other new object is switched on. So campaignStatus: ACTIVE + adSetStatus: PAUSED + adStatus: PAUSED keeps the campaign on with the new ad set and ad off, and all three PAUSED creates the whole tree paused.

Rejected with a 400 alongside adSetId (that ad set already exists; change it with PUT /v1/ads/ad-sets/{adSetId}/status). Performance Max and Demand Gen accept PAUSED only.

Value in

  • "ACTIVE"
  • "PAUSED"
adStatus?AdActivationStatus

Sets the switch of the new ad alone, also when attaching to an existing ad set with adSetId, and overrides status for it. Same precedence as adSetStatus. Omitted, it follows status. LinkedIn: a creative held off is created DRAFT, because LinkedIn refuses PAUSED on a creative until its review is APPROVED. A DRAFT creative is not submitted for review and cannot serve; it reads paused, and resuming the ad (PUT /v1/ads/{adId}/status active) switches it ACTIVE, which starts the review.

X returns a 400: a promoted post has no switch of its own, so hold the line item with adSetStatus. PAUSED with buyingType: RESERVED returns a 400 (Meta creates the first Reach and Frequency ad ACTIVE). Performance Max and Demand Gen accept PAUSED only.

Value in

  • "ACTIVE"
  • "PAUSED"
budgetLevel?string

Meta only. Where the budget lives, which selects the Meta budget model:

  • adset (default): ABO (Ad-set Budget Optimization). The budget is set on the ad set. This is the back-compatible behaviour; omit this field to keep it.
  • campaign: CBO (Campaign Budget Optimization / Advantage Campaign Budget). The budget AND bidStrategy are set on the CAMPAIGN, and Meta distributes spend across ad sets automatically. The returned ad stores the applied budgetLevel and budget in campaignBudget for CBO or adSetBudget for ABO. Edit CBO budgets with PUT /v1/ads/campaigns/{campaignId} and ABO budgets with PUT /v1/ads/ad-sets/{adSetId}. Meta requires the budget at exactly one level, never both. Non-Meta platforms ignore this field. Ignored on the attach shape (adSetId), which inherits the existing budget.
Default"adset"

Value in

  • "adset"
  • "campaign"
currency?string

ISO 4217 currency code matching the ad account's currency (e.g. USD). Meta only. Optional: Zernio resolves it from the ad account when omitted. The value selects the minor-unit exponent Zernio converts budget/bid amounts by before calling Meta (most currencies are cents; zero-decimal currencies like JPY/KRW are sent as-is).

Length3 <= length <= 3
headline?string

Required for Meta, Google, Pinterest, LinkedIn, OpenAI Ads and Reddit Ads on legacy + attach shapes (skip for multi-creative; use creatives[].headline). Ignored for TikTok. On X, the website card title (needs imageUrl and linkUrl, max 70). Max: Meta=255, Google=30, Pinterest=100, LinkedIn=400, OpenAI=50 (min 3), Reddit=300 (the promoted post's title). On LinkedIn this is the ad's headline (the bold text on the creative); for traffic ads it's the link card title. On OpenAI Ads this is the chat card's title.

longHeadline?string

Google Display only. Defaults to headline if omitted. On LinkedIn, reused as the optional secondary description text on traffic (link) ads; omitted if not provided.

Lengthlength <= 90
body?string

Required on legacy + attach shapes. For X this is the tweet text (max 280 chars including a ~24-char URL when linkUrl is set). On LinkedIn this is the post commentary (the intro text shown above the ad). On OpenAI Ads this is the chat card's body text. On Reddit it is the promoted post's body: optional on image, video and carousel ads, required on text ads. Max: Google=90, Pinterest=500, OpenAI=100.

description?string

Meta only (facebook/instagram). Link description: the secondary text shown below the headline (Meta's link_data.description; on video creatives mapped to video_data.link_description). When omitted, Meta auto-pulls the destination URL's OpenGraph description. Applies on legacy, attach, and placementAssets shapes; for multi-creative use creatives[].description (this field is the shared fallback). For multi-text variations use descriptions (array) instead.

Lengthlength <= 255
bodies?array<string>

Meta only. Multiple Text Options (Advantage+ Flexible Format): supply 1-5 primary-text variations and Meta optimises delivery across them, WITHOUT enabling full Dynamic Creative (dynamicCreative). Uses optimization_type: DEGREES_OF_FREEDOM on the asset feed, so multiple ads per ad set are allowed (unlike dynamicCreative which is limited to one). Requires imageUrl or video, linkUrl, and callToAction. When set, the top-level body field is used as the object_story_spec.link_data.message (the preview text) and headlines must also be present. On a video creative the copy lands in video_data.message / video_data.title instead of link_data. Mutually exclusive with dynamicCreative, carouselCards, and creatives[]. With placementAssets the pool is applied to the placement-customized creative instead (optimization_type: PLACEMENT, every body carries the label of each rule that does not pin its own body, and the same for headlines), so Meta rotates the options per placement while media stays pinned; a rule's own body / headline stays pinned for that placement. descriptions[] cannot join a placement-customized creative (Meta refuses a second description per rule, subcode 1885878).

Items1 <= items <= 5
headlines?array<>

Meta only. Headline variations for Multiple Text Options. Must be sent alongside bodies. The top-level headline field is used as the object_story_spec.link_data.name (video_data.title on a video creative).

Items1 <= items <= 5
descriptions?array<>

Meta only. Optional description variations for Multiple Text Options. Sent alongside bodies and headlines.

Items1 <= items <= 5
callToAction?string

Required on legacy + attach shapes for Meta. Honoured on TikTok (passes through to the Spark Ad creative's call_to_action) and on LinkedIn (the CTA button on the ad; defaults to LEARN_MORE when linkUrl is set). LinkedIn accepts: LEARN_MORE, SIGN_UP, DOWNLOAD, SUBSCRIBE, REGISTER, JOIN, ATTEND, REQUEST_DEMO, VIEW_QUOTE, APPLY, SEE_MORE, SHOP_NOW, BUY_NOW. Reddit (image, video and carousel ads) accepts: LEARN_MORE, SHOP_NOW, SIGN_UP, CONTACT_US, DOWNLOAD, GET_QUOTE, SUBSCRIBE, APPLY_NOW, BOOK_NOW, BUY_TICKETS, DONATE_NOW, GET_DIRECTIONS, GET_SHOWTIMES, LISTEN_NOW, ORDER_NOW, SEE_MENU, INSTALL_MOBILE_APP (other values return 400). Ignored by Google, Pinterest, and X.

Value in

  • "LEARN_MORE"
  • "SHOP_NOW"
  • "SIGN_UP"
  • "BOOK_TRAVEL"
  • "CONTACT_US"
  • "DOWNLOAD"
  • "GET_OFFER"
  • "GET_QUOTE"
  • "SUBSCRIBE"
  • "WATCH_MORE"
  • "ADD_TO_CART"
  • "APPLY_NOW"
  • "BOOK_NOW"
  • "BUY_TICKETS"
  • "DONATE"
  • "DONATE_NOW"
  • "GET_DIRECTIONS"
  • "GET_SHOWTIMES"
  • "LISTEN_NOW"
  • "ORDER_NOW"
  • "PLAY_GAME"
  • "REQUEST_TIME"
  • "SEE_MENU"
  • "START_ORDER"
  • "INSTALL_MOBILE_APP"
  • "USE_APP"
  • "REGISTER"
  • "JOIN"
  • "ATTEND"
  • "REQUEST_DEMO"
  • "VIEW_QUOTE"
  • "APPLY"
  • "SEE_MORE"
  • "BUY_NOW"
linkUrl?string

Required on legacy + attach shapes (skip for multi-creative). On LinkedIn it's the ad's destination URL; required for traffic ads, optional for engagement / awareness. NOT required when goal is lead_generation (the ad opens a Lead Gen form instead of a destination). On LinkedIn, imageUrl + linkUrl publishes an ARTICLE-content creative; this is LinkedIn's article ad format, with the image as thumbnail and longHeadline as description. Required for OpenAI Ads (the chat card's target_url). Reddit: required on image and video ads (the post's destination and the ad's click URL), the fallback for carousel cards without their own, and refused with 400 on text ads.

Formaturi
leadGenFormId?string

Lead Gen form ID to attach to the ad's creative. REQUIRED when goal is lead_generation. Create one via POST /v1/ads/lead-forms. On Meta (facebook/instagram) this is the leadgen_forms ID; the ad set's promoted_object.page_id + LEAD_GENERATION optimization + destination_type ON_AD are derived automatically from the goal. On LinkedIn this is the adForm ID; the creative's leadgenCallToAction.destination is set to urn:li:adForm:{id} and the campaign objective is set to MAX_LEAD. Forms must be owned by the sponsoredAccount (not the organization) for the URN to resolve. Also required on every Meta ATTACH (adSetId) call that targets a lead ad set (the form attaches per-ad; Meta rejects a formless ad in a lead ad set). placementAssets, dynamicCreative and carouselCards (Meta multi-card Instant-Form lead ad; linkUrl and per-card linkUrl are optional and forwarded as real destinations when sent, falling back to Meta's lead-form link when omitted) ARE supported on Meta instant-form lead ads. Reddit: rejected with 400 on every goal (Reddit retired its native lead forms).

imageUrl?string

Image creative for Meta/Google/Pinterest/LinkedIn on legacy + attach shapes (mutually exclusive with video). Required for LinkedIn ads unless video is set. Not required for Google Search campaigns. For TikTok, this field carries the VIDEO URL (the TikTok ads endpoint is video-only; the field retains the imageUrl name for cross-platform consistency). On X, imageUrl + headline + linkUrl together build a website card (image, title and destination under the post; the URL is not added to the post text); sending only some of them is a 400. description, video and callToAction are rejected with 400 on X (promote a post that carries a video with POST /v1/ads/boost). For Google Display, treated as the landscape image (alias of images.landscape); supply images.square alongside or the request is rejected. For LinkedIn the image is uploaded to LinkedIn under the authoring Company Page (see organizationId); recommended ratio 1.91:1 (e.g. 1200×627). Required for OpenAI Ads (uploaded as the chat card's image; OpenAI has no video ad format). Reddit: builds an image ad (requires linkUrl); on a Reddit video ad it is the fallback for video.thumbnailUrl.

Formaturi
images?

Google Display (Responsive Display Ads) only. Google RDA requires both a landscape (1.91:1) and a square (1:1) marketing image; sending only one is rejected upstream as 'Too few.' (NOT_ENOUGH_*_MARKETING_IMAGE_ASSET). Supply both URLs here. Either this field or the legacy imageUrl can provide the landscape, but square has no legacy counterpart so it must be set here for Display.

video?

Meta (facebook, instagram), LinkedIn and Reddit. On Reddit url, linkUrl and a thumbnail (thumbnailUrl, or imageUrl) are all required. Creates a single VIDEO ad. Mutually exclusive with imageUrl. Supply url to upload a file, or id to reuse a video already on the ad account (list them with GET /v1/ads/videos). Works on the single-ad and attach (adSetId) shapes; for Meta multi-creative, set video per entry inside creatives[] instead. For LinkedIn the video is uploaded to LinkedIn under the authoring Company Page (see organizationId) and the campaign format is set to SINGLE_VIDEO; LinkedIn ignores thumbnailUrl (it auto-generates the poster frame). Supply MP4 H.264/AAC, 3s-30min, 75KB-500MB.

creatives?array<>

Meta-only. When present, switches to the multi-creative shape: creates 1 campaign + 1 ad set + N ads (one per entry here). Top-level headline / body / imageUrl / linkUrl / callToAction are ignored in this mode. Mutually exclusive with adSetId.

Items1 <= items
adSetId?string

When present, switches to the attach shape: adds one new ad to this existing ad set without creating a new campaign. Budget, targeting, goal, schedule, AND bid strategy are inherited from the ad set on Meta, and passing bidStrategy in attach mode returns 400. To change an existing ad set's bid, use PUT /v1/ads/ad-sets/{adSetId}. Mutually exclusive with creatives[]. dynamicCreative returns 400 in attach mode: create a new dynamic ad set by omitting adSetId instead.

The attached ad takes the full single-creative surface: headline/body/description/callToAction plus either imageUrl/video OR placementAssets (its own per-placement Feed/Story assets) OR translations/defaultLocale (its own per-locale asset feed, Meta only), and leadGenFormId when the target is a lead ad set (the parent must be ON_AD, true for ad sets created via goal lead_generation; Meta rejects a formless ad there, so pass the form on EVERY attached ad). This is the way to build N full ads sharing one ad set: create the first ad via the normal shape, then attach the rest one call each.

Supported on Meta (facebook, instagram), Google Ads, TikTok, and LinkedIn. On TikTok the adSetId is the ad group ID; the new ad inherits the ad group's bid + budget + targeting. On LinkedIn the adSetId is the LinkedIn Campaign ID (numeric); we attach a new Creative to that Campaign, so the Campaign's platformSpecificData bidding, targeting, budget and schedule are inherited (passing those fields returns 400).

On Google Ads the adSetId is the AD GROUP id (numeric). goal is not required: the ad group keeps its campaign's bidding, budget and targeting. campaignType is read from the ad group's campaign (Search, Display or Demand Gen), so it can be omitted; a campaignType that contradicts the ad group returns 400, an ad group Google does not know in that customer returns 404, and any other campaign type returns 422. A Search ad needs headline + body + linkUrl; a Display ad also needs images.landscape + images.square + businessName. Google allows 3 responsive search ads per ad group: a fourth returns 422 naming the limit (RESPONSIVE_SEARCH_ADS_PER_AD_GROUP) and the ad group. budgetAmount/budgetType and bidding fields (bidStrategy, bidAmount, portfolioBidStrategyId) return 400 on this shape; the ad group already owns them. With campaignType: "demand_gen" the ad group must belong to a Demand Gen campaign (otherwise 400); demandGen carries only the ad's creative, the new ad is created PAUSED, and ad group settings (geo, languages, demandGen.channels/audience/audienceId/ adGroupName) return 400. validateOnly is supported.

existingCampaignId?string

Meta, Google Ads, LinkedIn and TikTok. On TikTok: creates the ad group and the ad under this existing campaign; the campaign is neither created nor activated and its objective must fit goal; not with smartPlus. On Meta: add the new ad set under this EXISTING campaign instead of creating a new one (multi-ad-set audience testing). The new ad set's budget is matched to the campaign's mode automatically: for a CBO campaign (campaign-level budget) omit budgetAmount/budgetType, since the campaign owns the budget; for an ABO campaign pass them (they go on the new ad set). On LinkedIn: create a new Campaign (and its Creative) under this EXISTING CampaignGroup. On Google Ads: create a new ad group under this EXISTING campaign; the new ad group inherits the campaign's budget, so omit budgetAmount/budgetType (and any bidding field), or the request returns 400. With campaignType: "demand_gen" the campaign must be a Demand Gen campaign (otherwise 400): the new ad group (with its geo, languages, channels and audience from the request) and its ad are created in one atomic request, the ad group PAUSED; schedule and locationTargetingType belong to the campaign and return 400. A campaign migrated from Discovery that still targets locations and languages on the campaign refuses them on a new ad group, so geo and language fields return 400 there; the new ad group follows the campaign's targeting. On failure only the entities we authored are cleaned up; the pre-existing parent is left untouched and is never (re)activated. Mutually exclusive with adSetId and creatives[].

phoneNumber?string

Meta attach shape only (adSetId). The phone number (E.164, e.g. +4712345678) for an ad added to a "website and phone call" ad set (destination_type WEBSITE_AND_PHONE_CALL), sent as the creative's call configuration. Optional: when omitted, the number on the ad set's existing ads is reused, and if none of them carries one the request is a 400. Rejected with 400 on any other ad set, with existingCreativeId, without adSetId, or on other platforms (a new call campaign is POST /v1/ads/call).

Match^\+[1-9]\d{6,14}$
existingCreativeId?string

Meta only. Reuse an EXISTING ad creative by id instead of building a new one from the copy/media fields (which are then ignored). Combine with existingCampaignId to build a multi-ad-set campaign that shares one creative. Mutually exclusive with creatives[], dynamicCreative, and placementAssets. The creative id used is returned as creativeId on the create response.

businessName?string

Google Display only

Lengthlength <= 25
boardId?string

Pinterest only. Board ID (auto-creates if not provided).

organizationId?string

LinkedIn only. The Company Page that authors the Direct Sponsored Content ("dark") post backing the ad. Accepts a numeric organization ID or a full urn:li:organization:N URN. Required unless the resolved accountId is a connected LinkedIn Company-Page account (defaults to that page) or the LinkedIn ad account is org-owned (defaults to the account's owning organization). The authenticated member must be an ADMINISTRATOR or DIRECT_SPONSORED_CONTENT_POSTER of this page (and the page must be associated with the ad account), or LinkedIn returns 403. Ignored by every other platform.

targeting?

Nested targeting object, the same TargetingSpec shape as POST /v1/ads/boost, POST /v1/ads/targeting/reach-estimate, and saved_targeting audiences. Merged UNDER the flat inline targeting fields below: savedTargetingId < targeting < flat fields (a flat field present on the body replaces the nested value entirely). Both forms are equivalent; use whichever your integration already builds. Reddit-only targeting (communities, excludedCommunities, conversationKeywords, excludedConversationKeywords) is sent here.

countries?array<string>

ISO 3166-1 alpha-2 country codes (e.g. ['NL']). Defaults to ['US'] when no other geo targeting (flat or nested targeting) is provided. (LinkedIn and OpenAI Ads currently honour country-level targeting only; any other targeting field returns 400 for OpenAI Ads.)

countryGroups?array<>

Meta only. Continents and trade blocs (geo_locations.country_groups), for targeting a whole region without listing its countries. Combines with countries rather than replacing it. Discoverable via GET /v1/ads/targeting/search?dimension=geo&geoType=country_group.

cities?array<|>

City-level geo targeting (Meta, Google and TikTok). An entry is either { key } or the key alone as a plain string (["1006410"]). Each city is targeted by the platform's opaque key (the city ID) which can be looked up via GET /v1/ads/targeting/search?dimension=geo&q=<name>&countryCode=<ISO>. Optional radius + distance_unit (Meta only) extend the targeting beyond the city limits (e.g. radius 25 km around the city center). Both must be set together, or both omitted (Meta defaults to ~16 km when omitted).

On Meta, cannot overlap with the same country in countries (Meta returns a "locations overlap" error). Either drop the country or scope it to a different country. On TikTok, keys are numeric location ids and can be sent without countries.

regions?array<string|>

Region-level (state/province) geo targeting (Meta, Google and TikTok). Each region is targeted by the platform's opaque key (the region ID) which can be looked up via GET /v1/ads/targeting/search?dimension=geo&q=<name>&countryCode=<ISO>. An entry may also be the key alone as a plain string.

ageMin?integer
Range13 <= value <= 65
ageMax?integer
Range13 <= value <= 65
interests?array<>

Interest objects from /v1/ads/targeting/search?dimension=interest (or dimension=interestKeyword on TikTok). Each must include id and name. On TikTok, bare numeric ids are interest categories (interest_category_ids) and ids like keyword:123456 from dimension=interestKeyword are additional interests (interest_keyword_ids); any other id returns 422 naming targeting.interests. TikTok matches people who fit ANY selected interest, additional interest or behavior (one OR'd group), and that group is ANDed with location, age, gender and the other dimensions.

zips?array<string|>

Postal/ZIP geo targeting. key is the platform's postal location ID from /v1/ads/targeting/search?dimension=geo&geoType=zip. Supported on Meta, Google, TikTok, Pinterest, X.

metros?array<string|>

DMA / metro-area geo targeting (Meta and TikTok). key is the platform's metro ID from /v1/ads/targeting/search?dimension=geo&geoType=metro (TikTok metros appear as type metro, e.g. the New York DMA).

customLocations?array<>

Point-radius (lat/lng) geo targeting. Meta only (custom_locations). Rejected on platforms without radius support.

behaviors?array<>

Behaviour entities from /v1/ads/targeting/search?dimension=behavior (Meta, TikTok and LinkedIn). On TikTok they target people who interacted with videos (watched to the end, liked, commented or shared, last 15 days) or creators (followed or viewed the profile) in the picked categories, or who viewed videos with a picked hashtag (hashtag:<id> ids from dimension=hashtag, sent as a HASHTAG_RELATED action with VIEW_HASHTAG), and the ad group uses the TikTok placement only. On TikTok, behaviors are OR'd with interests and additional interests. Each must include id.

workPositions?array<>

Meta only. Job title entities from /v1/ads/targeting/search?dimension=workPosition. Each must include id. Rejected on other platforms (use LinkedIn's jobTitles there).

workEmployers?array<>

Meta only. Employer entities from /v1/ads/targeting/search?dimension=workEmployer. Each must include id.

workIndustries?array<>

Meta only. Work-industry entities from /v1/ads/targeting/search?dimension=workIndustry. Each must include id. Rejected on other platforms (use LinkedIn's industries there).

incomeTier?string

Normalized household-income tier. Meta and TikTok express all four; Google maps only top_10; rejected on LinkedIn, X, and Pinterest. On Meta, income targeting is incompatible with housing/employment/credit specialAdCategories.

Value in

  • "top_5"
  • "top_10"
  • "top_10_25"
  • "top_25_50"
languages?array<string>

e.g. ["en","es"]. Google: campaign language targeting (language_constant) using Google's language codes (ISO 639-1, plus variants such as zh_CN); unknown codes return 400. On Meta, a bare code targets all regional variants ("en" = all English), or use a region-qualified code for a specific one ("en_GB", "pt_BR", "zh_TW"); unknown codes are rejected. Other ad platforms use their own language-code systems.

placements?

Meta only. Manual ad placements. Omit for automatic placements (Meta's default, recommended for most cases, since Meta optimises delivery across all eligible surfaces). When set, restricts delivery to the chosen surfaces, mapped onto the ad set's targeting.{publisher_platforms, facebook_positions, instagram_positions, messenger_positions, audience_network_positions, threads_positions, whatsapp_positions, device_platforms}. Enum membership is validated here; Meta additionally enforces co-selection rules (e.g. some positions require their parent publisher platform) and returns an actionable error which we surface. Non-Meta platforms reject this field.

savedTargetingId?string

ID of a saved_targeting audience (created via POST /v1/ads/audiences). When set, its stored TargetingSpec is expanded as the base targeting; inline fields on this body merge on top. Lets you reuse a named targeting preset without re-sending every field.

rawTargeting?

Meta only. A raw Meta-native targeting spec (snake_case: geo_locations, age_min, excluded_custom_audiences, flexible_spec, targeting_automation, user_os, wireless_carrier, business places, etc.), exactly the shape GET /v1/ads/{adId} returns for external ads. Sent alone it reaches the ad set VERBATIM (the clone-a-campaign's-targeting-exactly path). Meta validates and surfaces any errors.

Can be combined with the camelCase targeting fields (countries/regions/cities/interests/ageMin/..., targeting, savedTargetingId, audienceId): rawTargeting is the BASE layer and the built camelCase spec is merged on top, key by key, with the camelCase side winning on collision (the camelCase precedence chain stays savedTargetingId < targeting < flat fields). The merge goes one level deep inside geo_locations and excluded_geo_locations: built sub-keys win, raw-only sub-keys such as location_types survive alongside built countries. Array values (flexible_spec, ...) are replaced as a WHOLE key when the camelCase spec builds them, never element-merged. When rawTargeting is present the defaults the camelCase builder normally injects (US geo, targeting_automation.advantage_audience: 0) are suppressed, so raw's values are not clobbered. Include targeting_automation in the raw spec (or send advantageAudience) as Meta requires it on create. If cloning an EU campaign, also pass dsaBeneficiary / dsaPayor (those are separate fields, not part of targeting).

specialAdCategories?array<>

Meta only. Declares the ad's special category, required for housing, employment, credit, or political/social-issue ads (Meta enforces restricted targeting for these). Note: setting a special category disables income/zip targeting on Meta.

specialAdCategoryCountry?array<string>

Meta (metaads) only. 2-letter ISO country codes the special ad category applies to. Requires specialAdCategories to be set (400 otherwise). Ignored when joining an existing campaign via existingCampaignId (the existing campaign's category/country already governs it).

regionalRegulatedCategories?array<string>

Meta only. Regional regulation categories required when the ad set targets certain countries. Known values: AUSTRALIA_FINSERV, AUSTRALIA_UNIVERSAL, BRAZIL_REGULATION, INDIA_FINSERV, MALAYSIA_UNIVERSAL, SINGAPORE_UNIVERSAL, TAIWAN_FINSERV, TAIWAN_UNIVERSAL, THAILAND_UNIVERSAL. Meta rejects the ad set without this when the targeting geo includes the corresponding country.

regionalRegulationIdentities?

Meta only. Verified identity IDs of the beneficiary and payer for each declared regionalRegulatedCategories value. Meta may require them on ad sets created through the API even when a default beneficiary and payer are set in Ads Manager advertising settings (seen on AUSTRALIA_FINSERV), so sending them explicitly is the reliable path. The value is the Verified identity ID (Meta Business Settings > Verified identities), not a Page or Business ID, and beneficiary and payer may be the same ID. Send each ID as a string of digits: Verified identity IDs can have 17 digits, more than a JSON number holds exactly, and a number that arrives rounded is rejected. Numbers up to 9007199254740991 are still accepted. The IDs reach Meta unchanged. Keys by category: AUSTRALIA_FINSERV uses australia_finserv_beneficiary / australia_finserv_payer; TAIWAN_FINSERV uses taiwan_finserv_beneficiary / taiwan_finserv_payer; INDIA_FINSERV uses india_finserv_beneficiary / india_finserv_payer; TAIWAN_UNIVERSAL uses taiwan_universal_beneficiary / taiwan_universal_payer; SINGAPORE_UNIVERSAL uses singapore_universal_beneficiary / singapore_universal_payer; AUSTRALIA_UNIVERSAL, BRAZIL_REGULATION, MALAYSIA_UNIVERSAL and THAILAND_UNIVERSAL use universal_beneficiary / universal_payer. When Meta rejects the ad set for a missing verified advertiser, the error carries param regionalRegulationIdentities, details.expectedIdentityKeys (the keys the declared categories expect), details.suggestedCategories (the markets Meta ties to that subcode) and Meta's metaCode, metaSubcode and fbtraceId. Ignored when adSetId adds the ad to an existing ad set.

endDate?string

Required for lifetime budgets. Read like startDate (account timezone on Meta, TikTok, X, Pinterest, OpenAI and Reddit; whole account-local days on Google; UTC on LinkedIn), and a date-only end runs to 23:59:59 local.

Formatdate-time
startDate?string

Ad-set start time (ISO 8601, e.g. "2026-06-10T09:00:00Z"), mapped to the ad set's start (Meta start_time, TikTok ad group schedule_start_time, LinkedIn / Pinterest / X / Google campaign start). When omitted the ad starts delivering immediately. For lifetime budgets Meta also requires endDate. On Meta, TikTok, X, Pinterest, OpenAI and Reddit a value without an offset (YYYY-MM-DD, YYYY-MM-DD HH:MM:SS or YYYY-MM-DDTHH:MM:SS) is read in the ad account timezone (Meta itself would read it as UTC). Google runs whole days in the customer account timezone and takes the value's calendar date as written. LinkedIn ad accounts carry no timezone, so there a value without an offset is read as UTC. The created ad's schedule echoes the start and end the platform stored, as UTC instants; when attaching to an existing ad set (adSetId) it is that ad set's start and end. Same field as on POST /v1/ads/boost.

Formatdate-time
pageId?string

Meta only. The Facebook Page the ad runs as (object_story_spec.page_id). Defaults to the Page bound to the connection. Pass another Page ID to run the ad as that Page: any Page granted to the connection is accepted (for a business-login connection, every Page granted in Meta's dialog; list them with GET /v1/ads/instagram-accounts). The Instagram identity is re-resolved for that Page unless instagramAccountId is set. A Page the connection cannot see is a 400 on pageId naming the granted Pages.

instagramAccountId?string

Meta only. Override the Instagram account the ad is delivered as. Pass an Instagram Business Account ID (e.g. 17841...), mapped to the creative's instagram_user_id; it is sent as-is and never replaced. When omitted we use the Instagram account linked to the Page; formats that need an Instagram actor without one use the actor the Page's other ads already run as, then the Page's page-backed Instagram account. If Meta refuses that default because the ad account has no access to it (200/1815199), the creative is retried once with an Instagram account the ad account is authorized for; with none, the request fails with a 422 on instagramAccountId (list the usable accounts with GET /v1/ads/instagram-accounts). A creative replaced later through /v1/ads/{adId} follows the same rule. Useful when a Page has more than one eligible IG account.

dynamicCreative?

Meta only. Dynamic Creative: supply a POOL of assets and Meta auto-combines and optimises them into the best-performing variations within a single ad (mapped to the creative's asset_feed_spec). When set, the top-level single-creative fields (imageUrl, headline, body, linkUrl, callToAction) are ignored. Mutually exclusive with the creatives[] multi-creative shape. Exactly ONE of imageUrls / videoUrls is required (Meta allows one ad format per asset feed; sending both → 400). Limits remain 10 images or videos and 5 bodies, titles or descriptions. The ad set is created with is_dynamic_creative: true. Combining this field with adSetId returns 400: omit adSetId to create a new dynamic ad set. Multiple headlines go in titles; multiple primary texts go in bodies.

carouselCards?array<>

Reddit: builds a carousel ad, each card with its image, destination (card linkUrl, falling back to the top-level linkUrl, 400 when neither is set), call to action (card callToAction, falling back to the top-level one) and caption (card headline, else description). headline is still required.

Meta only. Hand-built carousel: 2-10 authored cards, sent in the order given as the creative's link_data.child_attachments. Unlike dynamicCreative, you author every card's copy and link, but by default Meta may still reorder the cards for performance (multi_share_optimized defaults to true); send optimizeCardOrder: false to keep the order given. Requires top-level body and callToAction; linkUrl is also required UNLESS leadGenFormId is set. Those become the ad's own Destination and button (link_data.link / link_data.call_to_action), and double as the per-card fallback when a card omits its own. Mutually exclusive with imageUrl/video, creatives[], dynamicCreative, placementAssets, existingCreativeId, adSetId and goal catalog_sales. Combines with leadGenFormId to build a carousel Instant-Form lead ad: linkUrl and per-card linkUrl become OPTIONAL and, when sent, are forwarded as the real card and top-level destinations; when omitted, the destination falls back to Meta's lead-form link.

Items2 <= items <= 10
optimizeCardOrder?boolean

Meta only (a 400 on any other platform), and only with carouselCards (a 400 without them). Sets the creative's link_data.multi_share_optimized. false keeps the cards in the order given; Meta then takes at most 5 cards, so false with more than 5 carouselCards is a 400. true lets Meta pick and reorder the cards for performance (up to 10). Omitted, nothing is sent and Meta's default (true) applies.

defaultLocale?string

Meta only. Language the top-level copy is written in (e.g. en, pt_BR), used by the translations default rule. Defaults to en. Meta rejects a language asset feed whose default rule carries no locales of its own. Must NOT also appear as an entry in translations.

translations?array<>

Meta only. Multi-language ads (Dynamic Language Optimization): ONE ad carrying per-locale copy and, optionally, per-locale media: the "Languages" toggle in Ads Manager. Keeps social proof (likes/comments/shares) on a SINGLE post instead of splitting it across one ad per language.

The ad's top-level copy is the DEFAULT shown to every locale you do NOT list, and it counts as one of the language variants.

IMPORTANT, and the opposite of what you might expect: text does NOT inherit. Every entry must carry its own headline, body AND description, and all of them must be DISTINCT from each other and from the ad's top-level copy. Meta deduplicates identical strings inside the asset feed, so two locales sharing a string collapse into one asset and the create fails with a misleading "Too few ... texts provided in asset creation" (subcode 1885817) that names a field which is actually present. We validate this before calling Meta and return a 400 naming the offending locale and field. description is therefore effectively required on the ad whenever translations is present, even though it is optional otherwise.

Do NOT list defaultLocale inside translations: Meta rejects the duplicate with "The language asset feed includes an unsupported targeting field" (subcode 1885985).

Media DOES inherit and is uploaded once when shared, and linkUrl inherits too: each locale may name its own landing page and unlisted locales fall back to the ad's top-level linkUrl. Meta enforces Dynamic Creative image dimensions on language feeds, so an imageUrl that works on a normal ad may be rejected with "The following images have invalid dimensions for Dynamic Creative" (subcode 1885558). Video is not affected.

Mutually exclusive with dynamicCreative, placementAssets, carouselCards, existingCreativeId and creatives[]. Meta allows one asset_feed_spec shape per creative.

Items1 <= items <= 10
placementAssets?

Meta only. Placement asset customization: pin a SPECIFIC asset (image OR video) to each placement group on a SINGLE ad (e.g. a 9:16 on Stories/Reels and a 4:5 on Feed). The same thing Meta Ads Manager produces with "different creative per placement", mapped to the creative's asset_feed_spec + asset_customization_rules. Deterministic pinning, NOT the auto-optimizing pool of dynamicCreative (mutually exclusive). Works on the legacy single shape AND the attach shape (adSetId + placementAssets adds one placement-customized ad to an existing ad set, the way to build N per-placement ads sharing one ad set: create the first normally, attach the rest). Cannot be combined with creatives[] or top-level bodies/headlines/descriptions arrays. Each rule can override headline, body and description with one string per field. Omitted fields and unmatched placements use the top-level copy; linkUrl and callToAction remain shared. Zernio emits labelled text with optimization_type: PLACEMENT. Multiple text options rotating within a placement are not supported by this input. Each rule's placements accepts the same fields as the top-level placements object; Meta enforces co-selection rules and returns an actionable error.

Meta controls text rendering by placement and format. Validation accepts these fields but does not prove that every field appears in delivery. Preview the ad; put copy that must always be visible into the image or video itself.

validateOnly: true supports all-image placementAssets without uploading or creating anything. Video placement validation remains unsupported because it requires uploads.

A block is all-image OR all-video, never mixed (Meta's asset_feed_spec carries one ad format). Image mode: defaultImageUrl + rules[].imageUrl. Video mode: defaultVideoUrl + rules[].videoUrl (optional thumbnailUrl/defaultThumbnailUrl posters; Meta auto-generates when omitted). Exactly one catch-all default is required.

audienceId?string

Custom audience ID for targeting

campaignType?string

Google only. Performance Max requires assetGroup and Demand Gen requires demandGen; both are always created PAUSED. With adSetId it defaults to the ad group's own campaign type instead of display.

Default"display"

Value in

  • "display"
  • "search"
  • "pmax"
  • "demand_gen"
locationTargetingType?string

Google only (400 elsewhere). Set on the new campaign; a request that joins an existing campaign (existingCampaignId or adSetId) returns 400, change that campaign with PUT /v1/ads/campaigns/{campaignId}/targeting instead. presence reaches only people in or regularly in the targeted locations.

Value in

  • "presence"
  • "presence_or_interest"
assetGroup?

Google Performance Max creative assets. At least one description must be 60 characters or fewer. Texts within each list must be distinct.

demandGen?

Creative, channel and audience settings for a Google Demand Gen campaign (campaignType demand_gen). Creates one ad group with one ad: a multi-asset image ad, a video responsive ad when youtubeVideoIds is sent, or a carousel ad when carouselCards is sent.

keywords?array<|>

Google Search only. Keywords on the new ad group; entries are strings (BROAD) or { text, matchType }. Editable later via PUT /v1/ads/{adId} targeting.keywords.

Itemsitems <= 1000
negativeKeywords?array<|>

Google Search only; other platforms return 400. Ad-group-level negative keywords on the new ad group. Editable later via PUT /v1/ads/{adId} targeting.negativeKeywords.

Itemsitems <= 1000
campaignNegativeKeywords?array<|>

Google Search only; other platforms return 400. Campaign-level negative keywords (campaign_criterion.negative), created alongside the ad group. Editable later via PUT /v1/ads/campaigns/{campaignId}/negative-keywords.

Itemsitems <= 1000
additionalHeadlines?array<|>

Google Search RSA only. Extra text assets as strings or objects with text and optional pinnedField. Existing string input remains supported. The effective create lists, including primary text and deduplication, must contain 3-15 headlines and 2-4 descriptions; excess entries return 400. Each headline is at most 30 characters (Google's limit): a longer one returns 400 naming the field and index (e.g. additionalHeadlines[2]), it is never truncated. On Search the same limit applies to headline and longHeadline (30) and to body (90).

additionalDescriptions?array<|>

Google Search RSA only. Extra text assets as strings or objects with text and optional pinnedField. Existing string input remains supported. The effective create lists, including primary text and deduplication, must contain 3-15 headlines and 2-4 descriptions; excess entries return 400. Each description is at most 90 characters (Google's limit): a longer one returns 400 naming the field and index (e.g. additionalDescriptions[2]), it is never truncated. On Search the same limit applies to headline and longHeadline (30) and to body (90).

sitelinks?array<>

Google Search only. Sitelink assets to create and attach at the campaign level. Each entry becomes an Asset (with sitelink_asset + Asset.final_urls) plus a CampaignAsset link (field_type SITELINK). Approval is async: Google reviews assets after creation; poll asset.policy_summary later to read the verdict. Google requires at least two sitelinks to surface them on an ad; four or more is Google's own recommendation for maximum visibility. The response's creative.sitelinks[] echoes each input plus its Google resourceName.

Items2 <= items <= 20
callouts?array<>

Google Search only. Short callout texts (max 25 chars each) that appear as non-clickable annotations under the ad, e.g. "Free shipping", "24/7 support". Each becomes one Asset (callout_asset) plus a CampaignAsset link with field_type CALLOUT. Response's creative.callouts[] echoes each input plus its Google resourceName.

Items1 <= items <= 20
structuredSnippets?array<>

Google Search only. Structured snippets: one header from Google's predefined list plus 3-10 values (max 25 chars each). Each becomes one Asset (structured_snippet_asset) plus a CampaignAsset link with field_type STRUCTURED_SNIPPET.

Items1 <= items <= 20
advantageAudience?integer

Meta only. Controls the Advantage audience feature (targeting_automation). 0 = disabled (default), 1 = enabled. Meta Marketing API requires this field on all ad set creation requests.

Value in

  • 0
  • 1
attributionSpec?array<>

Meta only. Conversion attribution window for the ad set, mapping 1:1 to Meta's ad-set attribution_spec. Only honored for conversion goals (conversions, lead_generation, app_promotion); ignored for awareness/traffic/engagement. Omit to use Meta's default (7-day click + 1-day view). Meta enforces the valid combinations: VIEW_THROUGH only allows windowDays: 1 (7d/28d view windows were removed Jan 2026); ENGAGED_VIDEO_VIEW only 1 and only alongside VIEW_THROUGH: 1; CLICK_THROUGH: 28 only on certain objectives. Invalid combos surface as a Meta 400. Example: [{ "eventType": "CLICK_THROUGH", "windowDays": 7 }, { "eventType": "VIEW_THROUGH", "windowDays": 1 }]

Items1 <= items <= 3
gender?string

Restrict the audience by gender. 'male' targets men only, 'female' targets women only, 'all' (default) targets everyone. Applied on Meta, TikTok, Pinterest and Reddit. Ignored on Google, LinkedIn and X.

Default"all"

Value in

  • "all"
  • "male"
  • "female"
bidStrategy?string
Deprecated

Deprecated: send it inside platformSpecificData instead (Meta today; TikTok's nested shape is planned). The flat field keeps working during the deprecation window; sending both shapes returns a 400.

Meta bid strategy applied to the ad set.

OpenAI Ads: sent via this flat field, the only channel it supports (platformSpecificData is Meta/LinkedIn-only and returns 400 for OpenAI). LOWEST_COST_WITH_BID_CAP or COST_CAP with bidAmount sets a fixed bid (fixed_bid, the CPA bid on conversions). Omitted or LOWEST_COST_WITHOUT_CAP bids automatically with OpenAI's Maximize Results (maximize_clicks for traffic, maximize_conversions for conversions), which needs budgetType: daily; on a lifetime budget or the awareness goal it returns 400.

Reddit Ads: sent via this flat field and applied to the new ad group. LOWEST_COST_WITHOUT_CAP or omitted = automatic bidding (BIDLESS), LOWEST_COST_WITH_BID_CAP + bidAmount = MANUAL_BIDDING with that bid, COST_CAP + bidAmount = MAXIMIZE_VOLUME with that cap. Any other strategy returns 422 before anything is created.

Google (not deprecated there, this shared flat field is Google's only shape): applied to the campaign this call creates. On Google: LOWEST_COST_WITHOUT_CAP = Maximize Conversions, COST_CAP + bidAmount = Target CPA, LOWEST_COST_WITH_MIN_ROAS + roasAverageFloor = Target ROAS, LOWEST_COST_WITH_BID_CAP + bidAmount = Maximize Clicks with a CPC ceiling; portfolioBidStrategyId attaches a portfolio strategy instead. Omitted, the campaign falls back to a goal-based default.

Value in

  • "LOWEST_COST_WITHOUT_CAP"
  • "LOWEST_COST_WITH_BID_CAP"
  • "COST_CAP"
  • "LOWEST_COST_WITH_MIN_ROAS"
bidAmount?number
Deprecated

Deprecated: send it inside platformSpecificData instead (Meta today; TikTok's nested shape is planned). The flat field keeps working during the deprecation window; sending both shapes returns a 400.

Bid cap in WHOLE currency units (USD: 5 = $5.00; JPY: 100 = ¥100). Required when bidStrategy is LOWEST_COST_WITH_BID_CAP or COST_CAP. Meta only: sending bidAmount WITHOUT bidStrategy requires existingCampaignId (400 otherwise), and sets the new ad set's cap under the joined campaign's COST_CAP / LOWEST_COST_WITH_BID_CAP parent. The strategy itself is inherited from the campaign. Restating bidStrategy here is accepted but has no effect on the ad set.

Rejected with 400 in adSetId attach mode: that shape inherits its cap from the platform. Use PUT /v1/ads/ad-sets/{adSetId} there instead.

roasAverageFloor?number
Deprecated

Deprecated: send it inside platformSpecificData instead (Meta today; TikTok's nested shape is planned). The flat field keeps working during the deprecation window; sending both shapes returns a 400.

Minimum ROAS as a decimal multiplier (e.g. 2.0 = 2.0x ROAS). Required when bidStrategy is LOWEST_COST_WITH_MIN_ROAS. Sending it without bidStrategy is a 400. Sent to Meta as bid_constraints.roas_average_floor × 10000. Known gap: a CBO campaign's ROAS floor lives on the campaign only (set via POST /v1/ads/campaigns); there is no supported way to set it while joining a CBO campaign here.

portfolioBidStrategyId?string

Google Search and Display only. Performance Max rejects portfolio bidding. Attach an existing portfolio bid strategy (numeric id from GET /v1/ads/bid-strategies) to the new campaign instead of a standard one. Exclusive with bidStrategy.

Match^\d+$
targetImpressionShare?

Google Search only. Target impression share bidding. Exclusive with bidStrategy, portfolioBidStrategyId and manualCpc; bidAmount is refused alongside it (the ceiling is maxCpc).

manualCpc?

Google Search and Display only. Manual CPC bidding. Enhanced CPC is not offered: Google no longer lets Search and Display campaigns enable it. Exclusive with every other strategy field.

networkSettings?

Google Search only, when the call creates the campaign (400 with existingCampaignId or adSetId).

Properties1 <= properties
valueRuleSetId?string

Meta only (facebook, instagram; other platforms return 400). Value rule set to attach to the new ad set, from /v1/ads/value-rule-sets. Attachment is driven by this id, so valueRulesApplied is optional alongside it.

Rejected with 400 in adSetId attach mode: that shape inherits the existing ad set's attachment, so the field would be silently ignored. Use PUT /v1/ads/ad-sets/{adSetId} there instead.

Ignored (stripped before the ad-set create) when buyingType is RESERVED: value rules only apply to auction ad sets on LOWEST_COST_WITHOUT_CAP or COST_CAP, and a Reach & Frequency reservation has no auction bid strategy.

Read back with GET /v1/ads/ad-sets/{adSetId}?fields=value_rule_set_id; the attachment is not mirrored onto Zernio's ad documents.

Match^\d+$
valueRulesApplied?boolean

Meta only (facebook, instagram; other platforms return 400). Optional when attaching, and requires valueRuleSetId. false is REJECTED here with 400: a newly created ad set has nothing to detach, so detaching lives on PUT /v1/ads/ad-sets/{adSetId}.

platformSpecificData?|

Platform-specific options. The platform is derived from accountId; sending options for a different platform returns a 400. LinkedIn (campaign bidding and delivery controls) and Meta (the bid trio) have options today.

Meta: bidStrategy, bidAmount and roasAverageFloor may be sent here instead of at the root, which is the preferred home going forward. Sending the bid fields in BOTH places returns a 400 (mutually_exclusive_fields), and sending any of them in adSetId attach mode is a 400 too (the ad set already has its bid). dailyMinSpendTarget / lifetimeMinSpendTarget set the new ad set's minimum spend and live here only; they are rejected in adSetId attach mode as well.

dsaBeneficiary?string

Legal entity that benefits from the ad. Required when targeting EU users (EU DSA, Article 26). Optional if the ad account has a default beneficiary: set it once via PATCH /v1/ads/accounts or in Meta Ads Manager, and Meta fills it in whenever the field is omitted.

Lengthlength <= 100
dsaPayor?string

Legal entity that pays for the ad. Can differ from dsaBeneficiary (for example, an agency paying for a client's ads). Same rules as dsaBeneficiary: required for EU targeting unless the ad account has a default payor.

Lengthlength <= 100
brandIdentity?

TikTok only. Synthetic Brand Identity used when the ad attributes to a CUSTOMIZED_USER (instead of a real TT_USER @username). Required on the FIRST CUSTOMIZED_USER ad on a tiktokads SocialAccount with no cached identity; omit on subsequent ads (the identity is cached on the account after first creation). Non-TikTok platforms ignore this field.

Alternative: configure once via PATCH /v1/connect/tiktok-ads, then create ads without this field.

identityId?string

TikTok: the identity the ad runs as, from GET /v1/ads/tiktok-identities. Overrides the connected account's own identity; must be authorized on the advertiser.

identityType?string

TikTok only. Forces the identity attribution on the ad:

  • TT_USER: the posting account's open_id (real @username branding). Requires a connected TikTok posting account on the same profile.
  • CUSTOMIZED_USER: synthetic Brand Identity (display name + avatar). Requires a configured Brand Identity (cached on the tiktokads SocialAccount via PATCH /v1/connect/tiktok-ads) or an inline brandIdentity to create one on the fly.

When omitted, defaults to TT_USER if a posting account is connected on this profile, else CUSTOMIZED_USER. Spark Ads (POST /v1/ads/boost) always use TT_USER regardless of this field, because TikTok requires the original organic post's author identity for Spark.

Value in

  • "TT_USER"
  • "CUSTOMIZED_USER"
  • "BC_AUTH_TT"
smartPlus?boolean

TikTok only. Creates the ad as a TikTok Upgraded Smart+ campaign: TikTok automates targeting, bidding and delivery. Supports goals conversions (Smart+ Web Conversions), lead_generation (Smart+ Lead Generation with a website form on linkUrl; TikTok Instant Forms not supported) and app_promotion (Smart+ App installs; the ad's destination is the app store, so linkUrl is not used). The web goals require promotedObject.pixelId AND promotedObject.customEventType; app_promotion requires promotedObject.applicationId instead. Targeting works like on any TikTok ad (defaults to countries: ["US"] when omitted); TikTok automates delivery within it. The budget lives on the Smart+ campaign (Campaign Budget Optimization); a lifetime budget additionally requires endDate. Cannot be combined with adSetId.

userOs?array<>

Meta only. Operating systems and version ranges, such as iOS_ver_14.0_and_above or Android. Emitted as user_os. May also be supplied inside targeting.

Items1 <= items
userDevice?array<>

Meta only. Device models such as iPhone. Emitted as user_device. May also be supplied inside targeting.

Items1 <= items
isSkadnetworkAttribution?boolean

Meta app promotion only. Immutable campaign flag. Set true for iOS 14+ SKAdNetwork campaigns and supply promotedObject.applicationId plus promotedObject.objectStoreUrl. The campaign receives promotedObject only when this flag is true. Cannot be changed on an existing campaign.

campaignAttribution?string

Meta ad-set attribution. Required as SKADNETWORK for iOS 14+ app promotion or a SKAdNetwork campaign. Requires AUCTION buying. Standalone Meta ad-set creation is not supported; use this field on /v1/ads/create.

Value in

  • "AEM"
  • "SKADNETWORK"
promotedObject?

What the ad optimises against. Behaviour depends on the platform.

Meta: forwarded to the ad set's promoted_object (snake-cased). For goal: app_promotion, it is also sent on the campaign only when isSkadnetworkAttribution: true. Plain Android app installs keep the existing campaign payload, with the promoted object only on the ad set. POST /v1/ads/campaigns forwards this object on Meta only for that explicit SKAN flag. Required for goals whose ad-set optimization_goal points at a specific event/page/app (without it Meta rejects the ad-set create with error_subcode: 1815430 "Please select a promoted object for your ad set"):

  • goal: conversions / lead_conversion (OFFSITE_CONVERSIONS): requires pixelId + customEventType, or customConversionId when optimising against a Custom Conversion (the conversion carries its own event definition). For a pixel CUSTOM event (one you named yourself in CAPI/Events Manager), send customEventType: OTHER + customEventStr with the event name.
  • goal: app_promotion (APP_INSTALLS): requires applicationId + objectStoreUrl
  • goal: lead_generation (LEAD_GENERATION): pageId is auto-filled from the connected Page when omitted

Other Meta goals (engagement, traffic, awareness, video_views) ignore this field.

TikTok: used by goal: conversions and the Smart+ goals (smartPlus: true).

  • pixelId maps to the ad group's pixel_id. Required: a TikTok website-conversion ad group without a pixel is rejected with 40002: Please select a pixel.
  • customEventType maps to the ad group's optimization_event (the pixel event to optimise for). Optional on the regular conversions flow, required on Smart+. See the customEventType field below for the valid TikTok codes.
  • applicationId (Smart+ goal: app_promotion only) maps to the ad group's app_id: the App ID of an app registered on the TikTok Ads account (Assets → Events → App Events). Install optimization needs the app's MMP tracking configured.

OpenAI (ChatGPT) Ads: goal: conversions only, on POST /v1/ads/create and POST /v1/ads/campaigns. customEventType picks the conversion event the campaign optimizes for (sent as the campaign's conversion_event_setting_ids); every other promotedObject field is rejected with a 400. See customEventType below.

Reddit Ads: Reddit puts a pixel on every ad group, whatever the goal. pixelId (or tracking.pixelId) picks it; when omitted the ad account's first pixel is used, and an ad account with no pixel returns 422 (create the pixel in Reddit Ads Manager). A pixelId that is not a pixel of the ad account returns 400. goal: conversions requires customEventType (see below). goal: lead_generation optimizes toward the pixel's Lead event. Reddit only optimizes toward an event the pixel has already received: when it never fired, the request returns 422 invalid_resource_state. Fire it from your site or send it with POST /v1/ads/conversions first.

The remaining promotedObject.* fields are Meta-only. Platforms other than Meta, TikTok, OpenAI and Reddit ignore promotedObject entirely.