brief or wholesale discovery, use 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). -
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 — When satisfied, execute the final selection via
create_media_buy.
The refine array
This is the deprecated
get_products compatibility grammar. New compact-lifecycle integrations use refine_proposals. The surfaces share ask, product include/omit verbs, and the budget_range shape. Compact hard constraints are strict while these legacy filters remain extension-tolerant, and their response envelopes are intentionally different.refine 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":
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. The entire field is optional — sellers that don’t track per-ask outcomes can omit it — but sellers that return it MUST return valid, position-matched entries.
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
Price and budget asks
Use the free-textask field for commercial requests such as a lower price or a different budget allocation. For example, a buyer can ask for a proposal-scoped CPM reduction:
get_adcp_capabilities.media_buy.buying_modes includes "refine". That declaration means the seller can receive this free-text ask; it does not guarantee that the seller understands every negotiation strategy or has commercial flexibility on price. There is no finer-grained price-negotiation capability flag. Compact refine_proposals integrations separately inspect media_buy.proposal_refinement.supported_dimensions for typed parse-and-validate support.
When the seller returns refinement_applied, read the matching entry’s status (applied, partial, or unable) and optional notes to learn how it handled this specific request. These fields acknowledge handling; they are not the executable commercial terms. Inspect the returned proposal’s pricing and allocations before finalizing or buying.
Because the entire refinement_applied field is optional, omission communicates no per-ask outcome: do not infer applied, partial, or unable. A seller may omit it even when the response applies the requested change. Treat the returned proposal and any reported outcome as runtime evidence for this request, not as a general promise that the seller can negotiate price.
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/omitentries, 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 or ignored. If a seller returns a proposal you didn’t request, you can refine it in a follow-up call, or simply 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
opaque product and proposal IDs carry the configurations being refined; they do
not identify a transport session. Top-level filters, targeting_overlay, and
required_overlay_support, when present, are complete replacement state for
the refined results rather than deltas. When omitted, the configuration already
bound to each referenced product ID remains in force. Sales agents MUST NOT
depend on unreferenced transport-level or conversational state.
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/finalizefor proposals. - Filters are absolute — Send the full filter set you want applied, not a delta from the previous request.
- Targeting and future support are absolute when present — Send the complete
desired
targeting_overlayandrequired_overlay_support. Changed effective targeting, forecast assumptions, non-pricing terms, resolution, or overlay support produces a new configuredproduct_id; otherwise the seller keeps the prior ID stable until expiration. Changed commercial terms receive a new immutablepricing_option_id, so the selected pair remains unambiguous.
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"or"wholesale" - 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 omit products with
action: "omit"from the response - MUST omit proposals with
action: "omit"from the response - 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