Ad Campaigns

Create a standalone campaign

Changed on

Creates an empty campaign, paused, so you can add ad sets and ads to it afterwards.

Creates a campaign WITHOUT its first ad set / ad, on the platform of the given accountId. Ad sets join it later via existingCampaignId on the create endpoints. Platform notes: on Meta a budget here is campaign-level (CBO) by definition; omit it for ABO (each ad set carries its own budget), and specialAdCategories is Meta-only (400 elsewhere); bidStrategy is Meta and Google (400 elsewhere), and Google also accepts portfolioBidStrategyId instead. Google, X and OpenAI require a budget (422 without one; OpenAI accepts daily or lifetime, Google only budgetType: daily). On OpenAI goal sets the campaign objective, and conversions needs an active standard conversion event on the account. On Reddit goal is one of traffic, awareness, conversions, video_views or lead_generation (422 otherwise) and the budget is optional: with one, the campaign uses Reddit's campaign budget optimization and carries the pixel (see promotedObject, which then needs customEventType for conversions), daily or lifetime (a lifetime budget needs endDate, 400 otherwise). startDate / endDate set the campaign's schedule and need that campaign budget (400 without one). Without a budget, each ad set carries its own budget and dates. LinkedIn creates the campaign GROUP (our campaign level) and rejects a budget, which lives on the campaign (ad set) level there; it comes back status: DRAFT. Created PAUSED (TikTok DISABLE) unless status: ACTIVE where the platform supports it.

Idempotency: send an Idempotency-Key header to make retries safe.

post/v1/ads/campaigns

Request

  • Base URL: https://zernio.com/api
  • URL: https://zernio.com/api/v1/ads/campaigns
  • Auth: HTTP bearer

Headers

Idempotency-Keystring

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. Only 2xx responses are stored, so a request that failed with a 4xx can be retried with a corrected body under the SAME key.

Request body

accountIdstring required

Zernio SocialAccount id (posting or ads variant); its platform decides where the campaign is created.

adAccountIdstring required

Platform ad account id (Meta act_<n>, Google customer id, LinkedIn account id, ...).

namestring required
goal'engagement' | 'traffic' | 'awareness' | 'video_views' | 'lead_generation' | 'lead_conversion' | 'job_applicants' | 'conversions' | 'app_promotion' | 'catalog_sales' | 'page_likes' | 'page_visits' required

Mapped to the ODAX objective (same mapping as POST /v1/ads/create).

isSkadnetworkAttributionboolean

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.

buyingType'AUCTION' | 'RESERVED'

Every platform buys at auction by default, so this only needs sending on Meta, and only to choose RESERVED. AUCTION is accepted on every platform and changes nothing. RESERVED (Reach & Frequency) is Meta-only and is rejected with a 400 elsewhere. SKAdNetwork app promotion requires AUCTION.

validateOnlyboolean

Meta and ChatGPT (OpenAI). Runs campaign validation without creating or persisting a campaign; Idempotency-Key storage is bypassed. Returns HTTP 200 with validateOnly true and status VALIDATED. OpenAI has no dry-run, so there Zernio checks the budget, goal and conversion event itself. true on any other platform returns 501 feature_not_available (same as POST /v1/ads/create); false is ignored.

specialAdCategoriesstring[]
budgetAmountnumber

Campaign-level (CBO) 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. Requires budgetType.

budgetType'daily' | 'lifetime'
status'ACTIVE' | 'PAUSED'
startDatestring date-time

Reddit only (400 elsewhere). Campaign start (ISO 8601); a value without an offset is read in the ad account time zone. Requires a campaign budget (budgetAmount + budgetType, 400 otherwise). Defaults to now.

endDatestring date-time

Reddit only (400 elsewhere). Campaign end (ISO 8601); a value without an offset is read in the ad account time zone. Requires a campaign budget (400 otherwise), and is required for budgetType: lifetime.

locationTargetingType'presence' | 'presence_or_interest'

Google only. Who a campaign's location targeting reaches (Google's campaign.geo_target_type_setting.positive_geo_target_type). presence: people in, or regularly in, the targeted locations. presence_or_interest: also people searching for or showing interest in them. Omitted leaves Google's default, presence_or_interest. Accepted on Search, Display and Performance Max campaigns. Excluded locations always use presence (Google refuses presence_or_interest for exclusions on these campaign types).

bidStrategy'LOWEST_COST_WITHOUT_CAP' | 'LOWEST_COST_WITH_BID_CAP' | 'COST_CAP' | 'LOWEST_COST_WITH_MIN_ROAS'

Campaign bid strategy. Meta stores bid_strategy alongside the budget, so this REQUIRES budgetAmount + budgetType on the same request; sending it without a campaign budget is a 400. A campaign carrying a strategy without its bid_amount makes every ad set created under it fail with an error that names the ad set (code 100, subcode 1815857), so the bad state is rejected up front rather than accepted. To bid at ad-set level on Meta, set the strategy there instead. 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.

bidAmountnumber

Whole currency units (USD: 5 = $5.00). Required for LOWEST_COST_WITH_BID_CAP and COST_CAP; ignored otherwise. On Meta, validated here but NOT stored: the campaign object has no bid_amount field, only bid_strategy lives on it, and the amount takes effect once an ad set joins this campaign (existingCampaignId on POST /v1/ads/create) and supplies its own bidAmount there. On Google, stored directly on the campaign's bidding strategy.

roasAverageFloornumber

Decimal ROAS multiplier (2.0 = 2.0x). Required for LOWEST_COST_WITH_MIN_ROAS.

portfolioBidStrategyIdstring

Google only. 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.

Example request

{
  "locationTargetingType": "presence"
}

Response

Campaign validation passed without creating a campaign.

validateOnlyboolean

Always true.

adAccountIdstring
campaignId''

Empty because no campaign was created.

objectivestring
status'VALIDATED'

Changes