AdCP 3.0 Proposal - This specification is under development for AdCP 3.0. Feedback welcome via GitHub Discussions.
Abstract
The Media Buy Protocol defines a standard interface for AI-powered advertising automation. This protocol enables AI agents to discover advertising inventory, create and manage campaigns, synchronize creative assets, and track performance through natural language interactions.Protocol Overview
The Media Buy Protocol provides:- Natural language inventory discovery based on campaign briefs
- Campaign creation with package-level budget and targeting
- Creative asset management and synchronization
- Performance tracking and optimization feedback
- Human-in-the-loop approval workflows
Transport Requirements
Sales agents MUST support at least one of the following transports:
Sales agents SHOULD support MCP as the preferred transport.
Sales agents MUST declare Media Buy Protocol support via
get_adcp_capabilities:
Core Concepts
Request Roles
Every media buy request involves three entities:- Orchestrator: The platform making the API request (e.g., DSP, trading desk)
- Account: The billing relationship—who gets billed and what rates apply (identified by
account_id) - Agent: The entity placing the buy (identified by authentication token)
Sales Agent Types
Publisher Sales Agents — represent a single publisher’s inventory:- Sales agents MUST only return products for inventory they are authorized to sell
- Sales agents MUST validate authorization via
adagents.jsonwhen applicable
- Sales agents MUST clearly identify the source publisher for each product
- Sales agents MUST NOT misrepresent inventory provenance
Identifiers
Useplatform_id for an object’s ID in the seller’s ad platform, consistently across sync_creatives, sync_catalogs, sync_event_sources, and sync_audiences results. Reserve seller_id for the seller’s identity, such as the IAB sellers.json ID in adagents.json contact.seller_id and publisher identifiers of type seller_id.
The existing seller_id fields on event source and audience results are deprecated aliases for platform_id starting in AdCP 3.3. When reporting a platform identifier in 3.3 and later 3.x responses, sellers MUST emit both names with identical values. Buyers MUST prefer platform_id and fall back to seller_id only when platform_id is absent. These aliases retain their object-ID semantics and MUST NOT be repurposed as seller identity. They remain functional throughout 3.x, with removal no earlier than 4.0 under the deprecation policy.
product_id: Opaque identifier for a product returned byget_products. A non-custom wholesale ID remains stable for the same logical offer within the seller and declaredcache_scope, including across reads and wholesale-feed webhooks; feed and pricing versions communicate temporal catalog mutation, while retirement or replacement may end the identity. Binding commercial terms are immutable underpricing_option_id, so changed price, floor, currency, model, or priced applicability receives a new option ID. Anis_custom: trueID remains stable only in its issuing discovery/refinement lineage through its promised lifetime. Creating a package with the product and pricing-option IDs accepts the disclosed configuration and commercial option. A caller-authorized custom ID pastexpires_atreturnsPRODUCT_EXPIRED; after its tombstone is no longer retained or outside its account/lineage,PRODUCT_NOT_FOUNDapplies without disclosing cross-tenant existence.
Structured requirements and briefs
Buyers SHOULD use a structured request field whenever one represents their requirement. Structured input is compact, machine-validatable, and avoids lossy natural-language inference. Briefs carry goals, context, semantic intent, preferences, and requirements without a structured representation. An explicit hard requirement in a brief remains binding. Sellers MUST apply it even when the buyer did not duplicate it in a structured field. When structured hard targeting inferred from prose materially affects product eligibility, pricing, or forecasting, sellers MUST confirm the resulting predicate once inrequest_proposals or get_products response-root
targeting_resolution.brief_targeting, or in the corresponding
refine_proposals.results[] entry when the interpretation came from that
refinement’s ask. Otherwise confirmation remains a best practice. Absence of
Product targeting resolution confirms exact acceptance of a structured overlay
for that product only; it does not confirm how the seller interpreted prose.
required_overlay_support requests later-selectable capability, not concrete
values, value-specific availability, or a forecast for every possible value. A
requirement true matches support true or any valid support object; an object
requirement matches true or a support object containing every requested
boolean and array subset. Unrequested fields and numeric seller limits do not
participate. Forecasts without concrete values describe the product’s
discovery/default scope; buyers rediscover with concrete targeting_overlay
values for value-specific forecasts. Fixed prices and floors remain binding
uniformly for supported selections, while price guidance remains non-binding.
Sellers with value-dependent rates return a concretely targeted configured
product, split rate tiers into products, or withhold binding prices until
concrete rediscovery; they never reuse a selected pricing_option_id at new
terms.
Placement selection follows the same exact-outcome rule across discovery,
create, and update. A selected set exactly equal to the product’s complete
mode: "included" set is an inherent restatement and does not require
overlay_support.placement_selection; partial selection does.
Frequency-cap scope and product support
Pick the field by what you are doing:
Scope is determined by field location. A cap nested in a package
targeting_overlay owns an independent counter for that package. A root
frequency_cap on a MediaBuy request, accepted commercial terms, or MediaBuy
state owns one counter shared across every package. FrequencyCap therefore
defines no scope discriminator; unknown extension fields remain accepted but
cannot change the scope established by location. When both levels are present,
an impression is eligible only when both caps permit it. Repeating the same
package cap on every package does not create a shared counter.
Package caps
The legacy package capability remainsProduct.overlay_support.frequency_cap: true, with its existing broad meaning
within seller-wide limits. A product that is constrained or create-only uses
the separate frequency_cap_support field instead:
frequency_cap_support object
or the broad legacy declaration, so buyers send frequency_cap_support alone;
a requirement carrying both is likewise rejected, because frequency_cap: true
alone would exclude every constrained product. Legacy frequency_cap: true
promises the broad contract, including changes through update_frequency_caps;
a seller that cannot change a product’s cap after creation switches that
product to frequency_cap_support with mutable_fields: [].
Each count or duration constraint is either an exhaustive preset list or an
inclusive range, never both, so a unit-only declaration never implies support
for every interval or count. A field omitted from frequency_cap_support
inherits the seller-wide media_buy.frequency_capping declaration and never
broadens it: an omitted supported_per_units inherits the seller-wide per
units, and omitted window_constraints or suppression_constraints inherit
the seller-wide supported_window_units with no interval restriction, so a
product that needs interval limits must declare them. A product value absent
from the seller-wide declaration is invalid. A weekly window is
{ "interval": 7, "unit": "days" }; there is no weeks unit. mutable_fields lists the cap fields that can change after
creation: [] means create-only, ["max_impressions"] permits a count change
while keeping per and window fixed, and omission leaves update support
undeclared. A product that omits mutable_fields, like one declaring legacy
frequency_cap: true, matches any mutable_fields requirement because the
broad legacy meaning applies; [] never matches a non-empty requirement.
Product capability describes implementation support, and a product with a
non-empty mutable_fields SHOULD also list update_frequency_caps in its
allowed_actions template; MediaBuy.available_actions remains authoritative
for whether a particular buy can change now.
Discovery matching uses subset containment after inheritance. A requested
window or suppression unit matches when the product’s resolved unit list
contains it: the product’s own window_constraints or
suppression_constraints when present, otherwise the seller-wide
supported_window_units (omitted means every unit).
update_frequency_caps authorizes package-cap changes. For heterogeneous
packages, the action carries applicable_package_ids; omission means every
package. A missing action or a package outside that list returns
ACTION_NOT_ALLOWED; a cap shape outside product capability, including a
change to a field not listed in mutable_fields, returns
UNSUPPORTED_FEATURE; a change outside accepted commercial terms returns
REQUOTE_REQUIRED. These checks run in that order: available_actions
authorization first, then product capability, then accepted commercial terms,
so a request that fails more than one returns the earliest code. Failure
preserves state.
Shared MediaBuy caps
Aggregate support is advertised separately asmedia_buy.aggregate_frequency_capping; the legacy media_buy.frequency_capping
object continues to mean package support. Participation is product-scoped as
Product.media_buy_support.frequency_cap: true, optionally narrowed by
frequency_cap_constraints. Every selected product must advertise
participation and must support the root cap’s per value; a product mix that
does not is rejected atomically. Discovery uses required_media_buy_support
when the value is not yet known, and media_buy_frequency_cap when the exact
aggregate value must be executable; the exact value alone suffices because it
implies participation. In 3.2, root caps are max-impression caps only: duration
windows are rolling, campaign spans the MediaBuy flight, and the units are
the same as package caps (a weekly window is seven days). Suppression remains
package-only because its legacy value does not identify a counter entity.
A root cap outside the seller-wide capability or any selected product’s
resolved constraints, or a product mix that cannot share it, is rejected with
UNSUPPORTED_FEATURE before any mutation; sellers MUST NOT silently drop,
soften, or clamp it. A create, update, or buy response that does not echo the
root cap means the seller did not apply it; buyers MUST treat the buy as
uncapped and MUST NOT assume the cap is enforced. media_buy_support.frequency_cap_constraints.mutable_fields
declares whether a product’s implementation can change the root cap after
creation, and a product declaring a non-empty list SHOULD also list
update_media_buy_frequency_cap in its allowed_actions template;
MediaBuy.available_actions decides whether a live buy can change now. A
root-cap change inside accepted commercial terms uses control_media_buy and
applies immediately; a change the accepted terms do not permit returns
REQUOTE_REQUIRED and goes through proposal refinement. The same order
applies to the root cap: a change on a buy whose available_actions omits
update_media_buy_frequency_cap returns ACTION_NOT_ALLOWED before any
capability check, a cap the seller-wide domain or a selected product cannot
execute returns UNSUPPORTED_FEATURE before any commercial-terms check, and
only then does a change outside accepted terms return REQUOTE_REQUIRED.
frequency_cap_constraints.mutable_fields governs which fields of an existing
root cap may change; clearing the cap, or setting one on an uncapped buy, is
governed by the action alone.
Cross-MediaBuy or cross-order counters require a separately identified grouping
resource; they are not another value of FrequencyCap.scope.
Changing a cap never resets its counter. Prior qualifying exposures in the
resulting active window continue to count. Adding or removing a package also
does not reset or subtract aggregate history. A package addition or proposal
revision whose resulting product mix cannot enforce the root cap is rejected
atomically; the check applies to the state the request would produce, so an
update that clears the root cap and adds packages in one request adds them
uncapped. Proposal refinement uses remove_media_buy_frequency_cap: true to
clear a cap because omission inherits it. Successful creates, updates, and reads
echo root and package caps at their separate locations.
Product execution requirements (experimental)
Many products need account resources that live outside the media buy: a conversion product needs a pixel or dataset, an app-promotion product needs a registered app, a catalog-sales product needs a product catalog, and a product that runs as a page or profile needs that identity connected.Product.execution_requirements lists these needs so a buyer knows, before
create_media_buy, what it must supply and where it goes on the request. A
seller using this experimental surface lists media_buy.execution_requirements
in experimental_features on get_adcp_capabilities.
Each entry’s kind determines where the resource is bound:
- All entries are required. Alternatives inside one entry (
event_types,catalog_types) are any-of. - Account-independent. The declaration says what the product needs, not
whether the caller’s account has it. It does not change
cache_scope, so a public wholesale catalog stays cacheable, and it MUST NOT carry account-specific resource IDs or names. Connection entries carry noresource_ref,connection_id, orexpires_at, nostatusother thanunknown, and arequired_forthat includescreate_media_buywhen present. - Complete when present. Together with the
required_connectionsof the format declarations a package selects, the list is complete for this vocabulary: a seller MUST NOT reject a package that satisfies all of them for lacking a resource of an undeclared kind. A connection every package needs, including the advertiser account itself, belongs inexecution_requirements; a connection only some formats need (such as a post authorization for published-post formats) stays on the format declaration. Absence of the field means undeclared, not “needs nothing”. - Declare only what execution needs. Declare
event_sourceonly when the product requires event optimization. A pixel wanted only for attribution or reporting is not an execution requirement in 3.2: declaring it would push buyers into an event goal that changes the bidding objective. Closed-loop retail sellers withplatform_managedmeasurement likewise declareevent_sourceonly when the package must bind the seller-managed source. - Coherent with product capabilities. On the full
Product, anevent_sourcerequirement requiresconversion_tracking. On both the full and the canonical product, acatalogrequirement requirescatalog_types, and every type it lists MUST appear there. - App promotion. Use
catalog_types: ["app"]; app items carryapp_id,bundle_id,apple_id, andstore_url. Platforms typically promote one app on one store per package, so a seller MAY require the app catalog to resolve to a single item (select it withids) and treat a multi-item catalog asineligible. Anapp_installor other app event source that is not associated with the bound app is alsoineligible. - Rejection. When an
event_sourceorcatalogentry is unmet on a buyer-suppliedcreate_media_buypackages[]entry orupdate_media_buynew_packages[]entry, or when an update removes or replaces a package’s satisfying binding, a seller that declares the requirement MUST returnVALIDATION_ERRORwitherror.fieldnaming the first unmet binding (for examplepackages[0].optimization_goalson create, ornew_packages[0].catalogson update) anderror.detailsconforming toexecution-requirement-unmet.json.reasondistinguishesnot_bound(nothing referenced),not_found(the referenced resource is not available to the account), andineligible(the resource exists but does not satisfy the requirement). Fornot_foundandineligible, sellers SHOULD pointfieldat the offending entry, such aspackages[0].optimization_goals[1].event_sources[0]. A seller that also emitsissues[]SHOULD emit one entry per unmet requirement in the same order. Unrelated updates to an existing package are not re-checked. An unmetdownstream_connectionreturnsAUTHORIZATION_REQUIREDwitherror.details.missing_connections[]. - Satisfy everything before calling. A package can miss several
requirements across both error codes, and a seller may report either first.
Buyers SHOULD check every declared entry, and every selected format’s
required_connections, before calling rather than fixing one error per retry. - Proposal execution. Packages a seller derives from
create_media_buy.proposal_idare not buyer-supplied, so the rejection rule above does not apply to them in 3.2. How a proposal carries these bindings is tracked with per-account readiness for 3.3.
Product identity absence and reach reporting (experimental)
Product.identity is an optional, product-scoped declaration for inventory
whose delivery does not have a persistent per-entity identifier. It is not a
seller-wide capability: mixed inventory may legitimately return one product
with persistent_identifier: false and another without that constraint. A
seller using this experimental surface lists media_buy.product_identity in
experimental_features on get_adcp_capabilities; its absence is
undeclared, not proof of identifier availability.
When persistent_identifier is false, the product MUST NOT declare
overlay_support.frequency_cap, overlay_support.frequency_cap_support, or
media_buy_support.frequency_cap. A seller that cannot enforce a requested cap
rejects it under the exact-apply-or-reject rule; it never accepts and ignores it.
Delivery for such a product MUST NOT report reach_unit as devices,
accounts, or cookies. individuals and households remain valid for
panel-based or place-based modeled measurement. For an identity-absence
product, a delivery row that reports frequency MUST also report
reach_unit, even when that row omits reach. If either reach or
frequency is reported with reach_unit: "custom", the discovered product
MUST include a non-empty identity.reach_methodology describing that unit.
This product-to-delivery conformance invariant applies only when the product
has made the experimental identity-absence declaration; it does not tighten
the stable shared delivery-metrics payload for legacy products.
A package row, and a breakdown row attributed to that package, is bound to its
selected product for this invariant. A media-buy totals row is bound only when
every contributing package selected a product with
persistent_identifier: false; mixed-product totals do not make a
product-specific identity claim. Sellers use package or product-bound
breakdowns to report and verify reach or frequency for mixed buys.
-
media_buy_id: Unique identifier for a media buy. Sales agents MUST return this on successful creation. Orchestrators MUST use this for all subsequent operations on the media buy. Amedia_buy_idis a stable handle to any order in the seller’s ad server that the authenticated account owns — not only orders originally booked via AdCP. -
package_id: Unique identifier for a package within a media buy. Sales agents MUST return this for each package created. -
idempotency_key: Client-generated unique key for safe retries. Sales agents that receive a duplicate key for the same account MUST return the original response rather than re-executing.
Account Ownership vs. Creation Surface
AdCP is a protocol onto the seller’s ad operations, not a shadow ledger beside them. Account-scoped tasks (get_media_buys, get_media_buy_delivery, update_media_buy, creative sync where applicable) are scoped by account ownership, not by the surface through which a resource was created. A sales agent MUST NOT partition its own inventory into “AdCP-managed” and “non-AdCP” subsets for the purpose of these tasks.
Concretely:
get_media_buysMUST return every media buy owned by the authenticated account — whether created viacreate_media_buy, via the seller’s native APIs or UI, via manual trafficking, or via legacy/third-party systems — subject only to the declared filters (status_filter,predecessor_media_buy_id, and the like) and pagination.- Any
media_buy_idreturned byget_media_buysMUST be a valid argument toget_media_buy_deliveryand toupdate_media_buyfor every action listed in itsvalid_actions. - A sales agent MUST NOT mark a buy read-only, hide it, or return
MEDIA_BUY_NOT_FOUNDon the basis that it was not originally booked through AdCP. - When a specific action is unavailable for business reasons (contractual obligations, platform constraints, policy), the sales agent expresses that by omitting the action from
valid_actions, not by refusing the buy from account-scoped listings. - Creation surface is never a business reason. Sales agents MUST NOT omit an action from
valid_actions— or returnINVALID_STATEon an otherwise-valid update — solely because the buy was created outside AdCP.valid_actionsomissions are legitimate only when grounded in actual contractual, platform, or policy constraints that would apply equally to an AdCP-created buy in the same state. A seller that returns non-AdCP buys with a systematically emptyvalid_actionsis non-conformant, because that behavior is indistinguishable from hiding the buy and defeats the normative intent of the rules above.
Partitioning belongs at the account boundary
When a seller has a legitimate reason to keep a set of buys outside a caller’s operational reach — child-seller models, NDA-scoped PMP deals, sandbox-vs-production separation, tenant-level privacy partitions — the correct mechanism is account partitioning, not within-account filtering:- Expose the hidden subset as a separate account (or sub-account) the caller is not authorized to reference.
- Within any account the caller is authorized on, return the full set of buys that account owns per the rules above.
get_agent_capabilities is the introspection surface for the scope granted on a given account. Callers can see what they can see; what they cannot see lives behind an account reference they do not hold. Within-account filtering — returning only a subset of an account’s buys to a caller who is authorized on that account — reintroduces the shadow-ledger problem this rule forbids.
Media Buy Lineage
A media buy MAY declare the earlier buy it succeeds — a renewal onto a new flight, a replacement, a migration, or a sunset into a successor — so buyers do not rebuild spend continuity, pacing baselines, and dispute context out of band.history[] records one buy’s own revisions; lineage links two different buys.
- Forward-only. The successor carries
predecessor_media_buy_idandlineage_reason, set once at creation oncreate_media_buy,buy_products, oraccept_proposal(when acceptance creates a new MediaBuy). There is no field on the predecessor and no update action: the fields are not part ofupdate_media_buy, and a seller MUST NOT apply them if sent there. Creating a successor MUST NOT change the predecessor’s AdCP-visible state, includingstatus,revision,updated_at, andhistory[], and MUST NOT add a reverse pointer to it. Canceled and completed buys are never mutated. Creating the successor does not cancel or complete a live predecessor; the buyer does that separately, and this section does not limit how many buys name the same predecessor. - Same seller, same account. The value MUST be the
media_buy_idof an existing buy owned by the same seller and the same account as the request.media_buy_idis seller-namespaced, so cross-seller lineage is not expressible. The predecessor MAY be in any status; it need not be terminal. - Invalid links are rejected. The seller MUST reject the request with
MEDIA_BUY_NOT_FOUNDwhen no such buy is accessible to the caller in the request’s account, and MUST NOT reveal whether the ID exists under another account. It MUST reject withINVALID_REQUESTwhenpredecessor_media_buy_idandlineage_reasonare not supplied together, or when anaccept_proposalrequest carries them for a proposal whoseproposal_kindismedia_buy_updateormedia_buy_cancellation. A rejected request creates no media buy. Because the link is set once and must name an already-existing buy, a cycle cannot form; there is no depth limit and no lineage-specific error code. - No silent drop. A seller that receives a schema-valid link it cannot record MUST reject the request with
UNSUPPORTED_FEATUREand MUST NOT create an unlinked buy. Because these schemas allow additional properties, a seller that predates lineage may ignore the fields; a buyer therefore reads the echo onget_media_buysafter creation and treats its absence as “link not recorded”. - Closed reason.
lineage_reasonisrenewal,terms_change,migration,sunset, orreplacement.replacementis the seller substituting a buy for one it could not fulfill as booked. Reasons outside the enum travel inext; buyers branch only on the enum. - Async and replay. A seller SHOULD validate the link before returning a
submittedenvelope, and the link MUST appear on the completed buy. The fields are part of the request the idempotency key protects: an exact replay returns the stored outcome without re-validating the predecessor, and a changed link under the same key is an idempotency conflict. - Read and reverse lookup.
get_media_buysechoes both fields on every buy created with them and accepts apredecessor_media_buy_idfilter that returns the direct successors of a buy in every status. Chains are walked one hop at a time, and the predecessor of a returned buy MAY no longer be retrievable. - No commercial effect. Lineage is a record, not a contract: it carries no pricing, budget, delivery, or creative inheritance, the seller applies none of the predecessor’s terms to the successor, and the fields are outside any proposal terms digest.
media_buy.lineage). A seller that implements lineage MUST list media_buy.lineage in experimental_features on get_adcp_capabilities, and buyers send the fields only to sellers that do; a seller that does not list it MUST NOT record them, and rejects them with UNSUPPORTED_FEATURE as above. See Experimental Status.
Buy-level lineage is distinct from spot-level replaces_spot_id, which links airings inside a delivery report (media_buy.spot_lifecycle). Seller-initiated replacement offers are a separate flow; lineage is the persistent link recorded once a replacement buy exists, whatever initiated it. Decision record: DR-0030.
Asynchronous Operations
The Media Buy Protocol is asynchronous by design. Operations MAY return immediately or MAY require extended processing:- Synchronous responses: Sales agents MAY return completed results immediately
- Asynchronous responses: Sales agents MAY return
status: "submitted"orstatus: "working"with a task reference - Human-in-the-loop: Sales agents MAY require internal human review (e.g. IO signing) by keeping the task in
status: "submitted"until the reviewer acts. Sales agents MAY also usestatus: "input-required"when the buyer must respond (e.g. confirm a budget). Human approval is modelled at the task layer — there is nopending_approvalMediaBuy status (that value exists only on Account.status for account onboarding review) - Rejection: Sales agents MAY reject a media buy in
pending_creativesorpending_startstatus when platform setup reveals the order cannot be fulfilled (e.g., inventory sold out, policy issue discovered during ad server setup). Orchestrators MUST treatrejectedas a terminal state. If the seller does not want to accept the order at create time, it SHOULD failcreate_media_buywith an error rather than creating a media buy inrejectedstatus.
Transaction boundaries across lifecycle surfaces
create_media_buy and update_media_buy are compound 3.x compatibility
facades. Each request is one atomic mutation. Sellers MUST validate every
requested package, inline creative, creative assignment, catalog selection, and
cross-field relationship before committing any part of the request. If one
inline creative or assignment is invalid, the complete create or update fails;
the seller MUST NOT leave behind a MediaBuy, package, library write, assignment,
or partial update from that request.
Request validation is not creative review. After valid creative input has been
committed, a later content-policy, brand-safety, or accessibility rejection
updates creative and assignment approval state but does not roll back the
MediaBuy or a successful update. Before activation, a buy without sufficient
approved coverage remains pending_creatives until the buyer supplies a
correction or replacement. Rejection affecting an active buy follows the
creative-impairment and serving rules instead of reversing commitment.
The compact lifecycle separates commitment, operational control, and creative
traffic. buy_products or accept_proposal commits before sync_creatives can
assign against the returned package IDs. Each call is atomic, but the sequence
is a saga: failure of a later sync leaves the purchase committed. Likewise, a
legacy update split into proposal acceptance, control_media_buy, and
sync_creatives has multiple independent failure boundaries and is not an
atomic compatibility translation. Compensating cancellation, when allowed by
the accepted terms, is a new state transition rather than rollback.
Callers use the compatibility facade whenever one transaction must combine
targeting with assignment replacement, or when an inline-only seller has no
creative library. A targeting change that would orphan an assignment MUST be
validated with its complete assignment replacement in the same facade update;
control_media_buy followed by sync_creatives cannot provide that guarantee.
Definition pins
terms_digest covers commercial_terms, but commercial_terms names some
definitions that a third party publishes and can change after the proposal is
committed: a publisher’s format option, a placement, a collection, a data
provider’s signal. A pin records what the seller resolved when it authored the
terms, so a buyer or seller can detect that a referenced definition changed even
though terms_digest still verifies.
Pins are commercial_terms.definition_pins[] entries
(definition-pin.json).
Each carries ref_kind, a closed canonical reference identity object, a
lowercase-hex SHA-256 content_digest, and an optional informational
source_version.
Covered set
A covered reference is one of these, found incommercial_terms.purchases[]:
These are not pinned: property lists and collection lists (buyer-managed and
designed to change during a flight, with
list_applications receipts as the
evaluation record), product-local format options, seller-inline placements,
product-scoped and signal-source signals (a seller would only be hashing and
checking its own data), and default-mode selections (mode: default). A pin
covers the entry as published, not the entries it names by ID: a placement pin
does not pin the catalog format options that placement lists, which are pinned
only when a purchase references them itself.
Authoring and verification
- A seller MUST emit one pin for every distinct covered reference in
purchases[]on everynew_media_buyandmedia_buy_updatedraft and revision, and MUST NOT emit such terms without the pins. Entries are unique by(ref_kind, reference), withreferencecompared by its JCS bytes, and ordered byref_kind, then by the JCS bytes ofreference, both compared as UTF-8 bytes, so equal terms always produce equalterms_digestvalues. An amendment is a new commitment over the whole resulting envelope, so its pins are issued at the current definitions; the prior accepted snapshot is immutable and keeps its own pins. Amedia_buy_cancellationproposal commits no new reference and carries no pins. Sellers emit pins, anddefinition_changed, only to callers negotiating AdCP 3.3 or later. - Only sellers author pins. The
request_proposals,refine_proposals,accept_proposal, andbuy_productsinputs rejectdefinition_pinswithINVALID_REQUEST;check_governanceis exempt because it carries the seller-authored proposal. A directbuy_productscreates the seller-authored pins and the accepted snapshot atomically. - A revision re-resolves every covered definition and issues fresh pins. A
committed proposal is term-preserving, so
refine_proposalsfinalizere-resolves each existing pin and verifies every pin in the batch before taking any hold. If any pin fails, no hold is taken and the entry isoutcome: unablewithreason_code: definition_changedand a requireddrifted_pins[]naming each failing pin and the digest the definition resolves to now; its owndefinition_changedtakes precedence overbatch_aborted, which the otherwise-eligible siblings return. Onlycommercial_termsis frozen;forecastmay differ between a draft and its committed successor. accept_proposalre-verifies every pin before mutating any state, after the cheaper proposal ID, digest, expiry, and authorization checks. A failing pin returnsREFERENCE_DEFINITION_CHANGED, one error per drifted pin, witherror.fieldnaming the pin (for examplecommercial_terms.definition_pins[2]) anddetails.current_content_digest. An idempotent replay of an acceptance that already completed returns the stored result without re-verifying pins. The seller fetches and compares outside the commit, performs no network I/O while holding a lock, and commits only if the proposal state is still the one it verified.finalizelikewise runs its authorization and expiry checks before any fetch.- Resolution uses a copy of the publisher’s document no older than its own HTTP
cache validity and in any case no older than 300 seconds: a verifier
revalidates an older copy with a conditional request, and the
serve-stale-on-error fallback does not apply to pin verification. A verifier
fetches each distinct domain once per verification under an aggregate
deadline and follows the
adagents.jsonlocation,authoritative_location, redirect, and size rules unchanged. A document that was fetched and validated but no longer contains the entry means the definition was withdrawn: that is drift, withcurrent_content_digestomitted. A document that cannot be fetched or parsed is transient, not drift: the seller returnsSERVICE_UNAVAILABLEat task level without taking a hold or mutating state, with an error message that does not expose the fetch target, resolver detail, or internal addresses. Duplicate IDs in the source array make the definition unresolvable. Document-level context outside the entry (properties[],authorized_agents, tag maps) is outside the pin. - Accepted terms retain their pins as the historical record. Post-acceptance drift and remedy policy, retention of pinned content, and signal-source signals are not specified in this version.
- Private-list credentials, including
auth_token, never appear in a pin. Keeping them out of accepted commercial terms is specified separately from this version’s pins. A readback of accepted terms shows pins as the record of what was resolved at commitment and makes no ongoing integrity claim.check_governancecarries a buyer-submitted proposal, so a governance agent treats its pins as unverified claims.
Definition pin preimage
The preimage rules cannot change without making existing pins unverifiable, so the normative form is data:test-vectors/definition-pins/vectors.json
projection_rules. The algorithm is:
- Take the published entry exactly as parsed from the publisher’s
adagents.json, resolved from the same source the seller used for the reference. Apply no schema defaults; an absent member and anullmember are different. Every own member is hashed, including one literally named__proto__. An entry that is not I-JSON (RFC 7493), such as one with a lone surrogate, a duplicate member name, a number that is not finite or not exactly representable as an IEEE 754 double, nesting deeper than 64 levels, or an entry or nested element that is not an object, has no JCS form; a verifier treats that definition as unresolvable, which is drift. Pins are computed only from public, unauthenticated sources. - Remove the top-level members named in the kind’s exclusion list below. Exclusion applies only to top-level members, and to top-level members of each element of a nested array. Deeper members stay verbatim.
- For each named set array present, de-duplicate its elements by JCS bytes and sort them ascending by the UTF-8 bytes of their JCS serialization. A nested array is a set array whose elements are projected first, either with another kind’s rules or with element rules given inline for that array (an exclusion list and named set arrays of the element’s own).
- Serialize with RFC 8785 (JCS), compute SHA-256 over the UTF-8 bytes, and encode as lowercase hex with no algorithm prefix.
The exclusions are display text, presentation pointers, collection-card media
(
images, sample_content, and the collection page url), extension data,
freshness timestamps, cast, tags, and the origin-dependent publisher_domain.
Only a top-level ext is excluded (and the top-level ext of each element
projected with another kind’s rules); an ext deeper in the entry, such as
inside params or a distribution entry, stays verbatim.
Excluding tags is deliberate: authorization scope is verified through supply
path, not through pins. Everything else, including members the AdCP schemas do
not name, is part of the preimage. A rename or a new card thumbnail is not drift; a change to
content_rating, creative params, a collection’s status, or a signal’s
segmentation_criteria is. A refreshed platform-reported follower count
(distribution[].followers) is not drift; a change to any other member of a
distribution entry, such as an identifier, is. Publishers keep non-material
metadata in ext.
Arrays that are not named set arrays keep their published order. The table is
frozen for each ref_kind; changing a rule requires a new ref_kind value,
because the pin carries no projection version. The pointer members
presentation_ref, preview_provider, reference_renderer, and criteria_url
are excluded because they name where to find presentation or disclosure
material, not what is being bought; a verified pin does not vouch for what those
pointers serve, so consumers apply the SSRF profile when fetching them. The digest does not bind
ref_kind or reference: the pin’s own fields and terms_digest bind identity,
and a digest is only ever compared with the digest of the same pin.
Re-resolving a pin is a fetch of the publisher’s adagents.json and follows the
same safeguards as every other counterparty-controlled fetch (HTTPS, reserved
address rejection, bounded redirects, no credentials; see the
SSRF profile).
The pin’s domains are seller-chosen, so a buyer verifying pins applies those
rules rather than trusting the host named in a pin. source_version is never
dereferenced.
The vectors include a collection-mutation case: the pinned collection’s
content_rating changes after commitment, terms_digest still verifies, and
the pin fails.
Media Buy State Transitions
Media buys progress through a defined set of states. Terminal states (completed, rejected, canceled) allow no further transitions.
- Sales agents MAY return
active,paused,pending_creatives, orpending_startfromcreate_media_buy(seller’s choice based on platform setup time and the buyer’s create-timepausedrequest) - Sales agents MUST transition committed media buys (
confirmed_atnon-null) frompending_starttoactivewhen the flight date arrives. A provisional buy (confirmed_at: null) never auto-activates; see Provisional buys. Sales agents SHOULD notify orchestrators via webhook when this transition occurs. - A successful committed
create_media_buyresponse constitutes order confirmation. Sales agents MUST includeconfirmed_atin create/get responses; the value is the seller commitment timestamp, ornullfor a provisional buy that exists but is still waiting for seller commitment. - Sales agents MUST include
revisionin create, get, and update responses. The revision number MUST advance on every committed change to the stateget_media_buysreports, whatever the change’s origin; see Revision and confirmation semantics. active↔pausedtransitions useupdate_media_buywithpaused: trueorpaused: falsecreate_media_buywith top-levelpaused: truecreates a held media buy when it would otherwise beactive. Setup blockers still take precedence: no creatives yieldspending_creatives, and a future flight yieldspending_startuntil the start condition is satisfied. The hold is latent while those blockers are visible; once creatives are present and the flight can start, the buy enterspausedinstead ofactive.update_media_buywithpaused: falseresumes a buy that is currentlypausedor clears a latent create-time hold while the visible state is stillpending_creativesorpending_start. Clearing a latent hold does not bypass setup blockers; the visible status remains pending until creatives are supplied and the flight can start.activeorpaused→completedwhen the flight ends, goal is met, or budget is exhausted (seller-initiated)- Buyer-initiated cancellation uses
update_media_buywithcanceled: trueand optionalcancellation_reason - On the
update_media_buycompatibility facade, rootcanceled: truetakes precedence over package cancellation and every other requested mutation. The seller cancels the whole MediaBuy, releases all of its package assignments, ignores every other field exceptcancellation_reason, and SHOULD return a structured warning identifying the ignored fields. A package-only cancellation is evaluated only when rootcanceledis absent. Compactcontrol_media_buyinstead makes root cancellation mutually exclusive with package and other controls, so a mixed compact request is invalid rather than precedence-resolved. - Seller-initiated cancellation (e.g., policy violation, inventory withdrawal) transitions the media buy to
canceledwithcancellation.canceled_by: "seller"; buyers recover the state throughget_media_buys. A durable resource-scoped status webhook remains reserved for the 4.0 notification model—operation-scopedpush_notification_configis not a future lifecycle subscription. A 3.2 seller advertisingaccount.change_feedwithmedia_buycoverage instead emits the generic account-anchoredaccount.change_recordedinvalidation, which the buyer repairs throughget_media_buys; it is not a resource-status payload. - Seller-initiated rejection (from
pending_creativesorpending_start) is likewise observable throughget_media_buys; implementations MUST NOT synthesize an operation-completion webhook after the originating operation has already completed. - Sales agents MUST include a
cancellationobject withcanceled_atandcanceled_bywhen transitioning a media buy or package tocanceled - Sales agents MAY reject buyer cancellation of a non-terminal media buy with error code
NOT_CANCELLABLE(e.g., when the seller contractually refuses mid-flight cancellation) - When a buyer attempts to cancel a media buy already in
canceled(canceled: trueon acanceledbuy), sales agents MUST reject withNOT_CANCELLABLE - All other updates to media buys in terminal states (
completed,rejected,canceled) — includingcanceled: trueattempts againstcompletedorrejectedbuys — MUST be rejected withINVALID_STATE - Rejection (
rejectedstatus) is only valid frompending_creativesorpending_start. Sales agents MUST NOT reject media buys that have already transitioned toactive. - The base 3.x media-buy wire does not define a durable seller-initiated resource-status subscription. Sellers MUST preserve the transition in MediaBuy readback and history. In 3.2, a seller MAY provide immediate generic invalidation by advertising the experimental
account.change_feedcapability (and feature id) withmedia_buycoverage and accepting anaccount.change_recordedaccount subscriber; otherwise buyers needing immediate push delivery require an implementation extension until the 4.0 resource-scoped notification contract lands. - The
canceledfield on update requests uses"const": true— onlytrueis valid. Sendingcanceled: falsefails schema validation. Cancellation is irreversible; there is no “uncancel” operation. - Creative assignments are released on buy rejection or cancellation. When a media buy transitions to
rejectedorcanceled, all package-creative assignments on that media buy are released. For sellers that advertisecreative.has_creative_library: true, the creatives persist in the creative library per assignment state and creative state and MAY be referenced bycreative_idin a subsequentcreate_media_buyorsync_creativescall. Inline-only sellers that advertiseinline_creative_managementwithout a creative library MAY keep submitted creatives package-scoped; they do not advertise cross-buy reuse orlist_creativesreadback. - Creative review is independent of buy outcome. Sales agents MUST NOT implicitly reject a creative because its containing buy was rejected; a creative rejection MUST be a deliberate review decision with its own
rejection_reason. If the buy was rejected because a creative violated content policy, the sales agent MAY reject that creative — but only via the normal review path with its ownrejection_reason; the buy’srejectedstatus is not itself sufficient. - Observability of released assignments. The media-buy-level
canceledorrejectedtransition, preserved inget_media_buysstatus and history, IS the audit record for all its released assignments; buyers MUST NOT rely on per-assignment diff observability. A negotiated implementation webhook extension may accelerate discovery but is not required by 3.x. Released assignments no longer appear inget_media_buysresponses for that buy. Buyers confirming library reusability SHOULD calllist_creativesto verify the creative is still in the library and observe its current status. Inline-only buyers should retain their submitted creative bodies becauseget_media_buyssurfaces package-level approval state, not full creative-body retrieval. Package-scoped deadlines (creative_deadline) on the prior package have no bearing on a library creative’s eligibility for a new buy. - Retention. Sales agents with creative libraries SHOULD retain released creatives in the library for at least 90 days after the last assignment is released. A normative retention floor is specified in the creative retention contract tracked under #2260; until that contract lands, buyers relying on long-horizon reuse SHOULD verify persistence via
list_creativesbefore referencing a released creative on a new buy.
Revision and confirmation semantics
revision is the optimistic concurrency token for a media buy. The wire field on update_media_buy is named revision; implementations may call it expected_revision internally, but the semantics are the same: the caller is saying “apply this update only if the current stored media-buy revision still equals this value.”
Sellers MUST enforce the revision check atomically at the persistence boundary for every mutating update. A read-compare-write sequence in application memory can race with another writer and allow lost updates. The update and compare belong in one database transaction, conditional update, or equivalent atomic primitive. On mismatch, return CONFLICT and leave the media buy unchanged.
Any-origin rule. For every resource that carries a seller-maintained revision, the seller MUST advance it on every committed change to state that resource’s authoritative read reports, whatever the origin of the change:
- an AdCP task, from any authorized principal;
- a seller operator, for example an edit in the seller’s own console or ad server;
- seller automation, for example a policy action, an expiry or an approval decision; or
- a connected platform, whose change the seller observes and ingests.
source_coverage[].stale_after_seconds on list_account_changes). Reads, validation-only calls and exact idempotency replays do not advance it, and neither does an ingestion pass that observes no change in reported state. A platform report that matches state the seller already committed, such as the ad server echoing an update the seller just pushed, is not a change; if it were counted, the revision update_media_buy returns would be stale on arrival. A seller MAY coalesce the changes it commits in one atomic ingestion transaction into a single revision step. The fence is only as fresh as the seller’s ingestion, so a CONFLICT is not proof that nothing else changed. A seller that does not ingest external changes is unaffected until it does.
The rule covers the media-buy revision, the account revision and the creative seller_revision. For the media buy it applies to configuration and lifecycle state: status, flight dates, budget and caps, pacing, bidding, targeting, packages and their pause and cancellation state, creative assignments and per-package creative approval, invoice recipient and reporting-webhook configuration. It does not apply to delivery measurements (snapshot, get_media_buy_delivery), indicators, health, impairments or webhook_activity, which are derived observation rather than configuration, lifecycle or decision state, and carry their own as_of, observed_at or freshness fields. Refreshing status_as_of or updated_at advances nothing by itself. A committed change to configuration or lifecycle state advances the revision even when a measurement changes with it.
A seller that returns history appends an entry for each such change. For a change that did not arrive through an AdCP task, the entry MUST carry origin with the same kind vocabulary as account.change.origin (seller_operator, seller_system, connected_platform or unknown). When the seller cannot classify the origin, or the caller is not authorized to see it, it uses unknown; it still advances the revision and appends the entry.
Mutating updates therefore increment revision and return the new value. This includes budget, flight date, targeting, status, package, creative-assignment, inline-creative, reporting-webhook, invoice-recipient, and committed-metric changes made through update_media_buy, and the same changes made by any other origin.
Creative assignment changes made through sync_creatives (assignment_operations, or the deprecated assignments shorthand) are committed changes to package state and advance the MediaBuy revision of every affected media buy, once per buy per committed request, in the same atomic write as that buy’s assignment change (one transaction per buy; no cross-buy atomicity is required). A no-op assignment (already assigned, an unassign of nothing, an identical replace) advances nothing. A seller that applies assignments asynchronously advances it when the change commits. sync_creatives still has its own idempotency and atomicity boundary and does not accept a media-buy revision precondition; it advances the revision but cannot be fenced by it. A seller review decision that changes a package’s creative approval (creative_approvals) is seller automation or a seller operator change and advances the revision likewise. A library-only upload or edit that no package reports does not advance the MediaBuy revision; it advances the creative’s seller_revision.
Seller-optimized allocation follows the same rule. A change to any allocation, cap, target, pacing or lifecycle value that get_media_buys reports advances the revision and appends a history entry with origin.kind: "seller_system". Spend distribution between packages inside the accepted caps and goals is delivery behavior: no media-buy read reports it as state, so it advances nothing and is observable only through delivery measurement. This restates the 3.2 position more precisely rather than tightening it; if a later release adds a current per-package allocation to get_media_buys, changes to it advance the revision.
Buyers that send a revision they have not refreshed will see more CONFLICT than under 3.2. Buyers that omit revision are not fenced, as before. The extra CONFLICT is by design: those updates would otherwise overwrite a change the seller or a connected platform made.
Buyers SHOULD pass the latest observed revision on every update_media_buy call intended to change state. The request field remains optional for backward compatibility. When revision is present, sellers MUST compare it atomically with the write and reject stale values with CONFLICT. On CONFLICT, reload the media buy with get_media_buys, reconcile against the current state, then retry with the new revision and a fresh idempotency_key. Do not retry with the same stale revision.
When a MediaBuy has accepted_proposal state and a successful
update_media_buy compatibility request changes the commercial envelope, the
seller MUST atomically materialize an accepted successor proposal snapshot and
advance accepted_proposal_id, accepted_proposal_terms_digest, and
accepted_proposal with the MediaBuy update. The successor records the prior
accepted proposal as its parent and the pre-update MediaBuy revision as its
base. This keeps a later compact amendment anchored to the terms that are
actually in force. An in-envelope operational change or creative-only change
does not create a commercial successor. A seller that cannot update the
MediaBuy and accepted snapshot together MUST reject the compound facade update;
it MUST NOT leave a stale accepted-proposal pointer.
confirmed_at is the seller’s commitment timestamp, not a delivery-status timestamp. Once populated, it remains stable through later pause, resume, cancellation, completion, and reporting transitions. Use the submitted response branch when no media_buy_id is being returned to the buyer. Sellers MAY instead return synchronous success with media_buy_id, packages, and confirmed_at: null for a provisional buy; such buys MUST be retrievable via get_media_buys and MUST transition by setting confirmed_at exactly once on commitment. A provisional buy with confirmed_at: null MUST NOT be active and MUST NOT include packages[].committed_metrics.
Provisional buys (seller review hold)
A seller that reviews a non-guaranteed buy before committing to it MAY hold the buy by returning synchronous success with amedia_buy_id and confirmed_at: null, as described above. This is the hold path for sales-fixed-rate; guaranteed sellers that need an IO signature use the submitted task arm instead. While confirmed_at is null:
- Two independent axes.
status(media_buy_statusoncreate_media_buy,statusonget_media_buys) is the buyer-actionable readiness axis.confirmed_atis the seller-commitment axis. Commitment is keyed offconfirmed_at, never offstatus. A provisional buy reports the status it will have on confirmation:pending_creativeswhile creatives are missing or unapproved,pending_startonce the buy is otherwise ready. Neither value means the seller has accepted the buy, andactiveis not permitted. - Confirmation is a state change. When the seller commits, it sets
confirmed_atexactly once and, as for every mutating state change, incrementsrevision. Thestatusafter confirmation follows the ordinary lifecycle for the buy’s creative readiness and flight. - Idempotency replay. An exact replay of the original
create_media_buyreturns the original response, includingconfirmed_at: nulland the originalrevision, even after the seller has confirmed. Buyers read current state throughget_media_buys; a replay never shows the confirmed state and never incrementsrevision. - Rejection and cancellation. The seller MAY reject a held buy (
status: rejected, withrejection_reason), and the buyer MAY cancel it withupdate_media_buy(canceled: true). In both casesconfirmed_atstaysnull: a buy that was never committed does not acquire a commitment timestamp by ending. - Pause and resume. The
valid_actionstable listspauseforpending_creativesandpending_startregardless of commitment. A buyerpauseon a held buy sets the same latent create-time hold described above and does not commit the seller or changeconfirmed_at. A seller that does not accept buyerpauseorresumeon a buy it is still reviewing SHOULD omit them fromvalid_actionsand reject the request withINVALID_STATE. - Flight start before confirmation. Because a provisional buy MUST NOT be
active, thepending_starttoactivetransition does not occur for it. A held buy whose flight start passes stayspending_startuntil the seller either confirms it (after which it follows the ordinary lifecycle, so a confirmed buy whose flight has begun and whose creatives are ready isactive) or rejects it. Sellers SHOULD confirm or reject before the flight start. - Expected confirmation time. A seller MAY expose an informational
expected_confirmation_atonget_media_buyswhileconfirmed_atisnull, such as the end of a review window or the next ad-server read-back. It is absent onceconfirmed_atis set. It is not an SLA or an expiry, no seller or buyer behavior depends on it, and buyers MUST key commitment offconfirmed_at, never off this field. - Commitment waits on read-back. A seller whose commitment waits on ad-server read-back keeps
confirmed_at: nulluntil the check passes, and the buy reportspending_startorpending_creativesmeanwhile. - Observation is by polling. Confirmation, rejection, and seller cancellation are observable through
get_media_buys. The media-buy wire defines no status-change webhook, so buyers cannot rely on one; a seller advertising the experimentalaccount.change_feedcapability withmedia_buycoverage MAY emit its generic invalidation, which the buyer repairs throughget_media_buys.
- Packages MAY have a
creative_deadline— after this deadline, creative changes to the package are rejected withCREATIVE_DEADLINE_EXCEEDED. When absent, the media buy’screative_deadlineapplies.CREATIVE_REJECTEDcovers content-policy, brand-safety, and accessibility-review failures. - Package cancellation (
canceled: truein a package update) is irreversible and independent of the media buy’s status — a single package can be canceled while the media buy remains active. Creative assignments on the canceled package are released per the media-buy-level rule above; creatives assigned to other active packages on the same media buy are unaffected. - Sales agents MUST retain delivery data for canceled packages. When
include_snapshotis true, sales agents SHOULD return a final snapshot reflecting delivery state at the time of cancellation. - When all packages in a media buy are canceled, the media buy itself remains in its current status (
activeorpaused). Sellers that support mid-flight package additions advertiseadd_packagesinvalid_actions— the buyer can add new packages vianew_packagesinupdate_media_buy. Otherwise, the buyer SHOULD explicitly cancel the media buy. Sales agents SHOULD notify orchestrators (viacontext.notesin the update response) when the last active package is canceled. Sales agents MAY auto-transition the media buy tocanceledafter a seller-defined grace period if no new activity occurs.
Creative Approval on Packages
Schema:enums/creative-approval-status.json
Each package tracks per-creative approval status separately from the creative’s library-level status. A creative may be approved in the library but rejected on a specific package (e.g., wrong format for the placement).
Rejection is not terminal — the buyer fixes the creative and resubmits through the seller’s advertised creative path, which resets the approval to
pending_review. The resubmission path:
- Check
rejection_reasononget_media_buysresponse - Fix the creative (update assets, adjust manifest)
- Resubmit via
sync_creativesfor library-backed sellers, or viapackages[].creativesonupdate_media_buyfor inline-only sellers - Approval resets to
pending_review
creative_deadline: If a creative is rejected after the package’s creative_deadline, the buyer MAY still resubmit — sellers SHOULD accept resubmissions for rejected creatives even past the deadline, since the buyer is correcting a seller-identified issue. Sellers that cannot accept late resubmissions MUST return CREATIVE_DEADLINE_EXCEEDED.
Verification Tag Enforcement
When a confirmed package’sperformance_standards includes any entry with a vendor, creatives assigned to that package MUST include at least one URL asset with url_type: "tracker_script" or url_type: "tracker_pixel" corresponding to each specified vendor. Sales agents SHOULD reject creative assignments that lack required verification tags with CREATIVE_REJECTED and a details message identifying the missing vendor tag. Buyer agents SHOULD proactively include vendor tags based on the agreed measurement_terms before submitting creatives.
Tasks
The Media Buy Protocol defines the following tasks. See task reference pages for complete request/response schemas and examples.get_products
Schema:media-buy/get-products-request.json / media-buy/get-products-response.json
Reference: get_products task
Discover advertising inventory using natural language briefs or explicit wholesale intent.
Requirements:
- Orchestrators MUST set
buying_modeto"brief","wholesale", or"refine" - Orchestrators MUST include
briefwhenbuying_modeis"brief" - Orchestrators MUST NOT include
briefwhenbuying_modeis"wholesale"or"refine" - Orchestrators MUST include
refinewhenbuying_modeis"refine" - Orchestrators MUST NOT include
refinewhenbuying_modeis"brief"or"wholesale" - Orchestrators MUST provide
scopeandproduct_idfor each product entry inrefine, andscopeandproposal_idfor each proposal entry - Orchestrators MAY omit
actionon product and proposal entries (defaults to"include") - Orchestrators MUST NOT include multiple entries for the same product ID or proposal ID in a single
refinearray - Sales agents MUST return products matching the brief criteria when a brief is provided
- Sales agents MUST include
product_idandpricing_optionsfor each product - Sales agents SHOULD include relevance scoring when multiple products match
- Sales agents that understand a well-formed
brieforrefinerequest but deliberately decline to offer products for commercial, availability, or seller-policy reasons MUST return theGetProductsRejectedarm withstatus: "rejected"and a buyer-facingreason - Sales agents MUST NOT use
GetProductsRejectedfor a completed search with no matching inventory, incomplete work, or a technical failure: usestatus: "completed"withproducts: [], a usable partial response withincomplete[]/ an async task arm, or the error channel respectively incomplete[]describes missing scopes in an otherwise usable response; it does not classify the condition as retryable. A timeout or unavailable upstream that prevents a usable response MUST use the error channel, normallySERVICE_UNAVAILABLEwitherror.recovery: "transient"and optionalretry_after- Sales agents using
GetProductsRejectedMUST NOT also returnproducts,proposals,incomplete,filter_diagnostics,refinement_applied,cache_scope,errors, or envelope-leveladcp_error; the task completed successfully with a structured business outcome rather than a full or partial search result - Orchestrators receiving
GetProductsRejectedMUST treat the transport call as successful, dispatch onstatus: "rejected", and usesuggestions[]when present to decide whether to revise and resubmit the brief
get_products request with buying_mode: "refine" is self-contained — sales agents MUST NOT depend on transport-level session state. The refine array and filters on each request fully specify the refinement intent. Sellers maintain their own product and proposal registries; “stateless” means the protocol exchange carries no implicit state between calls. This enables stateless implementations and safe retries.
- Sales agents MUST treat a missing
actionon product and proposal refine entries asaction: "include" - Sales agents MUST omit products with
action: "omit"from the response - Sales agents MUST omit proposals with
action: "omit"from the response - Sales agents MUST return products with
action: "include", with updated pricing - Sales agents SHOULD fulfill the
askon product entries withaction: "include" - Sales agents SHOULD return additional products similar to those with
action: "more_like_this", plus the original product - Sales agents SHOULD consider request-level asks (
scope: "request") when composing the response — this MAY result in additional products beyond those explicitly referenced. Per-product actions take precedence over request-level direction. - Sales agents SHOULD fulfill the
askon proposal entries withaction: "include" - Sales agents SHOULD include
refinement_appliedin the response, with one entry per change request matched by position - Sales agents that return
refinement_appliedMUST echoscopeon each entry and MUST echoproduct_id/proposal_idfor product and proposal scopes, so orchestrators can cross-validate alignment - Sales agents MAY return proposals alongside products in refine mode, even when the orchestrator did not include proposal entries
Canonical creative-format discovery
Sales-agentlist_creative_formats is deprecated in AdCP 3.2. An agent-global list cannot represent product-, placement-, account-, or inventory-specific deliverability.
For older 3.x callers, the deprecated media-buy request still accepts account.
Sellers with account-specific catalogs use it to scope the returned formats;
sellers with one shared catalog MAY ignore it. New integrations use
get_products with account and read each product’s format_options[].
Use these canonical sources instead:
- Publisher acceptance: publisher
adagents.json.formats[] - Sales-agent deliverability:
get_productsProduct.format_options[] - Creative production/validation/preview: creative agent
get_adcp_capabilities.creative.supported_formats[]
- Sales agents MUST publish the closed sellable format set on every product’s canonical
format_options[] - Placement-level declarations MAY narrow, but MUST NOT broaden, the product set
- Sales agents MUST NOT present publisher-catalog formats as if every declared format were deliverable by that agent
- Compatibility
list_creative_formatsresponses MAY be projected from canonical declarations for older 3.x peers, but MUST NOT be independently authored
create_media_buy
Schema:media-buy/create-media-buy-request.json / media-buy/create-media-buy-response.json
Reference: create_media_buy task
Create a media buy from selected packages or execute a proposal.
Requirements:
- Orchestrators MUST include either
packagesarray orproposal_id - Orchestrators MUST include
start_timeandend_timefor the campaign - Orchestrators MAY include
predecessor_media_buy_idwithlineage_reasonto link the new buy to an earlier buy of the same seller and account; see Media Buy Lineage - Sales agents MUST return
media_buy_idon successful creation - Sales agents MUST return
confirmed_aton successful creation. The value is the seller commitment timestamp, ornullwhen the response creates a provisional buy that is not yet committed. - Sales agents MUST return
revisionon successful creation - Sales agents MUST return
creative_deadlineindicating when creatives must be uploaded - Sales agents MUST validate budget against pricing options
- On validation failure, sales agents MUST return an
errorsarray
update_media_buy
Schema:media-buy/update-media-buy-request.json / media-buy/update-media-buy-response.json
Reference: update_media_buy task
Modify an existing media buy’s budget, targeting, or settings.
Package operations are structurally explicit — the operation type is determined by where the package appears in the request:
Requirements:
- Orchestrators MUST include
media_buy_id - Sales agents MUST accept any
media_buy_idreturned byget_media_buysfor the authenticated account and MUST NOT refuse updates on the basis that the buy was originally created outside AdCP. Business constraints on specific operations are expressed by omitting them fromvalid_actions. See Account Ownership vs. Creation Surface. - Sales agents MUST apply PATCH semantics: only specified fields are updated;
omitted fields remain unchanged. Within a request-side package
targeting_overlay, this rule applies per dimension: omission preserves the dimension, a non-null value replaces it, andnullclears it. Successful response and readback schemas contain only the resulting non-null targeting state. - When a buyer attempts to cancel a media buy already in
canceled(canceled: trueon acanceledbuy), sales agents MUST reject withNOT_CANCELLABLE - All other updates to media buys in terminal states (
completed,rejected,canceled) — includingcanceled: trueattempts againstcompletedorrejectedbuys — MUST be rejected withINVALID_STATE - Orchestrators MAY cancel a media buy by setting
canceled: truewith optionalcancellation_reason - Sales agents MUST transition the media buy to
canceledstatus upon accepting cancellation - Sales agents MAY reject cancellation of a non-terminal media buy with error code
NOT_CANCELLABLE - Orchestrators MAY cancel individual packages by setting
canceled: trueon a package update - Sales agents MUST reject creative changes to packages past their
creative_deadlinewith error codeCREATIVE_DEADLINE_EXCEEDED - Orchestrators MAY add new packages to an existing media buy via
new_packages. Sales agents that support this MUST advertiseadd_packagesinvalid_actions. Sales agents that do not support adding packages MUST reject withUNSUPPORTED_FEATURE. - When root
canceled: trueis present alongside other fields in an update request, includingpackages[].canceled, sales agents MUST cancel the whole MediaBuy and MUST ignore all other fields exceptcancellation_reason. Root cancellation takes precedence over package cancellation and other concurrent mutations. Sales agents SHOULD include a structured successwarnings[]entry naming ignored fields; warnings no longer belong in opaquecontext. Package cancellation is evaluated independently only when rootcanceledis absent. Within a package update,canceled: truelikewise takes precedence over that package’s sibling fields: apply only its cancellation and reason and warn about ignored siblings. Root fields and other package entries still apply atomically unless root cancellation is present. - Sales agents MUST return
affected_packagescontaining the state of each directly modified package after the update is applied (or the proposed state if pending approval); an empty array is valid when only campaign-level fields (e.g.,paused,start_time) are updated - When manual approval is required, sales agents MUST persist the pending update request, MUST return
implementation_date: null, and MUST NOT return emptyaffected_packages - Sales agents SHOULD return the updated media buy state
sync_catalogs
Schema:media-buy/sync-catalogs-request.json / media-buy/sync-catalogs-response.json
Reference: sync_catalogs task
Synchronize catalogs (products, inventory, stores, and vertical feeds) on a seller account.
Requirements:
- Orchestrators MUST include
account_id - When
catalogsis provided, it MUST contain at least one catalog - When
catalogsis omitted, the call is discovery-only and returns existing catalogs without modification - Sales agents MUST return per-catalog outcomes, including what action was taken and any item-level issues
- Sales agents SHOULD support
dry_runfor validation without applying changes
list_creatives
list_creatives is defined in the Creative Protocol. Sales agents that host creative libraries MAY implement list_creatives as part of the Creative Protocol. See the list_creatives task reference.sync_creatives
sync_creatives is defined in the Creative Protocol. Any agent that advertises creative.has_creative_library: true implements sync_creatives as part of the Creative Protocol. See the sync_creatives task reference.get_media_buys
Schema:media-buy/get-media-buys-request.json / media-buy/get-media-buys-response.json
Reference: get_media_buys task
Retrieve operational media buy state, including package status, creative approvals, missing formats, and optional delivery snapshots.
Requirements:
- Orchestrators MAY filter by
account_id,media_buy_ids,status_filter, andpredecessor_media_buy_id(direct successors of a buy; see Media Buy Lineage) - Sales agents MUST echo
predecessor_media_buy_idandlineage_reasonon every media buy created with them - Orchestrators SHOULD use cursor pagination (
pagination.max_results/pagination.cursor) for broad scope queries - Sales agents MUST return every media buy owned by the authenticated account that matches the declared filter set, regardless of how the buy was created (AdCP, seller’s native APIs/UI, manual trafficking, legacy systems). See Account Ownership vs. Creation Surface.
- Sales agents MUST return current media buy status and package-level operational state for each matched media buy
- Sales agents SHOULD include
status_as_ofwhen the returned media-buy-levelstatusis served from a cache or computed as a rolled-up status from upstream legs. For rolled-up statuses,status_as_ofMUST NOT be later than the oldest upstream status observation that could affect the returned roll-up, so it never overstates freshness. Omit it or returnnullto make no freshness assertion; buyers MUST NOT infer that an omitted or null value means the status is live. Buyers usestatus_as_ofto interpret list-status freshness;updated_atremains the media buy’s last modification time. - Sales agents SHOULD include
valid_actionsfor each media buy, listing the actions the buyer can perform in the current state. This eliminates the need for agents to internalize the state machine. Expected mapping:
Sellers MAY omit actions based on business rules (e.g., omit
cancel when contractual obligations prevent it, omit add_packages when the platform does not support mid-flight additions).
When pending_creatives or pending_start is masking a create-time hold from paused: true, sellers SHOULD also include resume so buyers can clear the hold before the setup blocker clears.
valid_actions contains only mutation operations — read-only operations (get_media_buys, get_media_buy_delivery) are always permitted regardless of state. Agents SHOULD use valid_actions as an optimization hint but MUST handle INVALID_STATE errors gracefully, since valid_actions may not reflect real-time state changes from concurrent operations. Absence of an action means “not declared by this seller,” not necessarily “forbidden.”
For creative changes, sync_creatives in valid_actions is a legacy creative-change action label, not proof that the sync_creatives task exists. Buyers use the creative path the seller advertises: sync_creatives and creative_assignments for sellers with creative.has_creative_library: true, or packages[].creatives on update_media_buy for inline-only sellers that advertise media_buy.features.inline_creative_management: true.
- Orchestrators MAY request revision history by setting
include_historyto the number of most-recent entries desired. Sales agents SHOULD return ahistoryarray per media buy wheninclude_history > 0, containing revision number, timestamp, server-derived actor identity,origin(required on entries for changes that did not arrive through an AdCP task), action type, and optional summary. History entries MUST be ordered most-recent-first. - Sales agents MUST include one media-buy currency for every AdCP-authored buy.
total_budget, package budget/minimum constraints, and canonical BiddingPolicy monetary fields use that denomination, and every selected pricing option MUST declare it. Packages requiring another currency belong in a separate media buy.package.currencyremains a legacy/external-buy readback field;snapshot.currencymay identify an upstream reporting denomination but MUST NOT reinterpret authored budget or bidding values. - Sales agents SHOULD include creative approval outcomes and pending format requirements when available
- If
include_snapshotis true and snapshot data is omitted for a package, sales agents MUST returnsnapshot_unavailable_reason - When
include_snapshotis true and snapshots are returned, each snapshot MUST includeas_ofandstaleness_seconds - Default
status_filter: ["active"]applies only when bothmedia_buy_idsandpredecessor_media_buy_idare omitted
Creative state freshness
packages[].creative_assignments_as_of and packages[].creative_approvals[].approval_status_as_of on get_media_buys, and creatives[].status_as_of and assignments.assigned_packages[].approval_status_as_of on list_creatives, apply the status_as_of pattern to creative state. Keeping that state current is the seller’s job; these fields tell the buyer how current an answer is.
- Each value is the time of the seller’s last observation of its source of truth for that state. A rolled-up value MUST NOT be later than the oldest upstream observation that could affect it. Omission makes no freshness assertion; buyers MUST NOT infer that the state is live.
creative_assignments_as_ofcovers the package’s assignment set, meaning which creatives appear increative_approvals. A creative absent from the set is absent as of that time.approval_status_as_ofcoversapproval_status,rejection_reason, andapproval_scopes.status_as_ofcovers creativestatus.- Accepting an AdCP write (for example,
sync_creativesor a creative change onupdate_media_buy) MUST NOT by itself advance a value. It advances only when the seller observes the resulting state in its source of truth. A seller whose AdCP store is its source of truth observes at commit time. - A value earlier than the buyer’s last accepted write for that relationship means the seller has not yet confirmed the write, and the returned state predates it. No separate status value is needed for “not known yet.”
- Sales agents SHOULD include these values when the state is served from a cache or from a source that confirms asynchronously, such as a system that confirms assignments a day after the write.
- A change that only advances an
*_as_ofvalue MUST NOT firecreative.assignment_changedorcreative.status_changedand MUST NOT write an account change record. STALE_RESPONSEis the complementary signal: a seller that could not reach its source of truth and served cached state past its freshness target emits it alongside the payload. The*_as_ofvalues report how old each piece of state is in either case.
get_media_buy_delivery
Schema:media-buy/get-media-buy-delivery-request.json / media-buy/get-media-buy-delivery-response.json
Reference: get_media_buy_delivery task
Track performance metrics and campaign delivery.
Requirements:
- Orchestrators MUST include
media_buy_id - Sales agents MUST accept any
media_buy_idreturned byget_media_buysfor the authenticated account, regardless of creation surface. See Account Ownership vs. Creation Surface. - Sales agents MUST return delivery metrics at the package level
- Sales agents SHOULD include dimensional breakdowns when requested
- Sales agents MUST include
as_oftimestamp indicating data freshness
provide_performance_feedback
Schema:media-buy/provide-performance-feedback-request.json / media-buy/provide-performance-feedback-response.json
Reference: provide_performance_feedback task
Submit performance signals to enable publisher optimization.
Requirements:
- Orchestrators MUST include
media_buy_idand performance metrics - Sales agents MUST acknowledge receipt of feedback
- Sales agents SHOULD use feedback to optimize delivery within campaign constraints
sync_event_sources
Schema:media-buy/sync-event-sources-request.json / media-buy/sync-event-sources-response.json
Reference: sync_event_sources task
Configure event sources on a seller account for conversion tracking with upsert semantics.
Requirements:
- Orchestrators MUST include
account_id - When
event_sourcesis provided, it MUST contain at least one event source - When
event_sourcesis omitted, the call is discovery-only and returns all event sources on the account without modification - Sales agents MUST return per-source results with
actionindicating what happened - Sales agents MUST include seller-managed event sources in the response when present
- Sales agents SHOULD return
setupinstructions for newly created event sources - Sales agents MAY include
platform_idfor cross-referencing the event source in the seller’s platform; in 3.3 and later 3.x responses they MUST also emit the deprecatedseller_idalias with the same value when reporting this identifier
log_event
Schema:media-buy/log-event-request.json / media-buy/log-event-response.json
Reference: log_event task
Send conversion or marketing events for attribution and optimization.
Requirements:
- Orchestrators MUST include
event_source_idreferencing a configured event source - Orchestrators MUST include at least one event with
event_id,event_type, andevent_time - Sales agents MUST return
events_receivedandevents_processedcounts - Sales agents MUST deduplicate events by
event_id+event_type+event_source_id - Sales agents SHOULD report
partial_failuresfor individually failed events - Sales agents SHOULD return
match_qualityscore when user matching is attempted
sync_audiences
Schema:media-buy/sync-audiences-request.json / media-buy/sync-audiences-response.json
Reference: sync_audiences task
Manage first-party CRM audiences on a seller account. Upload hashed customer lists, check matching status, and reference the resulting audiences in targeting overlays.
Requirements:
- Orchestrators MUST include
account_id - Orchestrators MUST include at least one audience with hashed member data
- Sales agents MUST return per-audience outcomes with matching status
- Sales agents SHOULD support
push_notification_configfor async matching completion - Sales agents MUST accept SHA-256 hashed identifiers and MUST reject cleartext email/phone in
hashed_email/hashed_phonefields (PII minimization at the transport boundary; see Privacy Considerations for the retention/consent obligations that hashing does not satisfy)
Error Handling
Sales agents MUST return errors using the standard AdCP error schema. Common error codes:MEDIA_BUY_NOT_FOUND: Referenced media buy does not existPACKAGE_NOT_FOUND: Referenced package does not existPRODUCT_NOT_FOUND: Referenced product does not existPRODUCT_EXPIRED: Referenced configured product passedexpires_at; re-runget_productsBUDGET_EXCEEDED: Operation would exceed allocated budgetCREATIVE_REJECTED: Creative failed content-policy, brand-safety, or accessibility reviewCREATIVE_DEADLINE_EXCEEDED: Creative change submitted after the package’screative_deadlineINVALID_STATE: Operation is not permitted for the resource’s current status (e.g., updating a completed or canceled media buy)NOT_CANCELLABLE: Media buy or package cannot be canceled in its current stateGOVERNANCE_DENIED: A registered governance agent denied the transaction. Buyer may restructure the buy, escalate to human spending authority, or contact the governance agent.TERMS_REJECTED: Buyer-proposedmeasurement_termswere rejected by the seller. Error details SHOULD identify which term failed and the seller’s acceptable range or supported vendors. Recovery: adjust the proposed terms and retry, or omitmeasurement_termsto accept the product’s defaults.BIDDING_PLACEMENT_CONFLICT: Media-buy/package bidding scopes cannot be represented by the provider’s native campaign, package, or shared-strategy placement. Sellers MUST reject before mutation and identify conflicting scopes where safe.AMBIGUOUS_BIDDING_POLICY: Canonicalbiddingand legacy bidding fields are both present on the same effective package. Sellers reject rather than choosing precedence.REQUOTE_REQUIRED: Anupdate_media_buyrequest changes the parameter envelope (budget, flight dates, volume, targeting) the original quote was priced against. Thepricing_optionremains locked; the seller is declining the requested shape at that price. Distinct fromTERMS_REJECTED(measurement) andPOLICY_VIOLATION(content). Details SHOULD followerror-details/requote-required.json:envelope_fieldnames the breached paths,change_term_ididentifies the governing accepted right when present, anddecline_reasonSHOULD classify a structured seller-policy reason when the coarse dimension is safe to disclose. AdCP 3.2 buyers use the reason to inform refinement ofaccepted_proposal_id; some policies still require the buyer-safe message, a free-textask, or human negotiation. Legacy 3.1 callers adjust the update, rediscover products or terms, add packages when available, or create a separate media buy.REFERENCE_DEFINITION_CHANGED: A third-party-published definition pinned incommercial_terms.definition_pins[]no longer resolves to the pinnedcontent_digest.accept_proposalreturns it before mutating state. Details SHOULD followerror-details/reference-definition-changed.json. Recovery: refine the proposal to obtain fresh pins, review the changed definition, and finalize again. See Definition pins.VALIDATION_ERROR: Request format or parameter errorsAUTH_MISSING: No credentials were presented. Recovery: correctable.AUTH_INVALID: Credentials were presented but rejected (expired / revoked / malformed). Recovery: terminal.
Security Considerations
Transport Security
All Media Buy Protocol communications MUST use HTTPS with TLS 1.2 or higher.Authentication
- Orchestrators MUST authenticate with sales agents using valid credentials
- Sales agents MUST validate credentials before processing requests
- Sales agents MUST use account context to determine inventory access
Budget Authorization
- Sales agents MUST validate that accounts are authorized for requested budget levels
- Sales agents MUST NOT exceed authorized budget limits without explicit approval
Creative Security
- Sales agents MUST validate creative content for policy compliance
- Sales agents SHOULD scan creatives for malware and malicious content
- Sales agents MUST NOT serve creatives that fail security validation
Conformance
Sales Agent Conformance
A conformant Media Buy Protocol sales agent MUST:- Support at least one specified transport (MCP or A2A)
- Implement all tasks per their schemas
- Return required fields as defined in response schemas
- Use specified error codes
- Handle asynchronous operations appropriately
- Enforce authentication and authorization
- Apply
get_products.targeting_overlayto product availability, pricing, and forecasts - Omit
targeting_resolutionfor exact matches and disclose only modifications for configured alternatives - Treat placement, property, and collection selection as targeting and return complete effective package targeting on readback
- Return dimension-specific execution details, including demographics, inside the generic package
targeting_resolutionenvelope - For callers negotiating AdCP 3.3 or later, pin every covered third-party-published definition in
commercial_terms.definition_pins[], verify the pins atfinalizeand beforeaccept_proposalmutates state, and never accept request-authored pins
Orchestrator Conformance
A conformant Media Buy Protocol orchestrator MUST:- Authenticate with sales agents
- Include required fields as defined in request schemas
- Handle asynchronous task-level responses (
submitted,working,input-required) including webhook delivery of completion artifacts - Use
media_buy_idto reference media buys in subsequent operations - Respect
creative_deadlinefor creative uploads - Treat a selected context-scoped
product_idas acceptance of its disclosedtargeting_resolution
Implementation Notes
Response Time Expectations
Sales agents SHOULD target the following response times:Idempotency
Every state-mutating AdCP task request MUST carry anidempotency_key, except the published 3.x get_products compatibility facade, whose request shape keeps the field optional. In AdCP 3.2, product discovery uses explicit contracts: request_proposals, refine_proposals, and decline_proposals require a key; list_products and the legacy facade leave it optional. Pure reads MUST tolerate the field but MAY ignore it, so buyers MUST NOT rely on keyed reads returning a cached response. A seller that elects or is required to provide replay applies this contract:
- The same key and equivalent canonical payload within the replay window MUST return the original response without re-executing the request.
- The same key with a different canonical payload MUST return
IDEMPOTENCY_CONFLICT. - Keys are scoped to the authenticated caller and account so retries cannot cross tenant boundaries.
get_products through 3.x for compatibility. Its key remains optional on every arm. A seller may ignore a supplied key on a guaranteed side-effect-free synchronous read. Buyers SHOULD supply one whenever the request may allocate a task or change observable state. When supplied and the seller declares adcp.idempotency.supported: true, the seller MUST apply replay protection before that effect. If the key is omitted or the seller declares adcp.idempotency.supported: false, an ambiguous result cannot be retried with a portable at-most-once guarantee. See Request safety — idempotency for the normative cache, replay, and concurrency rules.
Human-in-the-Loop
Sales agents MAY require human approval for any operation. Approval is modelled at the task layer:- When the seller is waiting on an internal human (e.g. IO signing, traffic-manager review), sales agents MUST keep the task in
status: "submitted"until the reviewer acts. On completion, the task transitions tocompletedwith the final artifact carryingmedia_buy_idand the full success payload. - When the seller needs the buyer to respond (e.g. confirm a budget that exceeds a pre-approved limit), sales agents MUST return
status: "input-required"with a message describing what is needed. The buyer responds within the same A2A context. - Sales agents SHOULD provide an estimated approval timeline in the task message.
- Orchestrators SHOULD implement webhook handlers (via
push_notification_config) for completion notifications rather than polling.
pending_approval is not a valid MediaBuy or Task status — that value exists only on Account.status (for account onboarding review). Do not repurpose it for media-buy or task-level approval.