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

# Update campaign

> Apply partial updates and act on individual media buys

`PUT /api/v2/buyer/campaigns/:campaignId`

Accepts mode-specific campaign updates. For platform-managed campaigns, the
`mediaBuys[]` array lets you update, cancel, or delete individual media buys and
their packages in a single call. For a separately write-enrolled directed
campaign, the strict directed branch forwards one explicit AdCP patch to the
connected provider and reconciles the one-to-one mirror.

## Request

<CodeGroup>
  ```bash curl theme={null}
  curl -X PUT https://api.interchange.io/api/v2/buyer/campaigns/cmp_987654321 \
    -H "Authorization: Bearer $SCOPE3_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "mediaBuys": [
        {
          "action": "update",
          "mediaBuyId": "mb_abc123",
          "packages": [{ "packageId": "pkg_1", "budget": 15000, "pacing": "even" }],
          "optimization_goals": [
            { "kind": "metric", "metric": "completed_views", "target": { "kind": "cost_per", "value": 0.08 } }
          ],
          "updated_reason": "Mid-flight optimization based on early performance"
        },
        { "action": "cancel", "mediaBuyId": "mb_def456", "reason": "underperforming" }
      ]
    }'
  ```

  ```json Override creatives for one media buy theme={null}
  {
    "mediaBuys": [
      {
        "action": "update",
        "mediaBuyId": "mb_abc123",
        "creative_ids": ["cr_video_15s", "cr_video_30s"]
      }
    ]
  }
  ```
</CodeGroup>

## Parameters

| Field                            | Type   | Required        | Notes                                                                                                                                                                                                                                                             |
| -------------------------------- | ------ | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`                           | string | No              | Rename the campaign (max 255)                                                                                                                                                                                                                                     |
| `budget`                         | object | No              | `{ total, currency, dailyCap?, pacing? }`. Gross — fee-inclusive, like every buyer budget. See [Reducing campaign budgets](#reducing-campaign-budgets) before lowering `total` on an executed campaign                                                            |
| `pacingPeriods`                  | object | No              | Append periods; returns a `pacingCascadeResult` in the response                                                                                                                                                                                                   |
| `frequencyCaps`                  | array  | No              | Replaces all existing non-archived caps                                                                                                                                                                                                                           |
| `mediaBuys[]`                    | array  | No              | Per-media-buy actions (see below)                                                                                                                                                                                                                                 |
| `mediaBuys[].action`             | enum   | Yes (per entry) | `update` (default — modify), `cancel` (cancel running), `delete` (archive)                                                                                                                                                                                        |
| `mediaBuys[].mediaBuyId`         | string | Yes (per entry) | Target media buy                                                                                                                                                                                                                                                  |
| `mediaBuys[].packages`           | array  | No              | Per-package `budget`, `pacing`, bid, and flight `startTime`/`endTime` overrides. Every `packageId` must belong to the named live media buy; unknown IDs are rejected before anything is sent to the seller. Dates must fall within the media buy's own date range |
| `mediaBuys[].packageIds`         | array  | No              | Scope a `cancel` to specific packages only                                                                                                                                                                                                                        |
| `mediaBuys[].creative_ids`       | array  | No              | Replace creatives for this buy. Omit to leave assignments unchanged; `[]` clears all creatives. Only valid with `action: "update"`                                                                                                                                |
| `mediaBuys[].optimization_goals` | array  | No              | Event- or metric-based goals applied to every package — see [Optimization goals](/v2/buyer/campaigns/media-buys#optimization-goals) for supported metrics                                                                                                         |

<Warning>
  Always confirm `optimization_goals` with the buyer before changing them — silent goal changes break optimization continuity.
</Warning>

Retrieve package IDs with [Get campaign products](/v2/buyer/campaigns/tasks/get-campaign-products), optionally filtered by `mediaBuyId`. When the buyer names a package by its period or its dates ("the Week 6 package", "the display package ending 2026-08-11"), use [Get media buy packages](/v2/buyer/campaigns/tasks/get-media-buy-packages) instead: it is scoped to one media buy and returns each package's `pacingPeriod` and flight window, which is what resolves a description to exactly one id. Package IDs are opaque, so do not derive one by parsing the trailing number on another. Do not submit a real update to test whether an ID is valid: the API validates package ownership before dispatch and returns `VALIDATION_ERROR` for unknown IDs.

## Directed update (alpha)

Read-only mirror refresh remains available without write enrollment:

```json theme={null}
{ "mode": "directed", "refresh": true }
```

An upstream mutation requires the separate directed-write gate, subscription
governance, and this strict shape:

```json theme={null}
{
  "mode": "directed",
  "idempotencyKey": "buyer-2026-07-cancel-001",
  "currency": "USD",
  "expectedRevision": 1,
  "mediaBuy": {
    "canceled": true,
    "cancellation_reason": "Canary completed"
  }
}
```

`mediaBuy` must contain at least one AdCP update field. The campaign URL resolves
the connected account and upstream identity, so nested `account`,
`media_buy_id`, `idempotency_key`, and `revision` fields are rejected. Use root
`expectedRevision` for optimistic concurrency. The root currency must match the
subscription governance currency.

A synchronous provider result returns `{ campaign, directedWrite }` with
`directedWrite.status: "CONFIRMED"`. If the provider accepted the write but the
mirror cannot yet prove it, the endpoint returns `202 Accepted` with the raw
write result's status `AWAITING_CONFIRMATION`; poll the campaign or use the
read-only refresh branch. Replaying the same idempotency key and body is safe. A
different body with the same key is a conflict and is never dispatched.

TikTok accepts campaign pause/resume, package budget, bid and end-time changes,
and `{ "canceled": true, "cancellation_reason": "..." }`. The
`expectedRevision` is checked against a fresh, complete provider snapshot before
any write. Package cancellation, new packages, start-time changes, and other
package fields fail closed. Requests may change at most 20 packages. Interchange
pauses and verifies an enabled campaign before child writes, requires exact
provider readback, and restores reversible partial failures to their prior
values. TikTok has no native cancellation-reason field; Interchange retains
`cancellation_reason` as audit metadata alongside the directed write. This
lifecycle is ready for a signed live canary and is not yet Green evidence.

## Reducing campaign budgets

Lower an executed campaign's spend through this campaign update endpoint. Do
not archive and rebuild a media buy just to reduce its budget.

Every budget is **gross** (fee-inclusive), so the comparison is direct: live
media buys can never allocate more than `budget.total`. Lowering
`budget.total` below what live media buys have already allocated is rejected
with `INSUFFICIENT_MEDIA_BUDGET` — the error names the new total and the
committed allocation so you know what to shrink or cancel first. Lowering into
headroom (new total at or above the current allocation) succeeds on its own.

To reduce the campaign and its live media buys together, put both changes in
**one request**: the new `budget.total` plus explicit
`mediaBuys[].packages[].budget` reductions. The request is validated against
the projected post-update allocation and applied atomically — if a package
reduction cannot be applied, the campaign-level `budget.total` is not lowered.

```json Lower into headroom — budget change alone theme={null}
{
  "budget": { "total": 80000, "currency": "USD" }
}
```

```json Reduce campaign budget and live packages in one call theme={null}
{
  "budget": { "total": 50000, "currency": "USD" },
  "mediaBuys": [
    {
      "action": "update",
      "mediaBuyId": "mb_abc123",
      "packages": [{ "packageId": "pkg_1", "budget": 12000 }]
    }
  ]
}
```

## Updating a package's flight dates

Change an individual package's flight start or end date without canceling and
recreating it — useful when one package needs a narrower or extended window
than the rest of the media buy. `startTime`/`endTime` must fall within the
media buy's own date range.

```json Extend one package's flight end date theme={null}
{
  "mediaBuys": [
    {
      "action": "update",
      "mediaBuyId": "mb_abc123",
      "packages": [{ "packageId": "pkg_1", "endTime": "2026-08-11T00:00:00Z" }],
      "updated_reason": "Extending this package's flight to match the new campaign end date"
    }
  ]
}
```

Only the packages you name are changed — sibling packages on the same media
buy keep their existing flight windows.

When a package has no stored flight dates and its seller has not returned
`valid_actions` or `available_actions` for the buy, the request is rejected as
unsupported rather than forwarded speculatively. Wait for seller capabilities
or confirm package date support with the seller first.

## Response

```json theme={null}
{
  "campaign": {
    "campaignId": "cmp_987654321",
    "advertiserId": "12345",
    "name": "Q2 2026 Tech Launch",
    "status": "ACTIVE",
    "optimizationApplyMode": "MANUAL",
    "mediaBuyRefs": [
      { "mediaBuyId": "mb_abc123", "status": "ACTIVE" },
      { "mediaBuyId": "mb_def456", "status": "CANCELED" }
    ],
    "createdAt": "2026-05-01T09:00:00Z",
    "updatedAt": "2026-05-15T12:00:00Z"
  }
}
```

When the request includes `pacingPeriods`, the response also carries a `pacingCascadeResult` block at the top level alongside `campaign`, summarizing the per-media-buy outcome of pushing appended periods to live media buys. See the [Pacing periods guide](/v2/guides/pacing-periods) for that shape, append-only rules, and unsupported-agent fallback.

When one or more media buy updates require seller approval (e.g. a seller-managed storefront), the server returns `202 Accepted` with a `proposals` array instead of the campaign object:

```json theme={null}
{
  "proposals": [
    {
      "proposalId": "prop_abc123",
      "mediaBuyId": "mb_abc123",
      "status": "PENDING_SELLER_APPROVAL"
    }
  ]
}
```

## Errors

* `400 VALIDATION_ERROR` — a `creative_ids` entry is not linked to the campaign or does not match a format the media buy's products accept (the field is not silently filtered); or `creative_ids` was supplied with `cancel`/`delete`.
* `422 CAPABILITY_NOT_SUPPORTED` — a package flight-date update cannot be safely sent because the package has no stored dates and the seller has not declared available actions.
* `403 FEATURE_NOT_ENABLED` — a directed mutation was requested for a customer without the separate write alpha.
* `409 CONFLICT` — a directed idempotency key was reused for a different body or `expectedRevision` is stale.
* `409 INSUFFICIENT_MEDIA_BUDGET` — the requested `budget.total` is below the projected live media buy allocation, even after applying the package budget reductions included in the same request. The error names the new total and the committed allocation.
* `409 PRICING_NOT_CONFIGURED` — the budget change involves media buys whose fee terms cannot be determined, so the projected allocation cannot be computed. `details.unpricedBuyIds` names the buys; resolve pricing for them first.
* `404 NOT_FOUND` — campaign or referenced `mediaBuyId` not found.

See [Errors](/v2/reference/errors) for the full error contract.

## Related

<CardGroup cols={2}>
  <Card title="Get campaign" href="/v2/buyer/campaigns/tasks/get-campaign" icon="magnifying-glass">
    Read the current resource first
  </Card>

  <Card title="Get media buy status" href="/v2/buyer/campaigns/tasks/get-media-buy-status" icon="signal">
    Poll live ADCP status
  </Card>

  <Card title="Pacing periods" href="/v2/guides/pacing-periods" icon="calendar-week">
    Append-only pacing cascade
  </Card>

  <Card title="Campaign overview" href="/v2/object-guides/campaign" icon="rocket">
    Fields, lifecycle, and concepts
  </Card>
</CardGroup>
