Skip to main content
Refinement turns product discovery into a conversation. After an initial 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:
  1. Discover — Call get_products with buying_mode: "brief" or "wholesale" to find matching inventory. The seller returns products (and optionally proposals).
  2. Reload open work — Call get_products with buying_mode: "open" to retrieve unresolved products/proposals already associated with the account or opportunity. The response reuses the existing products and proposals arrays.
  3. Refine — Call get_products with buying_mode: "refine" and a refine array 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.
  4. Repeat — Refine as many times as needed. Each call is self-contained and stateless.
  5. Buy or decline — When satisfied, execute the final selection via create_media_buy. If a proposal is not being pursued, send a proposal-scoped action: "decline" so the seller can resolve that proposal version.
If the buyer is working within a named planning cycle, include the same 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.
Refinement is not required. Simple campaigns can go straight from discovery to purchase. But for campaigns involving multiple products, proposals with budget allocations, or iterative negotiation, refinement is where the value is.

The refine array

The 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

Use scope: "request" to describe what you want from the selection as a whole:
The seller may add, remove, or rebalance products based on this direction. Products not referenced in the 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 by proposal_id to request adjustments or remove them. Like product entries, action defaults to "include":

Decline a proposal

Use action: "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.
For 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 a refine 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:
The 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

Use more_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 with action: "finalize". Mixing finalize with include / omit / decline entries, or with request- or product-scoped entries, MUST be rejected by the seller with INVALID_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.
Multi-finalize is atomic at the observation point. When multiple finalize entries target different proposals in one call, the contract is: sellers MUST NOT return a success response unless every named proposal has both completed and been persisted as committed. Pre-commit validation runs before any side-effects (inventory pull, terms lock, governance attestation); if any proposal fails validation, the seller MUST reject the entire call without committing any. There is no 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.
Publishers typically omit proposals in wholesale mode, where the buyer is directing targeting and allocation themselves.

Statelessness

Each get_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 refine array requires at least one entry. An empty [] is rejected by schema validation.
  • Valid entries — Each product entry requires scope and product_id. Each proposal entry requires scope and proposal_id. Request-level entries require scope and ask. action is optional on product and proposal entries (defaults to "include"); valid values are include / omit / more_like_this for products and include / omit / finalize / decline for proposals. Proposal declines require proposal_version and reason.
  • Filters are absolute — Send the full filter set you want applied, not a delta from the previous request.
Client implementations should validate refinement requests against the request schema before sending.

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:
If you see this, an orchestrator is still constructing product or proposal refine entries with the generic 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 returning refinement_applied have breaking work alongside buyers:
  • Each response entry MUST now carry scope, and for product/proposal scopes MUST echo product_id / proposal_id. Flat {status, notes} entries are rejected by the response schema.
  • Missing action on an incoming refine[] entry MUST be treated as action: "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 id field — refresh your fixture corpus after upgrading.

Normative requirements

The Media Buy Specification defines the following normative requirements for refinement: Orchestrators:
  • MUST include refine when buying_mode is "refine"
  • MUST NOT include refine when buying_mode is "brief", "wholesale", or "open"
  • MUST provide scope and product_id for each product entry, and scope and proposal_id for each proposal entry
  • MAY omit action on product and proposal entries — sellers treat missing action as "include"
  • MUST NOT include multiple entries for the same product ID or proposal ID in a single refine array
  • MUST include proposal_version and reason when sending proposal-scoped action: "decline"
  • MUST NOT execute a declined proposal_id + proposal_version with create_media_buy
  • MUST NOT send action: "decline" for an unversioned proposal
  • SHOULD close other seller opportunities as purchased_elsewhere when a comparable buy is accepted with another seller, and SHOULD decline any evaluated proposal versions from those sellers as selected_alternative
Sales agents:
  • 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 referenced proposal_id + proposal_version
  • MUST return refinement_applied for requests containing proposal-scoped action: "decline"; status: "applied" is the signal that the decline is recorded
  • MUST reject create_media_buy attempts against a declined proposal version with INVALID_STATE
  • MUST reject stale proposal_version execution for a known proposal with CONFLICT
  • 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, and proposal_version as idempotent and MUST NOT double-count them
  • MUST return products with action: "include", with updated pricing
  • SHOULD fulfill the ask on product entries with action: "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 ask on proposal entries with action: "include"
  • SHOULD include refinement_applied in the response when the buyer provides refine, with one entry per change request matched by position
  • MAY return proposals even when the buyer did not include proposal entries

See also