brief or wholesale discovery, use buying_mode: "open" to reload unresolved products/proposals or buying_mode: "refine" to iterate on specific products and proposals — adjusting the selection, requesting changes, and exploring alternatives — before committing to a create_media_buy.
The refinement lifecycle
A typical media buying workflow follows this pattern:-
Discover — Call
get_productswithbuying_mode: "brief"or"wholesale"to find matching inventory. The seller returns products (and optionally proposals). -
Reload open work — Call
get_productswithbuying_mode: "open"to retrieve unresolved products/proposals already associated with the account or opportunity. The response reuses the existingproductsandproposalsarrays. -
Refine — Call
get_productswithbuying_mode: "refine"and arefinearray of change requests. Each entry declares a scope and what the buyer is asking for. The seller returns updated products with revised pricing and configurations. - Repeat — Refine as many times as needed. Each call is self-contained and stateless.
-
Buy or decline — When satisfied, execute the final selection via
create_media_buy. If a proposal is not being pursued, send a proposal-scopedaction: "decline"so the seller can resolve that proposal version.
opportunity.opportunity_id on each related brief, open, refine, and create_media_buy call. opportunity_id connects the overall opportunity; proposal_id + proposal_version resolves one exact seller offer. To close the planning cycle without buying, send a refine call with the same opportunity_id, opportunity.status: "closed", optional close reason/detail, and a request-level ask to close the opportunity.
The refine array
Therefine array is a list of change requests. Each entry declares a scope and what the buyer is asking for:
The
refine array requires at least one entry. The seller considers all entries together when composing the response, and replies to each one via refinement_applied.
Each scope uses its own id field — product_id for product entries, proposal_id for proposal entries — matching the id naming convention AdCP uses everywhere else. action is optional on product and proposal entries and defaults to "include".
Product actions
Product-scoped entries may declare an action. When omitted, the seller treats the entry as"include":
Request-level direction
Usescope: "request" to describe what you want from the selection as a whole:
refine array may appear in the response if the seller determines they fit the direction.
Precedence: Per-product actions take precedence over request-level direction. If the request-level ask says “less display” but a specific product carries an explicit action to include it, that product is returned regardless.
Proposal refinement
Reference proposals byproposal_id to request adjustments or remove them. Like product entries, action defaults to "include":
Decline a proposal
Useaction: "decline" when the buyer has evaluated a specific proposal version and decided not to pursue it. This is distinct from omit: omit only removes a proposal from the next response, while decline is a terminal outcome for the referenced proposal version.
Sellers that expect buyers to resolve a proposal version explicitly, whether by accepting it via create_media_buy or declining it here, MUST include proposal_version on the proposal. Buyers MUST NOT send decline for an unversioned proposal; use omit, ignore it, or re-run discovery/refinement to obtain a versioned proposal.
action: "decline", sellers MUST acknowledge the request with refinement_applied. status: "applied" means the decline was recorded and the proposal version is terminal. status: "partial" or "unable" means the buyer MUST NOT treat the proposal as declined.
After a seller applies a proposal-scoped decline, the buyer MUST NOT execute that same proposal_id + proposal_version with create_media_buy. Sellers MUST reject later create_media_buy attempts against a declined proposal version with INVALID_STATE.
Duplicate declines for the same buyer, account, proposal_id, and proposal_version are idempotent: sellers MUST NOT double-count the feedback. If a duplicate decline repeats the same reason and detail, sellers SHOULD acknowledge it as already applied. If a duplicate decline supplies different feedback, sellers SHOULD preserve the first applied reason and MAY return refinement_applied[].status: "partial" or "unable" with notes explaining that the proposal version was already declined.
Decline feedback is aggregate-friendly. Use reason for seller analytics and keep detail short and non-identifying. Buyers MUST NOT include competing seller/platform/product names, clearing prices, bid strategy, confidential audience definitions, budget reallocations, campaign IDs, NDA-scoped terms, or other sensitive campaign information in detail.
Resolving buyer outcomes
Use the narrowest terminal signal that matches what the buyer knows:open mode is a read surface for unresolved state; it is not itself a loss signal. Sellers SHOULD compute win/loss over closed opportunities and resolved proposal versions, not over all discovery/refinement/open calls.
Combining scopes
All scopes work together. A single refinement call can set direction for the selection, act on specific products, and request changes to a proposal:Seller response
When a buyer sends arefine array, the seller responds with refinement_applied — an array matched by position to the buyer’s change requests. Each entry reports whether the ask was fulfilled:
refinement_applied array MUST contain the same number of entries in the same order as the refine array. Each entry MUST echo scope and the matching id (product_id on product scope, proposal_id on proposal scope) so orchestrators can cross-validate alignment. For proposal entries where the buyer supplied proposal_version, sellers MUST echo proposal_version on the matching response entry. The entire field is optional for ordinary refinements — sellers that don’t track per-ask outcomes can omit it — but sellers MUST return refinement_applied for any request containing proposal-scoped action: "decline".
Orchestrators SHOULD cross-check entries by the echoed id rather than trusting positional order alone — a seller bug that reorders entries would otherwise silently mis-attribute each outcome.
Common refinement patterns
Find similar products
Usemore_like_this to discover products similar to ones you like. The seller returns the original product plus additional options matching its characteristics:
Adjust filters
Filters on a refine request represent the complete target state, not a delta. Always send the full filter set you want applied:Narrow or expand a proposal
The product entries define which products the seller should consider for the proposal. Combined with proposal entries, this narrows or expands the proposal’s product set:Finalize is exclusive within refine[]
action: "finalize" is seller commitment, not buyer acceptance — it transitions a draft proposal to committed with firm pricing and an expires_at hold window. The buyer accepts/executes a committed proposal later via create_media_buy(proposal_id). When a buyer wants to finalize, the spec requires the refine[] array contain only finalize entries:
- If any entry has
action: "finalize", all entries in the array MUST be proposal-scoped withaction: "finalize". Mixing finalize withinclude/omit/declineentries, or with request- or product-scoped entries, MUST be rejected by the seller withINVALID_REQUEST. - Buyers needing to refine and commit in close succession sequence the calls: first a refine call (no finalize), then a finalize call against the resulting
proposal_id(s). The two intents are distinct decisions and the spec treats them as distinct calls.
unfinalize operation — atomicity runs on the pre-commit validation gate, not on post-commit reversal. Sellers that cannot guarantee atomic pre-commit validation MUST reject multi-finalize arrays with MULTI_FINALIZE_UNSUPPORTED (preferred — explicitly signals seller-side capability gap rather than client-side mistake) or INVALID_REQUEST (acceptable fallback for sellers on a pre-3.1 error catalog), and buyers SHOULD then sequence single-finalize calls.
Mid-commit failure (post-validation, pre-persist). If a downstream system fails between commit one and commit two — e.g., the second ad server times out after the first has already locked inventory — the seller MUST return INTERNAL_ERROR with refinement_applied[] carrying per-position outcomes. The spec does NOT define a recovery path: buyers SHOULD treat the resulting state as undefined and re-read via get_media_buys / equivalent before retrying. Recovery from this case is operational, not protocol-defined.
Buyer intent caveat. Buyers whose intent specifically requires atomic commit (e.g., budget-shared proposals where one finalizing without the other is incoherent) MUST be prepared to abandon the intent if the seller returns MULTI_FINALIZE_UNSUPPORTED. The fallback path — sequencing single-finalize calls — is a looser commit guarantee than the original atomic intent; there is no recovery for that loss of intent beyond accepting the looser guarantee or declining to commit at all. There is no capability flag for multi-finalize support — the failure response is the discovery surface, so buyers MUST NOT assume support without a successful first attempt.
Proposals in refine mode
Sellers MAY return proposals alongside refined products, even when the buyer did not include proposal entries. For example, a buyer refining three products may receive those products back with updated pricing and a proposal suggesting how to combine them. Key points:- Proposals are not guaranteed. Sellers are not required to generate proposals in refine mode. Allocation and campaign optimization are primarily orchestrator (buyer-side agent) responsibilities.
- Signal interest via a request-level ask. Include
{ "scope": "request", "ask": "suggest how to combine these products" }to indicate you’d welcome a proposal. - Unsolicited proposals can be refined, declined, or ignored. If a seller returns a proposal you didn’t request, you can refine it in a follow-up call, explicitly decline it when you know you will not pursue it, or ignore it and build packages manually via
create_media_buy.
wholesale mode, where the buyer is directing targeting and allocation themselves.
Statelessness
Eachget_products request with buying_mode: "refine" is self-contained. The refine array and filters on each request fully specify the refinement intent. Sales agents MUST NOT depend on transport-level session state (e.g., remembering what was sent in a previous request).
Sellers still maintain their own product and proposal registries — “stateless” means the protocol exchange carries no implicit state between calls.
This design enables:
- Stateless implementations — sellers don’t need to track refinement sessions
- Safe retries — a failed refinement call can be retried with the same parameters
- Parallel exploration — an orchestrator can explore multiple refinement paths simultaneously
Client validation
Orchestrators should validate refinement requests before sending:- Non-empty refine — The
refinearray requires at least one entry. An empty[]is rejected by schema validation. - Valid entries — Each product entry requires
scopeandproduct_id. Each proposal entry requiresscopeandproposal_id. Request-level entries requirescopeandask.actionis optional on product and proposal entries (defaults to"include"); valid values areinclude/omit/more_like_thisfor products andinclude/omit/finalize/declinefor proposals. Proposal declines requireproposal_versionandreason. - Filters are absolute — Send the full filter set you want applied, not a delta from the previous request.
Error handling
Troubleshooting: “must NOT have additional properties” on refine[].id
Each scope branch in refine[] is additionalProperties: false, which means a stray id field from the pre-3.0-rc refine shape is rejected — not silently ignored — with an error along the lines of:
id field. Rename to product_id under scope: "product" and proposal_id under scope: "proposal". See the task reference for the current shape. The same rename applies to refinement_applied[] if you’re echoing on the seller side.
Seller migration
Sellers returningrefinement_applied have breaking work alongside buyers:
- Each response entry MUST now carry
scope, and for product/proposal scopes MUST echoproduct_id/proposal_id. Flat{status, notes}entries are rejected by the response schema. - Missing
actionon an incomingrefine[]entry MUST be treated asaction: "include", not parsed as an error. - Seller conformance tests against the 3.0 request schema will reject any lingering orchestrator payloads that still use the generic
idfield — refresh your fixture corpus after upgrading.
Normative requirements
The Media Buy Specification defines the following normative requirements for refinement: Orchestrators:- MUST include
refinewhenbuying_modeis"refine" - MUST NOT include
refinewhenbuying_modeis"brief","wholesale", or"open" - MUST provide
scopeandproduct_idfor each product entry, andscopeandproposal_idfor each proposal entry - MAY omit
actionon product and proposal entries — sellers treat missingactionas"include" - MUST NOT include multiple entries for the same product ID or proposal ID in a single
refinearray - MUST include
proposal_versionandreasonwhen sending proposal-scopedaction: "decline" - MUST NOT execute a declined
proposal_id+proposal_versionwithcreate_media_buy - MUST NOT send
action: "decline"for an unversioned proposal - SHOULD close other seller opportunities as
purchased_elsewherewhen a comparable buy is accepted with another seller, and SHOULD decline any evaluated proposal versions from those sellers asselected_alternative
- MUST omit products with
action: "omit"from the response - MUST omit proposals with
action: "omit"from the response - MUST treat proposal-scoped
action: "decline"as a terminal not-pursued outcome for the referencedproposal_id+proposal_version - MUST return
refinement_appliedfor requests containing proposal-scopedaction: "decline";status: "applied"is the signal that the decline is recorded - MUST reject
create_media_buyattempts against a declined proposal version withINVALID_STATE - MUST reject stale
proposal_versionexecution for a known proposal withCONFLICT - MUST support
buying_mode: "open"as a read of unresolved products/proposals when the seller persists that state - MUST treat duplicate declines for the same buyer, account,
proposal_id, andproposal_versionas idempotent and MUST NOT double-count them - MUST return products with
action: "include", with updated pricing - SHOULD fulfill the
askon product entries withaction: "include" - SHOULD return additional products similar to those with
action: "more_like_this", plus the original product - SHOULD consider request-level asks when composing the response — this MAY result in additional products beyond those explicitly referenced. Per-product actions take precedence over request-level direction.
- SHOULD fulfill the
askon proposal entries withaction: "include" - SHOULD include
refinement_appliedin the response when the buyer providesrefine, with one entry per change request matched by position - MAY return proposals even when the buyer did not include proposal entries
See also
get_productstask reference — API reference with request/response schemas- Media Products — product model and proposal structure
- Media Buy Specification — normative requirements
- Orchestrator Design — building buyer-side agents