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
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":
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
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 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/finalizefor proposals. - 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"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