---
title: "Update a Campaign Plan"
method: PATCH
path: "/context/campaign-plans/{slug}"
tags: ["Campaign Plans"]
---

# Update a Campaign Plan

`PATCH /context/campaign-plans/{slug}`

## Request

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

Updates a Campaign Plan. Send the `base_revision` you read. Omit a field to keep it. Send `null` to clear it.

**Any change puts an approved plan back into draft.** An approval speaks about content, and it cannot survive a change to that content. A write that changes nothing keeps the approval, and it does not move the `revision`.

The Line Items keep the state they hold. The revert is local to the Campaign Plan, so a `validated` line stays validated.

The answer is `409` with `error=currency_frozen` if a Line Item already has a budget, with `error=revision_mismatch` if the Campaign Plan changed after you read it, and with `error=slug_conflict` if the new name gives a slug that already exists.

## Path parameters

- `slug` string, required

## Request body

- object
  - `base_revision` integer, required: The revision last read via GET — guards concurrent edits.
  - `brand_name` string, nullable: Re-point the Campaign Plan at another Brand by slug; null unlinks it. Refused with 409 error=persona_not_in_brand while any Line Item still names a Persona of the current Brand.
  - `currency` string: ISO 4217. Refused with 409 error=currency_frozen once any Line Item carries a budget — re-denominating a costed plan would silently reinterpret every number in it.
  - `description` string
  - `end_date` string, nullable: null makes the Campaign Plan always-on — which also flips every inheriting line’s amount from a total to a daily rate.
  - `measurement` string, nullable
  - `name` string
  - `rationale` string, nullable
  - `slug` string: Optional new slug handle (a deliberate rename).
  - `start_date` string, nullable
  - `total_budget` number, nullable: null means "no declared constraint", not zero.

## Response `200`

The Campaign Plan as it now stands, with its Line Items. A change takes the approval away and moves the `revision`; the Line Items keep the state they hold.

- 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`: No Campaign Plan with that slug, or `brand_name` names no Brand. `error` is `not_found` for both.
- `409`: Four codes share this status. `revision_mismatch`: somebody wrote first. `slug_conflict`: the rename collides. `persona_not_in_brand`: re-pointing the Campaign Plan would orphan Line Items that name a Persona of the current Brand, and `lineItemIds` names them — they are not cleared for you. `currency_frozen`: a Line Item already carries a budget, so the denomination is fixed; nothing is ever converted.
- `422`: `invalid_slug`, `invalid_currency`, `invalid_amount` or `invalid_window`.

---

[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)
