Overview
A Media Buy represents one AdCP transaction with one sales agent. For platform-managed campaigns, Interchange spawns media buys when a Campaign executes. During the directed-campaign alpha, Interchange can also project one storefront-managed media buy into one read-only connected-account campaign. Campaigns managed by Interchange use the canonical buyer product and media-buy lifecycle instead of a separate connected-account write path.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.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:Product format identity
Products embedded in a media-buy read expose their URL-free canonicalformatOptions declarations. For a composed product, these declarations are
the only format identity returned; Interchange 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.
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’sbudget.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:
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 toseller CPM ÷ (1 − fee rate)).nullwhen 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.
Status
A media buy’sstatus is one of:
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.
pending_creatives and pending_start both collapse to PENDING_APPROVAL
here; active → ACTIVE, and so on. See
Media buy lifecycle for the full mapping.Most-restrictive rule
For a multi-agent campaign, the surfaced status is the most restrictive across all agents. For example, if one media buy isACTIVE 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.
Why a storefront held or rejected your buy
APENDING_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.
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 nestedmediaBuys[] in GET /campaigns/:id (camelCase fields), and the media-buy status poll (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
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 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:
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 asmedia_buy.* events, fired once per transition (never per retry):
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).Target kinds
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). For a reach or CPM-style goal, target metric: "reach":
Reach goal — target unique reach at a minimum frequency
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.
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.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.
Update budget and optimization goals
Cancel a single package
To cancel one package without touching the rest of the media buy, passpackageIds on the mediaBuys[] entry:
Override creative attachment
Omittingcreative_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.
- Omit
creative_idsto leave existing assignments unchanged. The platform does not fetch, validate, sync, or forward creative data for the buy in this case. - Explicit
creative_idsreplaces 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_idsis only valid withaction: "update". It is rejected withcancelordelete.
Package flight-date changes
Updating a package’sstartTime 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.
Updating a paused media buy
APAUSED 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
ACTIVEbuy. The materiality classification that decides whether a change needs approval does not special-casePAUSED— 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 fromPAUSEDtoACTIVEis reactivate media buy (or a campaign-level reactivate). - Updates on a paused buy still propagate to the seller. Submitting a
change against a
PAUSEDbuy is not a local-only edit — it queues exactly the same way it would for anACTIVEbuy. See Pending changes: what is live vs what you asked for for how to readpendingChangewhile 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 and reactivate a single 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[]:
debug: true on execute to include the full ADCP request, response, and A2A debug logs in each error entry.
Related concepts
Campaign
The parent media plan that spawns media buys at execution
Package
One per product per pacing period, beneath each media buy
Get media buy status
Poll live ADCP status across sales agents
Update campaign
The endpoint that reads and tunes media buys