Skip to main content

Overview

A Media Buy represents one AdCP transaction with one sales agent. For platform-managed campaigns, Apostra spawns media buys when a Campaign executes. During the tracked-campaign alpha, Apostra can also project one storefront-managed media buy into one read-only tracked campaign. 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, 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.
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.

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:
See Package 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:
  • 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.

Status

A media buy’s status is one of:
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. 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 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.
Polling is most useful right after execution, while you wait on publisher approvals. Once webhooks are flowing, prefer them — see Get media buy status.
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. 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 (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 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:
  • 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 as media_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

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) 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). 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 — conversely, performanceConfig.optimizationGoals supports an explicit target strength, which this level does not yet support.

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

Update budget and optimization goals

Always confirm optimization_goals with the buyer before changing them — silent goal changes break optimization continuity.

Cancel a single package

To cancel one package without touching the rest of the media buy, pass packageIds on the mediaBuys[] entry:
See Package 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.
  • 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 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 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 returns 202 Accepted.
  • When the seller reports the buy paused, Apostra moves it to PAUSED and clears pauseRequestedAt.
  • A campaign pause 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 returns 202 Accepted with resumeRequestedAt.
  • A campaign reactivate 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 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 (or a campaign-level reactivate).
  • 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 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 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[]:
Pass debug: true on execute to include the full ADCP request, response, and A2A debug logs in each error entry.

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