---
title: "Create a Campaign Plan"
method: POST
path: "/context/campaign-plans"
tags: ["Campaign Plans"]
---

# Create a Campaign Plan

`POST /context/campaign-plans`

## Request

- Base URL: `https://api.adcrunch.dev`
- URL: `https://api.adcrunch.dev/context/campaign-plans`
- Auth: HTTP bearer

Creates a Campaign Plan. A Campaign Plan is the AdCrunch planning document. It is **not** a provider campaign. For provider campaigns, see the entities in the Observe API.

Only a name, a description and a currency are necessary. You write the window, the budget envelope and the Brand one part at a time. Write each amount in whole units of the currency: `40.5` is €40.50.

The new plan starts in draft. The answer is `409` if the slug already exists in your organization.

## Request body

- object
  - `brand_name` string: The Brand's slug. Optional and re-pointable — a Campaign Plan may precede its brand or cover a portfolio. Addressed by slug because that is the handle every Context surface uses; the Campaign Plan stores the id.
  - `currency` string, required: ISO 4217. Editable while no Line Item carries a budget, frozen thereafter.
  - `description` string, required: One-line summary — what a reader picks this Campaign Plan by.
  - `end_date` string: Omit for an always-on plan. Line Items inherit this.
  - `measurement` string: How success will be judged (markdown).
  - `name` string, required: Display name.
  - `rationale` string: The argument for the split — what a reviewer approves (markdown).
  - `slug` string: Optional explicit slug handle; normalized to kebab-case. Defaults to one derived from the name.
  - `start_date` string
  - `total_budget` number: The envelope: what we have. Omit for "no declared constraint" — which is not the same as zero.

## Response `201`

The Campaign Plan as created, in `draft`, at `revision` 1, with no rows. `allocation.allocated` is 0 and `unallocated` is the whole envelope.

- object
  - `allocation` object, required: Derived on every read. Never stored.
    - `allocated` number, nullable, required: The sum of the Line Item budgets. `null` when the lines mix units, because that sum would be meaningless.
    - `comparable` boolean, required: False when the lines mix totals and daily rates. A surface must then print "not comparable" rather than a plausible-looking wrong total.
    - `unallocated` number, nullable, required: The envelope less the allocated, where both are known.
    - `unit` 'total' | 'daily', nullable, required: The shared unit, when the lines agree on one.
  - `brandId` string, nullable, required: Nullable and mutable: a plan may precede its brand, cover a portfolio, or be re-pointed. Deleting a Brand leaves its plans standing.
  - `createdAt` number, required: Milliseconds since the Unix epoch, UTC.
  - `createdBy` string, required
  - `currency` string, required: Editable until the first Line Item budget, and frozen thereafter.
  - `description` string, required
  - `endDate` string, nullable, required: A calendar date, `YYYY-MM-DD`.
  - `id` string, required
  - `lineItems` object[], required
    - `advertiserId` string, nullable, required
    - `ageMax` number, nullable, required
    - `ageMin` number, nullable, required: Each bound is independently optional, so "35+" is expressible.
    - `budget` number, nullable, required: Whole units of the **plan’s** currency.
    - `campaignPlanId` string, required
    - `channel` 'meta' | 'tiktok' | 'snapchat' | 'google_search' | 'google_pmax' | 'google_display' | 'youtube' | 'programmatic_display' | 'linkedin' | 'x', required: The planner’s word for what is being bought, not a provider name. A plan may name a channel we cannot execute.
    - `countries` string[], required
    - `createdAt` number, required: Milliseconds since the Unix epoch, UTC.
    - `createdBy` string, required
    - `effectiveEndDate` string, nullable, required: The line’s own bound where it has one, else the plan’s.
    - `effectiveStartDate` string, nullable, required: A calendar date, `YYYY-MM-DD`.
    - `endDate` string, nullable, required: A calendar date, `YYYY-MM-DD`.
    - `executable` boolean, required: Whether this channel could be executed at all today. Derived from our capabilities, never a promise that it will be.
    - `executions` object[], required: What this line has spawned. Never a claim that it is *finished* — nobody declared how many objects a line should produce.
      - `advertiserId` string, required
      - `createdAt` number, required: Milliseconds since the Unix epoch, UTC.
      - `entityId` string, required: The provider’s own id, unprefixed, so it joins to `entities`.
      - `entityType` string, required: The provider’s word for the level — `campaign`, `adset`, `ad`.
      - `id` string, required
      - `mutationWorkflowId` string, nullable, required: The durable execution that performed the write, when known.
      - `provider` string, required
    - `gender` 'all' | 'men' | 'women', nullable, required
    - `id` string, required
    - `objective` 'awareness' | 'engagement' | 'leads' | 'sales' | 'traffic' | 'app_promotion', required
    - `personaId` string, nullable, required: Explains the targeting; never supplies its values.
    - `provider` string, nullable, required: Who sells this channel, or null when nobody we integrate with does.
    - `revision` number, required: Increments on every write. Send it back as `base_revision` to edit; a stale one answers 409.
    - `startDate` string, nullable, required: A calendar date, `YYYY-MM-DD`.
    - `status` 'draft' | 'validated', required: `validated` is the gate on execution.
    - `unit` 'total' | 'daily', required: How `budget` reads, derived from the effective end date. Present → a total for the period; absent → a daily rate. A surface must print this beside the amount.
    - `updatedAt` number, nullable, required: Milliseconds since the Unix epoch, UTC.
  - `measurement` string, nullable, required
  - `name` string, required
  - `rationale` string, nullable, required
  - `revision` number, required: Increments on every write. Send it back as `base_revision` to edit; a stale one answers 409.
  - `slug` string, required
  - `startDate` string, nullable, required: A calendar date, `YYYY-MM-DD`.
  - `status` 'draft' | 'approved', required: The only two stored states. `active`, `closed` and "executed" are derived, never stored.
  - `totalBudget` number, nullable, required
  - `updatedAt` number, nullable, required: Milliseconds since the Unix epoch, UTC.

## Other responses

- `400`: The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed.
- `401`: No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`.
- `402`: The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`.
- `403`: The caller does not hold `campaign_plan:write`. `error` is `forbidden`.
- `404`: `brand_name` names no Brand in this organization. `error` is `not_found`. Despite its name the field takes a Brand **slug**.
- `409`: Another Campaign Plan already holds that slug. `error` is `slug_conflict`.
- `422`: Four codes share this status. `invalid_slug`; `invalid_currency` (it must be a three-letter ISO 4217 code); `invalid_amount` (an amount has no more decimals than its currency, and is never negative); `invalid_window` (the end precedes the start).

---

[API](https://skmtc.dev/adcrunch/apis/adcrunch-api.md) · [All operations](https://skmtc.dev/adcrunch/apis/adcrunch-api/llms.txt) · [OpenAPI document](https://skmtc.dev/adcrunch/apis/adcrunch-api/revisions/a420ba80914f?raw)
