---
title: "Create a standalone campaign"
method: POST
path: "/v1/ads/campaigns"
tags: ["Ad Campaigns"]
---

# Create a standalone campaign

`POST /v1/ads/campaigns`

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` /
`bidStrategy` are Meta-only (400 elsewhere). Google, X and OpenAI require a budget
(422 without one; OpenAI accepts only `budgetType: lifetime`). 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`. TikTok campaigns are
created without a status and report `ENABLE`. Created `PAUSED` unless
`status: ACTIVE` where the platform supports it.

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

## Headers

- `Idempotency-Key` string

## Request body

- object
  - `accountId` string, required — Zernio SocialAccount id (posting or ads variant); its platform decides where the campaign is created.
  - `adAccountId` string, required — Platform ad account id (Meta act_<n>, Google customer id, LinkedIn account id, ...).
  - `name` string, required
  - `goal` 'engagement' | 'traffic' | 'awareness' | 'video_views' | 'lead_generation' | 'lead_conversion' | 'job_applicants' | 'conversions' | 'app_promotion' | 'catalog_sales' | 'page_likes', required — Mapped to the ODAX objective (same mapping as POST /v1/ads/create).
  - `specialAdCategories` string[]
  - `budgetAmount` number — 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'
  - `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, set the strategy there instead.
  - `bidAmount` number — Whole currency units (USD: 5 = $5.00). Required for LOWEST_COST_WITH_BID_CAP and COST_CAP; ignored otherwise. Validated here but NOT stored by Meta: the campaign object has no bid_amount field, only bid_strategy lives on it. The amount takes effect once an ad set joins this campaign (existingCampaignId on POST /v1/ads/create) and supplies its own bidAmount there.
  - `roasAverageFloor` number — Decimal ROAS multiplier (2.0 = 2.0x). Required for LOWEST_COST_WITH_MIN_ROAS.

## Response `201`

Campaign created

- object
  - `adAccountId` string
  - `campaignId` string — Platform id of the new campaign
  - `objective` string — Resolved ODAX objective (e.g. OUTCOME_SALES).
  - `status` 'ACTIVE' | 'PAUSED'

## Other responses

- `400` — Invalid input, or Meta rejected the create
- `401` — Unauthorized
- `501` — Only supported on Meta (facebook/instagram)

## Changes

- **2026-08-25** `44b6b1ca2a8b` — 1 info
  - added the new `page_likes` enum value to the request property `goal`

[Change history](https://skmtc.dev/zernio/apis/zernio-api/changes/v1/ads/campaigns/post.md)

---

[API](https://skmtc.dev/zernio/apis/zernio-api.md) · [All operations](https://skmtc.dev/zernio/apis/zernio-api/llms.txt) · [OpenAPI document](https://skmtc-service-production.skmtc.workers.dev/v1/apis/zernio/zernio-api/revisions/08611567febf/schema)
