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

# Media Buy

> One ADCP transaction with one sales agent — platform-created or mirrored from a connected seller

## Overview

A **Media Buy** represents one AdCP transaction with one sales agent. For
platform-managed campaigns, Interchange spawns media buys when a
[Campaign](/v2/object-guides/campaign) executes. During the directed-campaign
alpha, Interchange can also project one storefront-managed media buy into one
[directed campaign](/v2/buyer/campaigns/directed-campaigns), through either a
connected account or a direct AdCP storefront request. Separately write-enrolled
accounts can author and update that upstream media buy through
the strict directed branches of the campaign API.

<Note>
  For `discovery` and `performance` modes, 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 sales agent. A `directed` campaign instead always projects exactly
  one mirrored media buy and has no execute step.
</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 sales agent)
    └── 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.

## 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` — the Scope3 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 the Interchange 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.interchange.io/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>

### 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 the operator's reason. Sellers judge buys against an **acceptance policy** (the categories, advertisers, and rules they will and won't carry) — a buy can be auto-forwarded as clearly on policy, held for the operator to look at, or flagged as against an explicit rule. The platform never auto-rejects; a human makes the call. Understanding the seller-side model helps your agent interpret a hold or rejection and adjust — see [Acceptance policy](/v2/concepts/acceptance-policy).

## 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.interchange.io/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 |
| `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` — when present — is the source's or reviewer's own (sanitized) explanation; it is the first thing to read on a rejection.

### `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 Scope3 support about a stuck buy — it is the reference both sides can look up.

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

### 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 discovery/performance campaign and does not yet support `reach_unit`/`target_frequency`.
</Note>

## Reading and tuning media buys

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.

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

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