> ## 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.

# Media Buy

> One AdCP transaction with one sales agent — platform-created or mirrored from a connected provider account

## Overview

A **Media Buy** represents one AdCP transaction with one sales agent. For
platform-managed campaigns, Apostra spawns media buys when a
[Campaign](/v2/object-guides/campaign) executes. During the tracked-campaign
alpha, Apostra can also project one storefront-managed media buy into one
read-only [tracked campaign](/v2/buyer/campaigns/directed-campaigns).
Campaigns managed by Apostra use the canonical buyer product and media-buy
lifecycle instead of a separate tracked-campaign write path.

When a campaign defines [channel groups](/v2/buyer/campaigns/channel-groups),
each media buy also preserves the coherent inventory selection it was created
for. The compiler creates one buy per channel group, seller, and settlement
currency, and reporting can roll delivery back up by that same group.

<Note>
  For platform-managed campaigns (`management: "managed"`), media buys are
  spawned at execution, not before. A `DRAFT` platform-managed campaign has no
  media buys. After `POST /campaigns/:id/execute`, the campaign holds a
  `mediaBuys[]` array — one entry per channel group, sales agent, and settlement
  currency (or per sales agent and currency when no groups are defined). A
  `directed` campaign instead
  always projects exactly one mirrored media buy and has no execute step.
  Manual product staging via `create_media_buys` is available on campaigns with
  `autonomy.inventorySelection: "manual"` or `"propose"` (the default). Campaigns
  with `autonomy.inventorySelection: "automatic"` reserve composition for
  `auto_select_products` and reject manual staging calls.
</Note>

### Where a media buy sits in the hierarchy

A campaign fans out into media buys, each of which fans out into packages, each of which delivers:

```
Campaign
└── Media Buy        (one per channel group, sales agent, and currency)
    └── Package      (one per product per pacing period)
        └── Delivery (impressions, spend, clicks)
```

See [Package](/v2/buyer/campaigns/packages) for the per-product, per-pacing-period unit beneath each media buy.

## Product format identity

Products embedded in a media-buy read expose their URL-free canonical
`formatOptions` declarations. For a composed product, these declarations are
the only format identity returned; Apostra does not manufacture a legacy
`creativeFormats` value or an agent URL from the canonical kind. A directly
connected older seller may still return `creativeFormats` while its catalog is
migrating, but new integrations should read `formatOptions` and use the stable
`format_option_id` to select the exact option during media-buy creation.

For a wholesale product, Apostra captures `formatOptions` from the exact
discovery result you selected and carries that snapshot onto the media-buy line
item. It does not re-read a later public or account-private seller cache when
serving the buy. Historical wholesale buys created before snapshots were stored
may omit the optional field; re-run discovery to create a buy with current
requirements.

Media buys created from an older composed-product row can temporarily retain
the exact legacy `creativeFormats` readback until that row is refreshed. This
compatibility path does not affect newly composed products.

## Budget

A media buy's budgets — its per-product budgets and the package budgets beneath it — are **gross**: fee-inclusive, in the buyer's currency, the same denomination as the campaign's `budget.total`. The media/fee split is derived at the fee terms locked when the buy was created, and media buy reads return it read-only:

```json theme={null}
{
  "mediaBuyId": "mb_abc123",
  "budget_denomination": "gross",
  "budget_breakdown": {
    "media_budget": 23000,
    "fee_amount": 2000,
    "fee_rate_percent": 8,
    "effective_gross_cpm": 20.11
  }
}
```

* `budget_denomination` — always `"gross"`.
* `budget_breakdown.media_budget` — the portion of the gross budget that buys media.
* `budget_breakdown.fee_amount` — Apostra fee inside the gross budget.
* `budget_breakdown.fee_rate_percent` — the fee rate locked when the buy was created. Budget updates on a live buy re-split at this rate; later contract changes affect new buys only.
* `budget_breakdown.effective_gross_cpm` — the buy's gross budget ÷ impression goal × 1000: the all-in price per thousand impressions, so "budget ÷ CPM = impressions" holds on the numbers you operate on (for a buy priced at a single fixed seller CPM, it works out to `seller CPM ÷ (1 − fee rate)`). `null` when the buy has no positive impression goal or no gross budget.

`budget_breakdown` is informational — you set and update gross budgets only. Legacy buys created before fee terms were locked carry neither `budget_denomination` nor `budget_breakdown`, and report delivered spend net, as the seller reported it. On all other buys, delivered spend on buyer surfaces is gross too, so delivery compares directly against these budgets. Sellers receive and report the media amount with the fee already carved out, and never see your fee rate. See [Budgets and fees](/v2/concepts/budgets-and-fees).

## Status

A media buy's `status` is one of:

| Status | Meaning |
| - | - |
| `DRAFT` | Created but not yet submitted |
| `PENDING_APPROVAL` | Submitted; awaiting seller approval |
| `INPUT_REQUIRED` | Seller needs additional information before approval can proceed |
| `ACTIVE` | Live and delivering |
| `PAUSED` | Spend halted; resumable |
| `COMPLETED` | Flight finished or fully delivered |
| `CANCELED` | Stopped before completion |
| `FAILED` | Submission or execution error |
| `REJECTED` | Seller declined the buy |
| `ARCHIVED` | Soft-deleted |

<Note>
  This is Apostra buyer rollup status — a coarser, derived view of the
  per-buy AdCP wire status (`pending_creatives`, `pending_start`, `active`, …) the
  sales agent returns, surfaced here as `adcp_status` on
  [Get media buy status](/v2/buyer/campaigns/tasks/get-media-buy-status).
  `pending_creatives` and `pending_start` both collapse to `PENDING_APPROVAL`
  here; `active` → `ACTIVE`, and so on. See
  [Media buy lifecycle](/v2/concepts/media-buy-lifecycle) for the full mapping.
</Note>

### Most-restrictive rule

For a multi-agent campaign, the surfaced status is the **most restrictive** across all agents. For example, if one media buy is `ACTIVE` and another is `PENDING_APPROVAL`, the campaign reports `PENDING_APPROVAL`. `INPUT_REQUIRED` signals that a seller is blocked waiting on more information.

### How status flows

Status updates reach you two ways:

* **Webhooks** from sales agents — preferred, near-real-time. No polling needed.
* **Polling** via `GET /api/v2/buyer/campaigns/:id/media-buy-status`, which queries each agent directly and persists any changes.

```bash theme={null}
curl https://api.apostra.com/api/v2/buyer/campaigns/cmp_987654321/media-buy-status \
  -H "Authorization: Bearer $SCOPE3_API_KEY"
```

<Tip>
  Polling is most useful right after execution, while you wait on publisher approvals. Once webhooks are flowing, prefer them — see [Get media buy status](/v2/buyer/campaigns/tasks/get-media-buy-status).
</Tip>

If you attach a creative while a seller is reviewing the media buy, Apostra
automatically retries delivery after approval. You do not need to detach,
reattach, or resubmit it. Retry can continue for up to 24 hours; if delivery
still has not completed, contact the seller or Apostra support. Some sellers
return their own creative ID after delivery. Apostra displays it when
available, but a missing seller ID alone does not mean delivery failed.

### Creative format fit

Creative format fit is evaluated for each product, not once for a campaign.
When a Creative is attached or considered for a media buy, Apostra compares
its canonical `formatKind` and `formatParams`, plus inspected uploaded-media
facts when available, with that product's declared format option. The response
is `fits`, `fits_with_adaptation`, `insufficient_facts`, or `nothing_fits`.
If the product requires a size, duration, codec, or container that the Creative
does not declare and its uploaded asset has not established, Apostra returns
`insufficient_facts` and names what is missing. It cannot confirm that an
uninspected or parameter-less Creative fits, but the attachment proceeds
unconfirmed. Required adaptations are named as
`resize`, `letterbox`, `trim`, or `transcode` with the target option's
parameters. `nothing_fits` names the closest option and what differs. An
attachment does not perform adaptations. A Creative that needs adaptations is
forwarded as-is for seller review, and the attachment records the required
adaptations in its audit record.

### Why a storefront held or rejected your buy

A `PENDING_APPROVAL` status means the storefront's operator is reviewing your buy
before it reaches inventory; a rejection carries a buyer-safe reason. Sellers
judge buys against **Brief Acceptance** (the categories, advertisers, and rules
they will and won't carry). A deterministic hard finding supplies a strong
operator-facing rejection signal, while an ambiguous or AI-judged conflict
escalates or fails closed. Clearly on-policy buys auto-forward; ambiguous
results, evaluator errors, and deterministic hard findings enter human review.
Understanding the seller-side model helps your agent interpret a hold or rejection
and adjust — see [AI Business Rules](/v2/concepts/ai-business-rules).

For an exact own-supply campaign managed by the Media Company that owns the
Storefront, revision-fenced launch confirmation and authenticated
non-cancellation updates are Storefront media-buy decisions. They do not create
a second operator approval task. The source can still return a pending or
rejected status, and every validation, readiness, financial, and safety gate
remains in force.

## Why isn't my buy live?

For storefront-routed buys, the platform derives a **why-visibility annotation** from its forwarding records and exposes it in three places: the single-buy lookup below, the nested `mediaBuys[]` in `GET /campaigns/:id` (camelCase fields), and the [media-buy status poll](/v2/buyer/campaigns/tasks/get-media-buy-status) (snake\_case fields). The fields are present only when forwarding state exists; a buy transacted directly with a sales agent carries none.

### One-call lookup

```bash theme={null}
curl https://api.apostra.com/api/v2/buyer/media-buys/mb_abc123 \
  -H "Authorization: Bearer $SCOPE3_API_KEY"
```

```json theme={null}
{
  "mediaBuy": {
    "mediaBuyId": "mb_abc123",
    "name": "CTV — Premium News",
    "status": "PENDING_APPROVAL",
    "pendingAt": "salesagent",
    "pendingReason": "awaiting_source_moderation",
    "pendingSince": "2026-07-03T09:12:00Z",
    "sourceMessage": "Awaiting manual moderation",
    "forwardedAt": "2026-07-03T09:11:58Z",
    "buyerReference": "sf:42:sf_mb_1783031864469_8pbc8mmr",
    "startTime": "asap",
    "endTime": "2026-07-31T23:59:59Z",
    "createdAt": "2026-07-03T09:10:00Z",
    "updatedAt": "2026-07-03T09:12:01Z"
  }
}
```

### `pendingReason` — what the buy is waiting on

`pendingReason` is a **derived annotation, never a status** — it explains a wait; the buy's `status` is unchanged. It is derived per source leg and rolled up to the most-blocking wait. See [Media buy lifecycle](/v2/concepts/media-buy-lifecycle#pendingreason-whose-side-owns-the-wait) for the full vocabulary and whose side owns each wait.

### `errorCode` — structured failure codes

When forwarding failed or the buy was rejected, `errorCode` carries one of a small buyer-safe set, and `errorOwner` says who owns the fix:

| `errorCode` | `errorOwner` | Meaning | What to do |
| - | - | - | - |
| `product_no_longer_available` | `platform` | A selected product no longer resolves on the storefront (catalog drift) | Re-run discovery and rebuild the buy with current products |
| `source_rejected` | `seller` | The inventory source declined the buy on its merits | Read `sourceMessage`, address the reason, resubmit |
| `storefront_rejected` | `seller` | The storefront operator rejected the buy in review | Read `sourceMessage` (the reviewer's note), adjust, resubmit |
| `source_unavailable` | `seller` | The source could not be reached or went silent — not a refusal | Retry later; escalate with your `buyerReference` if it persists |
| `source_configuration_error` | `seller` | The source is not configured to process this media buy | Ask the seller to correct the source configuration; retrying the same request will not help |
| `product_configuration_error` | `seller` | The selected product contains stale or unusable configuration | Re-run discovery and rebuild the buy with a current product; ask the seller to correct the product if it remains available but still fails |
| `invalid_request` | `buyer_input` | The request needs a correction from you | Fix the flagged input and resubmit |
| `quote_expired` | `platform` | The held cross-currency rate expired before the buy forwarded | Re-discover and submit a new buy at the current rate |
| `platform_error` | `platform` | A platform-side fault | Contact support with your `buyerReference` |

`sourceMessage` is the first detail to read. When present, it contains a sanitized explanation from the source or reviewer, or buyer-safe correction guidance from the storefront forwarder.

### `buyerReference` — the handle to quote

`buyerReference` (`sf:<storefrontId>:<mediaBuyId>`) identifies this buy's exchange with the seller. Quote it, together with `forwardedAt`, when contacting the seller or Apostra support about a stuck buy — it is the reference both sides can look up.

### Media-buy identifiers and source references

A storefront-routed buy can have a second identifier minted by the inventory
source that executes it. Successful `create_media_buy` and `update_media_buy`
responses may include buyer-safe pointers in `ext.source_references`:

```json theme={null}
{
  "ext": {
    "source_references": [
      {
        "source_name": "Example Sales Agent",
        "media_buy_id": "mb_upstream_123"
      }
    ]
  }
}
```

* A sales-agent source leg includes its source name when available and upstream
  `media_buy_id`.
* An ad server source leg includes the upstream `media_buy_id` without naming
  the internal source.
* Linked and modular source legs are omitted. Their underlying source topology
  is intentionally private.

Use a returned source reference when the counterparty asks for the identifier
in its own system. Continue to use `mediaBuyId` with Apostra APIs and
`buyerReference` when asking Apostra or the storefront to trace the full
exchange. The field is absent when no eligible upstream reference is available.

### Transition notifications

The same facts push to your [notification stream](/v2/guides/notifications) as `media_buy.*` events, fired once per transition (never per retry):

| Event | Fired when |
| - | - |
| `media_buy.forward_failed` | The first forward attempt failed (payload carries `errorCode`, and `pendingReason` when the platform is retrying or a correction is needed) |
| `media_buy.awaiting_source_moderation` | The source accepted the buy asynchronously and is moderating it |
| `media_buy.source_rejected` | The source declined the buy (payload carries the sanitized reason) |
| `media_buy.stuck` | The buy never resolved at the source (timeout or task failure) and needs attention |

Each payload includes `mediaBuyId`, `buyerReference`, and the applicable `pendingReason` / `errorCode` / `errorOwner` / `sourceMessage`, so a reacting agent does not need a second call to learn why.

## Optimization goals

Optimization goals are applied at the media-buy level — they propagate to every package in the media buy at execution time. Each goal is either **event-based** (optimize against tracked conversions) or **metric-based** (optimize against seller-native delivery metrics).

<CodeGroup>
  ```json Event goal — optimize for tracked conversions theme={null}
  {
    "kind": "event",
    "event_sources": [
      { "event_source_id": "website_pixel", "event_type": "purchase", "value_field": "value" }
    ],
    "target": { "kind": "cost_per", "value": 25.0 }
  }
  ```

  ```json Metric goal — optimize for seller-native delivery metrics theme={null}
  {
    "kind": "metric",
    "metric": "completed_views",
    "target": { "kind": "cost_per", "value": 0.08 }
  }
  ```
</CodeGroup>

### Target kinds

| Target kind | Meaning |
| - | - |
| `cost_per` | Target CPA (events) or CPM-like rate (metrics) |
| `per_ad_spend` | Target ROAS — value per dollar spent (events only) |
| `maximize_value` | No target — maximize total event value within budget |
| `threshold_rate` | Maintain a minimum rate of the metric (metrics only) |

This level does not yet support an explicit `strength` on a `cost_per`/`per_ad_spend` target — some sellers (e.g. Meta) require a specific strength (`cap` for `cost_per`, `floor` for `per_ad_spend`) and reject a goal here that omits it. Set the goal on the campaign's `performanceConfig.optimizationGoals` instead (see [Optimization goal target strength](/v2/object-guides/campaign#optimization-goal-target-strength-performanceconfig)) when targeting such a seller.

### Supported metrics

`clicks`, `views`, `completed_views`, `viewed_seconds`, `attention_seconds`, `attention_score`, `engagements`, `follows`, `saves`, `profile_visits`, `reach`.

`impressions` is a delivery rollup — something a package reports, not a metric you can target (see [Delivery metrics](/v2/object-guides/campaign#packages)). For a reach or CPM-style goal, target `metric: "reach"`:

```json Reach goal — target unique reach at a minimum frequency theme={null}
{
  "kind": "metric",
  "metric": "reach",
  "reach_unit": "individuals",
  "target_frequency": { "min": 3, "window": { "interval": 7, "unit": "days" } }
}
```

`reach_unit` is required when `metric` is `reach` — one of `individuals`, `households`, `devices`, `accounts`, `cookies`, `custom`, and must be a unit the product declares support for. `target_frequency` is optional — omit it to let the seller maximize unique reach without a frequency constraint.

<Note>
  This is the `mediaBuys[].optimization_goals` field on an *executed* media buy (snake\_case, the AdCP wire format — `reach_unit`/`target_frequency` included). It is distinct from `performanceConfig.optimizationGoals` (camelCase), which sets goals when *creating* a performance campaign and does not yet support `reach_unit`/`target_frequency` — conversely, `performanceConfig.optimizationGoals` supports an explicit target `strength`, which this level does not yet support.
</Note>

## Goal commitment

Every media buy booked after commitments were recorded carries a
`goalCommitment`: what the seller committed to for your goal at the moment
the buy was created. It is read off terms, never prose, and it never changes
on the row it was booked on; a later change to the buy books a new row with
its own commitment.

| Field | Meaning |
| - | - |
| `source` | Where the answers came from: `proposal` when at least one product on the buy keeps the accepted proposal version's answer; `campaign` when every answer was derived from the campaign's goals and the products' own pricing and delivery terms. |
| `proposalVersionId` | The accepted proposal version the buy was booked from, whenever there was one. It stays on the buy even when `source` reads `campaign` because none of the buy's products matched that version's answers, so you can always trace what was accepted. |
| `kind` | `guaranteed`, `best_effort`, or `report_only`, using the same rules as a proposal's commitment (see [Read what a seller committed to](/v2/setup/v3/buyer-workflows#read-what-a-seller-committed-to)). The buy is only as committed as its least committed product. |
| `askedGoal` | The goal you stated on the campaign when the buy was booked, or `null` when there was none. |
| `answeredTarget` | The target the products' terms commit or aim at for that goal, of the same kind as your ask when they answer it: the worst across products, meaning the highest cost per unit, the lowest rate, or the lowest return. `null` when nothing answered a number. |
| `meetsAskedTarget` | Whether every product answers your target, judged by its kind: a cost per unit is met at or under your ask, a rate or a return on ad spend at or above it. `false` when any product misses it or did not answer with the same kind, `null` when your goal carries no target or a maximise target, which has no number to meet. |
| `goalAnswers` | One answer per product: pricing model and fixed price, delivery type, measurement terms, and its own commitment kind. |
| `goalAnswers[].storefrontId` | The Storefront the answered line resolves to, fixed when the answer was booked. A later change to the draft keeps that line's answer for exactly that Storefront; two Storefronts' like-named products never share one. Present on every booked answer whose line resolved to a Storefront, campaign-derived or copied from an accepted proposal alike; absent only on a line with no resolved Storefront. |

A buy created without a proposal still gets a commitment from its own terms:
a fixed CPC on a guaranteed product reads `guaranteed` even if the campaign
stated no goal, so the price you are paying per result is always on record.

The commitment always answers for every product on the buy. When you accept a
proposal into a draft that already holds other products, each product the
proposal answered keeps that answer and every other product is answered from
its own pricing and delivery terms, so `kind` is the buy's weakest product,
never the proposal's alone. A proposal version can only be booked onto the
campaign it was quoted for; accepting a version that does not belong to the
campaign fails the booking rather than recording a commitment that is not
yours.

A product booked on several lines of the same buy gets one answer per line.
Each proposal answer goes to one line, and a line the proposal did not answer
is answered from its own booked terms, so one line's price is never copied onto
another.

The same `goalCommitment` is returned on the V3 `media_buy` reads (`get` and
`search`) and on each media buy in a campaign read with the `mediaBuys`
include. It is read-only. `get` returns at most 50 line answers, the same
bound as the line items it lists; `search` and the campaign include return at
most 20 per buy. When a buy has more answers than a read carries, that read
sets `goalAnswersTruncated: true`.

## Reading and tuning media buys

`GET /api/v2/buyer/media-buys/:mediaBuyId` returns the selected media buy's
`optimizationGoals` alongside its status and diagnostic fields. Use the
campaign read when you also need packages, products, or delivery details.

Because media buys aren't created directly, you act on them through `PUT /api/v2/buyer/campaigns/:id` using the `mediaBuys[]` array. Each entry targets one media buy by `mediaBuyId` and carries an `action`.

When lowering a campaign's `budget.total` below what live media buys have already allocated, include explicit `mediaBuys[].packages[].budget` reductions in the same request — the update is validated against the projected post-update allocation and applied atomically. A budget decrease that leaves live buys allocating more than the new total is rejected with `INSUFFICIENT_MEDIA_BUDGET`. See [Reducing campaign budgets](/v2/buyer/campaigns/tasks/update-campaign#reducing-campaign-budgets).

| Action | Effect |
| - | - |
| `update` | Default — modify budget, pacing, optimization goals, or creatives |
| `cancel` | Cancel a running media buy |
| `delete` | Archive the media buy |

### Update budget and optimization goals

<CodeGroup>
  ```json Update budget + optimization theme={null}
  {
    "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"
      }
    ]
  }
  ```

  ```json Cancel one media buy theme={null}
  {
    "mediaBuys": [
      { "action": "cancel", "mediaBuyId": "mb_abc123", "reason": "underperforming vs threshold" }
    ]
  }
  ```
</CodeGroup>

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

### Cancel a single package

To cancel one package without touching the rest of the media buy, pass `packageIds` on the `mediaBuys[]` entry:

```json theme={null}
{
  "mediaBuys": [
    {
      "action": "cancel",
      "mediaBuyId": "mb_abc123",
      "packageIds": ["pkg_2"],
      "reason": "format not supported on this device mix"
    }
  ]
}
```

See [Package](/v2/buyer/campaigns/packages) for what each package represents.

### Override creative attachment

Omitting `creative_ids` preserves the buy's existing creative assignments without revalidating, resyncing, or forwarding any creative data. Include `creative_ids` only when you intend to change which creatives are attached.

```json theme={null}
{
  "mediaBuys": [
    {
      "action": "update",
      "mediaBuyId": "mb_abc123",
      "creative_ids": ["cr_video_15s", "cr_video_30s"]
    }
  ]
}
```

* **Omit `creative_ids`** to leave existing assignments unchanged. The platform does not fetch, validate, sync, or forward creative data for the buy in this case.
* **Explicit `creative_ids`** replaces the buy's current assignments with exactly the provided list. Each ID must already be linked to the campaign and match a format accepted by the media buy's products. A validation error is returned if any ID fails this check — the field is not silently filtered.
* **`"creative_ids": []`** explicitly clears all creatives from the media buy.
* `creative_ids` is only valid with `action: "update"`. It is rejected with `cancel` or `delete`.

### Package flight-date changes

Updating a package's `startTime` or `endTime` is rejected with `CAPABILITY_NOT_SUPPORTED` when **both** of the following are true: the target package has no persisted flight dates, and the seller has not yet returned action metadata (`valid_actions` or `available_actions`) for this buy. Otherwise, the date change proceeds.

This guard prevents forwarding a date change to a seller that has not declared what mutations it supports. It typically means the buy has not yet received a complete seller response — wait for the seller to confirm the buy is active and has returned its capabilities before adjusting package dates.

### Pausing never waits for seller approval

A buyer pause reaches the seller's ad server immediately, including on a
storefront that reviews every change manually and on guaranteed buys. Pausing
suspends delivery only: it does not change the buy's budget, flight dates, or
contract, and the buyer alone carries the cost of the buy going dark. The
seller is notified that the pause took effect but is not asked to approve it.

This applies to an update whose only change is the pause. A pause combined
with any other change (a budget, date, or package edit) is an ordinary update
and follows the storefront's approval rules. See
[pause media buy](/v2/buyer/campaigns/tasks/pause-media-buy) for how to tell
that delivery actually stopped.

### Pausing on each kind of storefront

A pause is never refused because an earlier change to the same buy is still
pending, for example a budget change waiting for the seller's approval. The
earlier change keeps its place; when it is applied later, the buy stays paused.

* **Storefronts that forward to a sales agent**, including Apostra-hosted
  storefronts on Google Ad Manager: the pause goes straight to the seller's
  sales agent.
* **Ad-platform storefronts** (for example Google Ads, Amazon, Spotify, Meta,
  TikTok): the pause goes straight to the ad platform, whether you use
  Apostra's [pause media buy](/v2/buyer/campaigns/tasks/pause-media-buy) or
  call the storefront's AdCP `update_media_buy` with `paused: true` and nothing
  else.
* **Meta:** if another change to the same Meta campaign is still being written
  to Meta, the pause can wait up to 20 seconds for it to finish. If that change
  is still running after the wait, the pause is refused with a retryable
  `CONFLICT` (reason `media_buy_update_in_progress`); retry the pause.

Whenever a seller or ad platform refuses or fails a pause, you get an explicit
error, never a silent success, and Apostra's operations team is alerted
because the buy may still be delivering.

### When a seller is still applying a pause

Some sellers answer a pause with an AdCP "accepted, still processing" status
(`submitted` or `working`) instead of confirming it. Delivery has not stopped
yet, so Apostra does not show the buy as paused:

* The media buy stays `ACTIVE` and its `pauseRequestedAt` (on
  `GET /media-buys/:mediaBuyId`) records when the pause was first requested.
  A single-buy [pause](/v2/buyer/campaigns/tasks/pause-media-buy) returns
  `202 Accepted`.
* When the seller reports the buy paused, Apostra moves it to `PAUSED` and
  clears `pauseRequestedAt`.
* A [campaign pause](/v2/buyer/campaigns/tasks/pause-campaign) waiting on such
  a seller returns `503` and the campaign stays `ACTIVE`. It becomes `PAUSED`
  on its own once every media buy confirms.
* If the seller has not confirmed after two hours, Apostra's operations team is
  alerted once. Apostra never sends the pause again on its own; pausing again
  is your call and is safe.

A resume works the same way in reverse. When a seller answers a resume with
"accepted, still processing", the buy stays `PAUSED`, because it is not
delivering yet:

* A single-buy [reactivate](/v2/buyer/campaigns/tasks/reactivate-media-buy)
  returns `202 Accepted` with `resumeRequestedAt`.
* A [campaign reactivate](/v2/buyer/campaigns/tasks/reactivate-campaign)
  waiting on such a seller returns `503` with `reason`
  `campaign_reactivate_requested` when those buys are all that is outstanding.
* The buy becomes `ACTIVE` on its own once the seller reports it delivering,
  and so does its campaign.

### Resuming a paused media buy

A resume on its own restores terms the seller already approved, so it is not
treated as a material change:

* **Storefronts that forward to a sales agent** send a resume on its own
  straight to the seller's ad server, without asking the seller to approve it.
* **Ad-platform storefronts with manual review** (for example Google Ads,
  Amazon, or Spotify) review every change, so a resume still waits for the
  seller there.
* **A resume combined with any other change** (a budget, date, or package edit)
  is an ordinary update and follows the storefront's approval rules on every
  storefront.

See [reactivate media buy](/v2/buyer/campaigns/tasks/reactivate-media-buy) for
the responses.

### Updating a paused media buy

A `PAUSED` media buy accepts the same updates as an `ACTIVE` one — budget,
flight dates, name, packages, and creatives. Pausing halts delivery; it does
not lock the buy against edits.

* **Budget changes still require seller approval**, exactly as they would on
  an `ACTIVE` buy. The materiality classification that decides whether a
  change needs approval does not special-case `PAUSED` — any budget change is
  material regardless of the buy's current status.
* **Approving an update never resumes the buy.** Once a queued change is
  accepted, the newly-activated values (budget, dates, etc.) go live, but the
  buy's status is preserved as `PAUSED` — approval changes what the buy is
  worth, not whether it's running. The only way to move a buy from `PAUSED` to
  `ACTIVE` is [reactivate media buy](/v2/buyer/campaigns/tasks/reactivate-media-buy)
  (or a campaign-level [reactivate](/v2/buyer/campaigns/tasks/reactivate-campaign)).
* **Updates on a paused buy still propagate to the seller.** Submitting a
  change against a `PAUSED` buy is not a local-only edit — it queues exactly
  the same way it would for an `ACTIVE` buy. See
  [Pending changes: what is live vs what you asked for](/v2/concepts/media-buy-lifecycle#pending-changes-what-is-live-vs-what-you-asked-for)
  for how to read `pendingChange` while an update on a paused buy is in
  flight.

## Cascade behavior

Actions on the parent campaign cascade down to its media buys and packages:

* **Campaign pause** halts every media buy and package.
* **Campaign reactivate** brings them back to `ACTIVE` (post-flight if dates allow).
* **Creative manifest update** re-syncs to every media buy that uses the format.
* **Property list update** propagates to packages without a full re-execute.
* **Frequency caps** on the campaign or advertiser are enforced across all media buys.
* **Per-buy pause/reactivate** — [pause a single media buy](/v2/buyer/campaigns/tasks/pause-media-buy) and [reactivate a single media buy](/v2/buyer/campaigns/tasks/reactivate-media-buy) are the non-cascading counterparts: each touches exactly the targeted media buy, never the parent campaign row and never a sibling media buy. Use these when only one buy on a multi-agent campaign needs to stop or resume; use the campaign-level pause/reactivate above when the whole campaign should move together.

## Failures and debug

When a media buy fails to execute — a publisher rejection or an ADCP error — `POST /api/v2/buyer/campaigns/:id/execute` returns structured `errors[]`:

```json theme={null}
{
  "campaignId": "cmp_987654321",
  "previousStatus": "DRAFT",
  "newStatus": "ACTIVE",
  "success": false,
  "errors": [
    {
      "mediaBuyId": "mb_abc123",
      "salesAgentId": "agent_xyz",
      "message": "Sales agent returned 422: invalid format for product"
    }
  ]
}
```

Pass `debug: true` on execute to include the full ADCP request, response, and A2A debug logs in each error entry.

## Related concepts

<CardGroup cols={2}>
  <Card title="Campaign" href="/v2/object-guides/campaign" icon="rocket">
    The parent media plan that spawns media buys at execution
  </Card>

  <Card title="Package" href="/v2/buyer/campaigns/packages" icon="box">
    One per product per pacing period, beneath each media buy
  </Card>

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

  <Card title="Update campaign" href="/v2/buyer/campaigns/tasks/update-campaign" icon="pen-to-square">
    The endpoint that reads and tunes media buys
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.