> ## Documentation Index
> Fetch the complete documentation index at: https://docs.apostra.com/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`

For managed campaigns, the `mediaBuys[]` array lets you update, cancel, or
delete individual media buys and their packages in a single call. Tracked
tracked projections are read-only; their only accepted compatibility
update is a refresh request that reads from the seller.

Buyer-owned labels are managed separately through V3 MCP `save_campaign`,
not this REST endpoint. See [Dimensions and labels](/v2/object-guides/dimension).

## Request

<CodeGroup>
  ```bash curl theme={null}
  curl -X PUT https://api.apostra.com/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                                                                                                                                                                                                                           |
| `constraints.geo_countries`      | array  | No              | ISO 3166-1 alpha-2 country codes. On a campaign with live media buys, a change here is also pushed to every already-deployed package — see [Propagating geo targeting to live media buys](#propagating-geo-targeting-to-live-media-buys)                          |
| `constraints.geo_regions`        | array  | No              | ISO 3166-2 subdivision codes (e.g. `US-CA`). Same live-propagation behavior as `geo_countries`                                                                                                                                                                    |
| `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.

## Refresh a tracked campaign

The compatibility refresh request is:

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

This request prioritizes a fresh account read and reconciles the projection. It
does not change the connected provider account. Any other update shape for a
tracked campaign is rejected.

## 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 }]
    }
  ]
}
```

## Propagating geo targeting to live media buys

Changing `constraints.geo_countries` or `constraints.geo_regions` on a campaign
also pushes the new value to every already-deployed package on that campaign's
media buys — it is not limited to filtering future product discovery. The new
value **replaces** each package's existing declaration for that field.

```json Add a country to a live campaign's targeting theme={null}
{
  "constraints": { "geo_countries": ["AU", "NZ"] }
}
```

The response's top-level `geoConstraintPropagationResults` array reports one
entry per media buy with deployed packages:

```json theme={null}
{
  "campaign": { "...": "..." },
  "geoConstraintPropagationResults": [
    { "mediaBuyId": "mb_abc123", "status": "applied" },
    { "mediaBuyId": "mb_def456", "status": "unchanged" },
    {
      "mediaBuyId": "mb_ghi789",
      "status": "rejected",
      "reason": "Sales agent does not support the requested targeting: geo_countries. Remove these targeting fields or use a sales agent that declares support."
    }
  ]
}
```

* `applied` — the seller accepted the new value.
* `unchanged` — every package on that media buy already matched the requested value; nothing was sent.
* `rejected` — the seller (or its declared capabilities) does not support changing this field on an already-live buy. `reason` explains why. The campaign's own stored `constraints` still updates for future product discovery even when a live media buy rejects propagation.

`geoConstraintPropagationResults` is present whenever the request changes
`geo_countries` or `geo_regions` — as an empty array (`[]`) when the campaign
has no media buy with deployed packages yet (e.g. a DRAFT-only campaign; there
is nothing to propagate to). It is omitted entirely only when the request does
not touch `geo_countries` or `geo_regions` at all.

You cannot clear a field to "no value at all" this way: `geo_countries: []`
(or `geo_regions: []`) is rejected for any live package that currently has a
non-empty value, since there is no way to declare "no countries" to a seller —
only a non-empty replacement is supported.

A package that you also update explicitly via `mediaBuys[].packages[].targetingOverlay`
in the same request keeps that explicit value for the field(s) it sets; the
propagated campaign-level value is skipped for exactly those fields on that
package, not the whole package.

This behavior currently covers `geo_countries`/`geo_regions` only. Other
`constraints` fields (`language`, `device_type`, `device_platform`,
`geo_metros`, `geo_postal_areas`) are not propagated to live media buys.

Only one update to a given campaign can be applied at a time. A second
request that arrives while an earlier one on the same campaign is still
being applied — even one that doesn't touch `geo_countries`/`geo_regions` —
gets `409 CONFLICT` with a message telling you to retry shortly, rather than
risk silently reverting a `geo_countries`/`geo_regions` change that already
took effect. This is a brief window, not a queue: a non-`geo_countries`/
`geo_regions` request only holds the conflict window for its own write, not
for however long a concurrent geo propagation takes to reach the seller.

## 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 the request changes `constraints.geo_countries` or `constraints.geo_regions` and the campaign has live media buys, the response also carries a `geoConstraintPropagationResults` array — see [Propagating geo targeting to live media buys](#propagating-geo-targeting-to-live-media-buys).

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.
* `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.
* `409 CONFLICT` — another update to the same campaign was still being applied when this request arrived. Retry the request.
* `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>
