> ## Documentation Index
> Fetch the complete documentation index at: https://docs.semicola.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Campaign

> The unit of media planning: a brief, a flight, a budget, and the media buys that carry them out.

A **campaign** is where a buyer's plan lives. It holds the brief sellers read, the flight dates and
budget, any targeting, the creatives that run, and the media buys placed with each seller. Every
campaign belongs to exactly one [advertiser](/help/glossary#advertiser) and inherits its currency.

A campaign starts as a `DRAFT`. The draft is your **cart**: you add proposals and products to it and
stage media buys against it without contacting any seller. Launching turns the staged buys into real
AdCP transactions, one per seller.

## Key fields

<ResponseField name="campaignId" type="string" required>
  Stable id with the `cmp_` prefix.
</ResponseField>

<ResponseField name="advertiserId" type="integer" required>
  The advertiser that owns the campaign. The budget currency must match the advertiser's primary
  currency.
</ResponseField>

<ResponseField name="name" type="string" required>
  Display name, 1 to 200 characters.
</ResponseField>

<ResponseField name="brief" type="string">
  The plain-language description sellers read: goals, audience, channels, must-haves and
  constraints. Up to 20,000 characters. The better the brief, the better the proposals.
</ResponseField>

<ResponseField name="status" type="enum">
  `DRAFT`, `ACTIVE`, `PAUSED`, `COMPLETED`, `CANCELED` or `ARCHIVED`. See [lifecycle](#lifecycle).
</ResponseField>

<ResponseField name="management" type="enum">
  `managed` for campaigns authored in Semicola, which places and runs their buys. Every campaign
  you create today is `managed`. The value `tracked` is reserved for campaigns mirrored read-only
  from a connected ad platform account; mirroring isn't available yet, so no campaign is
  `tracked` and the campaign list's **Tracking** lens stays empty.
</ResponseField>

<ResponseField name="flight" type="object">
  `startAt` and `endAt`, both ISO 8601 date-times with an offset.
</ResponseField>

<ResponseField name="budget" type="object">
  `total` (decimal), `currency` (ISO 4217), `pacing` (`EVEN`, `ASAP` or `FRONTLOADED`) and an
  optional `dailyCap`. Every package staged on the campaign is sent to its seller with the
  matching pacing (`even`, `asap` or `front_loaded`); change a single package's pacing with
  `update_media_buy`.
</ResponseField>

<ResponseField name="allocatedBudget / unallocatedBudget" type="decimal string">
  How much of the total is committed to staged or live media buys, and how much is left. Allocated
  budget can never exceed the total.
</ResponseField>

<ResponseField name="targeting" type="object">
  Structured intent sent with the brief: `countries`, `channels`, `ageRange` (`min` 13 or more,
  `max` 99 or less), `audience` (free text) and `geoMetros`. Sellers that cannot honor part of it
  say so in their proposal.
</ResponseField>

<ResponseField name="autonomy" type="object">
  How independently Semi works on the campaign: `inventorySelection` and `rebriefing`, each `manual`,
  `propose` or `automatic`. Without a value, a new campaign copies its advertiser's setting. With
  `inventorySelection: "automatic"`, manual staging is refused and products come from
  [auto-select](/buy/autonomy-settings#auto-select-products). The campaign workspace shows it under
  Controls as **Inventory selection** and **Re-briefing**, marked when it differs from the advertiser.
  See [Autonomy and auto-select](/buy/autonomy-settings).
</ResponseField>

<ResponseField name="performanceConfig" type="object">
  Holds `optimizationGoals`: the ranked goals sellers optimize toward, primary first, 1 to 10. Set
  them with `save_campaign` (`optimizationGoals`) or REST `performanceConfig`. See
  [Optimization goals](#optimization-goals).
</ResponseField>

<ResponseField name="channelGroups" type="array">
  Coherent inventory selections (for example mobile web display and CTV). Each group becomes its own
  media buy per seller, and delivery can be reported by group. See [Channel groups](/buy/channel-groups).
</ResponseField>

<ResponseField name="frequencyCaps" type="array">
  Buyer-side caps (`max_impressions` per `window`). Stored, never sent to sellers and not enforced; see
  [Frequency caps](/guides/frequency-caps#buyer-side-caps).
</ResponseField>

<ResponseField name="labels" type="object">
  Values from your account's dimensions, such as `{ "market": ["us"], "tags": ["launch"] }`. Set with
  `save_campaign`; filter campaigns by them. See [Dimensions and labels](/buy/dimensions-and-labels).
</ResponseField>

<ResponseField name="creativeFormats" type="object">
  Format coverage for launch: `required`, `covered` and `missing`.
</ResponseField>

<ResponseField name="mediaBuys" type="array">
  The campaign's media buys, one per seller (and settlement currency). See [media buys and
  packages](#media-buys-and-packages).
</ResponseField>

<ResponseField name="revision" type="integer" required>
  Increments on every change. Writes that modify a campaign send `expectedRevision`; a stale value
  fails with `REVISION_CONFLICT` so two agents never overwrite each other.
</ResponseField>

## Optimization goals

A goal tells each seller what to optimize the campaign's packages toward. Goals are opt-in: set them
when the buyer states one. There are two kinds.

**Event goals** (`kind: "event"`) optimize toward conversions from the advertiser's
[event sources](/buy/event-sources):

| Field               | Notes                                                                                                                                                                 |
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `eventSources`      | 1 to 10 entries: `eventSourceId` (the source key), `eventType`, and optional `customEventName` (required for `custom`), `valueField` and `valueFactor`.               |
| `target`            | Optional: `cost_per` with a `value` (target cost per event), `per_ad_spend` with a `value` (target return; every entry then needs `valueField`), or `maximize_value`. |
| `attributionWindow` | Optional: `postClick` and optional `postView`, each an `interval` and a `unit` (`minutes`, `hours`, `days` or `campaign`).                                            |
| `priority`          | Optional; defaults to the goal's position in the list (primary = 1).                                                                                                  |

**Metric goals** (`kind: "metric"`) optimize toward a delivery metric the seller tracks: `metric` is
one of `clicks`, `views`, `completed_views`, `viewed_seconds`, `attention_seconds`,
`attention_score`, `engagements`, `follows`, `saves`, `profile_visits` or `reach`, with optional
`viewDurationSeconds`, a `target` (`cost_per` or `threshold_rate`, each with a `value`),
`attributionWindow` and `priority`. A campaign's reach goal takes no `reachUnit`: sellers need one,
so each package is sent the first reach unit its product declares, and a package whose product
declares none is sent without the reach goal.

```json theme={null}
{
  "optimizationGoals": [
    {
      "kind": "event",
      "eventSources": [{ "eventSourceId": "glaze-web", "eventType": "purchase", "valueField": "value" }],
      "target": { "kind": "per_ad_spend", "value": 4 }
    },
    { "kind": "metric", "metric": "clicks" }
  ]
}
```

* **Set.** `save_campaign` takes `optimizationGoals` on create and on update. Over REST, send
  `performanceConfig: { "optimizationGoals": [ … ] }` on `POST /campaigns`, `PUT /campaigns/{id}` or
  `campaign.create` in the media-buy batch (`POST /media-buys/batch`).
* **Change.** A list replaces the whole ranked list; omitting the field keeps the current goals;
  `null` clears them (REST: `performanceConfig: null`). An empty list is refused. A change is recorded
  in the campaign's activity as **Optimization goals**.
* **Validation.** Every `eventSourceId` must be a registered source of the campaign's advertiser.
  Otherwise the write fails with `VALIDATION_ERROR` on `optimizationGoals`, naming the missing keys in
  `details.unknownEventSourceIds`. Register sources first (see
  [Event sources](/buy/event-sources)). Each `eventType` must be one the source sends (its
  `eventTypes`). Otherwise the write fails the same way, and each mismatch is listed in
  `details.unsupportedEventTypes` with `eventSourceId`, `eventType` and the source's
  `sourceEventTypes`.
* **Sent.** When a buy goes to its seller, every package carries the ranked goals as AdCP
  `optimization_goals`. Each event source is replaced by the seller's own id for it; an event goal
  whose source isn't `provisioned` on that seller, or whose seller has **Events** turned off in
  [Connections](/buy/connections#distribution), is left out of that buy, and the buy goes ahead. See
  [How a source reaches each seller](/buy/event-sources#how-a-source-reaches-each-seller).
* **Live buys.** Changing the goals on a live campaign sends the new ranked list to each of its
  `ACTIVE` and `PAUSED` buys with `update_media_buy` (`packages[].optimization_goals`; `null` sends an
  empty list). The response carries `optimizationGoalsCascadeResult`: `totalMediaBuys`,
  `updatedCount`, `pendingCount`, `skippedCount`, `failedCount` and one `results` entry per buy
  (`outcome` `updated`, `pending`, `skipped` or `failed`, with a `reason`). `pending` means the
  seller must approve first; poll its `updateProposalId` with `get_update_proposal`. A buy is
  skipped when **Buy** is off for its seller, when a change is already waiting for that seller's
  approval, or when it has no seller-side buy. Draft buys pick up the goals when they're sent.

Not available yet: goals per media buy or per product, and showing goals in the campaign workspace
(read them from `performanceConfig`).

## Lifecycle

```text theme={null}
DRAFT --launch (confirmed)--> ACTIVE --pause--> PAUSED --reactivate--> ACTIVE
DRAFT | ACTIVE | PAUSED --cancel--> CANCELED
ACTIVE --flight over and every buy finished--> COMPLETED
any finished state --archive--> ARCHIVED
```

Pausing a campaign pauses every media buy on it. Reactivating is the only way back from `PAUSED`.

### Launch preconditions

A draft can launch when all of these hold:

* the advertiser is active and the budget currency matches its primary currency;
* at least one media buy is staged;
* allocated budget does not exceed the total;
* the required creative formats are covered, or you accept that some buys will wait for creatives;
* the account is allowed to buy live (otherwise only sandbox launches are possible).

The **Review & go live** widget (`open_campaign_receipt`) shows each of these as a checklist with any
blockers.

## Common operations

### Create a draft

<CodeGroup>
  ```json MCP (save_campaign) theme={null}
  {
    "advertiserId": 12,
    "name": "Holiday Glaze Push",
    "brief": "Reach gift shoppers aged 30 to 55 in the US with CTV, audio and display.",
    "flight": { "startAt": "2026-11-15T00:00:00-05:00", "endAt": "2026-12-31T23:59:59-05:00" },
    "budget": { "total": 50000, "currency": "USD", "pacing": "EVEN" },
    "targeting": { "countries": ["US"], "channels": ["ctv", "streaming_audio", "display"] },
    "idempotencyKey": "glaze-holiday-2026-create-01"
  }
  ```

  ```bash REST theme={null}
  curl -X POST https://api.semicola.com/api/v2/buyer/campaigns \
    -H "Authorization: Bearer $SEMICOLA_API_KEY" \
    -H "X-Account-Id: $ACCOUNT_ID" \
    -H "Idempotency-Key: glaze-holiday-2026-create-01" \
    -H "Content-Type: application/json" \
    -d '{"advertiserId": 12, "name": "Holiday Glaze Push", "budget": {"total": "50000", "currency": "USD"}}'
  ```
</CodeGroup>

If a required fact is missing (most often the currency), the tool answers with `needs_input` and a
question instead of guessing. Ask the person, then call again.

### Collect proposals and stage buys

1. `request_proposals` with `campaignId` and `expectedCampaignRevision` sends the brief to every
   eligible seller and returns an execution id straight away. Only one execution runs per buyer at a
   time.
2. Poll with `get` (`kind: "proposal"`) or watch the Proposals widget as sellers answer.
3. `refine_proposal` asks one seller for a revised version.
4. `save_media_buy` with `fromProposalId` stages the proposal's products as a draft media buy.

### Launch

Launching is deliberately two calls so a person can see the spend before it happens.

<Steps>
  <Step title="Ask to go live">
    Call `save_campaign` with `campaignId`, `expectedRevision` and `desiredPhase: "active"`. The
    result is `pending_confirmation`, with a summary of spend per seller and an expiry.
  </Step>

  <Step title="Get the person's approval">
    Show the summary. In the Semicola app and in MCP Apps hosts, a confirmation card does this for
    you.
  </Step>

  <Step title="Confirm">
    Call `save_campaign` again with `confirmLaunch: true` and the same `expectedRevision`. Semicola
    dispatches one AdCP `create_media_buy` per seller, each with its own idempotency key.
  </Step>
</Steps>

<Warning>
  A brief, a budget or a staged buy is never authority to spend. Agents must not set `confirmLaunch`
  without an explicit approval from the person they act for.
</Warning>

The launch result reports `mediaBuysExecuted` and a list of `errors`, one per failed buy, with a
`recovery` of `transient`, `correctable` or `terminal` and a `retrySafe` flag. Launching again
resubmits only the failed draft buys.

### Pause, reactivate, cancel, archive

Send `isPaused`, `desiredPhase: "canceled"` or `isArchived` to `save_campaign` with
`expectedRevision`. Over REST, use `POST /api/v2/buyer/campaigns/{id}/pause` and
`/reactivate`.

## Media buys and packages

A **media buy** is one AdCP transaction with one seller in one settlement currency. Its id starts
with `mb_`. A **package** (`pkg_`) is one product inside a media buy, with its own budget, pacing
(`even`, `asap` or `front_loaded`), optional bid price, flight and creatives.

| Media buy status       | Meaning                                                                                           |
| ---------------------- | ------------------------------------------------------------------------------------------------- |
| `DRAFT`                | Staged in the cart. No seller has seen it.                                                        |
| `PENDING_APPROVAL`     | Sent; the seller is reviewing it. `pendingAt` says whether the storefront or its source holds it. |
| `INPUT_REQUIRED`       | The seller needs something from you before it can continue.                                       |
| `ACTIVE`               | Accepted. It may still be waiting to start or waiting for creatives.                              |
| `PAUSED`               | Paused by you or by the campaign.                                                                 |
| `COMPLETED`            | The flight ended or the budget ran out.                                                           |
| `REJECTED`             | The seller declined it. The seller's reason is attached.                                          |
| `FAILED`               | Dispatch failed. Safe failures return to `DRAFT` on the next launch.                              |
| `CANCELED`, `ARCHIVED` | Stopped or filed away.                                                                            |

The campaign's **operational status** is the most restrictive status across its buys: one of
`no_media_buys`, `draft`, `pending_creatives`, `pending_start`, `active`, `paused`, `completed` or
`attention_required`. Read it from `GET /api/v2/buyer/campaigns/{id}/media-buy-status` or the campaign
workspace.

## Related

<CardGroup cols={2}>
  <Card title="Buying overview" icon="bullhorn" href="/buy/overview">
    The whole loop from brief to delivery.
  </Card>

  <Card title="v3 Tool Catalog" icon="toolbox" href="/v3/tool-catalog">
    Exact inputs for `save_campaign`, `request_proposals` and `save_media_buy`.
  </Card>

  <Card title="Errors" icon="triangle-exclamation" href="/v3/errors">
    Revision conflicts, `needs_input` and partial launches.
  </Card>

  <Card title="Glossary" icon="book" href="/help/glossary">
    Every term on this page, defined.
  </Card>
</CardGroup>
