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

# Approve a Campaign Plan

`POST /context/campaign-plans/{slug}/approve`

## Request

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

Approves a Campaign Plan and validates its draft Line Items. This opens the gate for execution: AdCrunch executes a line only after somebody validates it.

**This call blocks on nothing.** You can approve a Campaign Plan with no budget, with more allocated than the envelope, or with parts missing. Approval is the human act, and execution makes its own checks.

This call creates nothing and spends nothing. To execute a line, use the mutation tools with the `mutation:write` permission.

This call moves the `revision` of the Campaign Plan, and the `revision` of each Line Item it validates. Read the Campaign Plan again before your next guarded write.

A line that you add after the approval starts in draft, and it puts the Campaign Plan back into draft. The answer is `409` with `error=revision_mismatch` if the Campaign Plan changed after you read it.

## Path parameters

- `slug` string, required

## Request body

- object
  - `base_revision` integer, required: The revision last read via GET. This is what makes it an approval **of the content that was read**, rather than of whatever the row holds now.

## Response `200`

The approved plan, and how many Line Items this call validated. A line that was already validated is not counted. **Approving spends nothing and creates nothing** — it opens the execution gate, and executing goes through the mutation tools on `mutation:write`.

- object
  - `campaignPlan` object, required
    - `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.
  - `lineItemsValidated` number, required: How many draft Line Items this approval moved to `validated`. Zero when they were all validated already.

## 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. `error` is `not_found`.
- `409`: The plan moved since you read it. `error` is `revision_mismatch`. This is what makes the call an approval **of the content you read**, rather than of whatever the row holds now.

---

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