Skip to main content
Refinement turns product discovery into a conversation. After an initial 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:
  1. Discover — Call get_products with buying_mode: "brief" or "wholesale" to find matching inventory. The seller returns products (and optionally proposals).
  2. 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.
  3. Repeat — Refine as many times as needed. Each call is self-contained and stateless.
  4. Buy — When satisfied, execute the final selection via create_media_buy.
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

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.
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":

Check an exact package before booking

In AdCP 3.3, a product entry with action: "include" (or no action) may carry an optional package. This asks whether the exact proposed package fits, using the same field types and validation rules as a package on create_media_buy. The enclosing entry supplies product_id; omit it inside package. pricing_option_id and numeric budget are required.
Resolve the inputs as on create. The budget is in the selected pricing option’s currency. An explicit impressions goal has the same meaning as on create; otherwise the seller derives the goal from the budget and selected pricing terms when possible. For example, a budget of $25,000 at a fixed $10 CPM implies 2,500,000 impressions. A budget alone need not imply a delivery goal for flat-rate or auction pricing. If the goal cannot be determined unambiguously, the seller reports unable rather than inventing a goal. Explicit package start_time and end_time take precedence. Each omitted boundary defaults to the corresponding filters.start_date or end_date, using the seller’s availability-check date and timezone semantics. If those inputs cannot resolve an exact flight, the seller reports unable; buyers can supply explicit timestamps to remove ambiguity. An applied outcome MUST echo both exact boundaries as resolved_start_time and resolved_end_time, including boundaries supplied explicitly. The end MUST be after the start. Resolve the product’s bound discovery targeting and any replacement top-level targeting_overlay first, then apply package.targeting_overlay per dimension as on create: omission inherits, non-null values replace, and null clears a configurable default. An empty object inherits targeting; it does not request untargeted inventory. Inherent product scope and offer filters remain binding. Free-text ask cannot override structured package inputs; contradictory requirements are rejected with INVALID_REQUEST. Return an exact result or explain why it cannot be computed. A seller handling a request containing any package MUST return the full, position-matched refinement_applied array. For each such entry:
  • applied means the returned product carries a forecast computed for exactly the resolved package goal, flight, targeting, and other supplied controls. The outcome MUST include result_product_id, identifying exactly one returned Product, and both resolved flight timestamps. The outcome’s product_id still echoes the request. Sellers MUST return this forecast even when fields omitted it. It uses forecast_range_unit: "availability"; points omit budget, report total available units in the goal metric in metrics, and carry availability_status. Zero availability still reports the goal metric as zero so the buyer can calculate the shortfall.
  • unable with non-empty notes explains unsupported controls, an unresolvable goal or flight, or an unavailable forecasting service. The corresponding product MUST NOT carry a forecast computed for different inputs. Sellers MUST NOT silently drop a control, substitute dates or targeting, or label a package forecast partial.
Insufficient inventory is a successfully computed (applied) forecast with availability_status: "unavailable", rather than a failure to forecast. With 2,000,000 available impressions in the CPM example, the shortfall is 500,000. Do not cap available units at the goal: the metrics report total availability. Forecast method describes the calculation, so a read-only check normally uses estimate or modeled; it does not establish reserved, guaranteed delivery. Carry the exact configuration into purchase. Changed request-bound targeting or forecast assumptions require a distinguishable configured product_id, as in ordinary refinement. Use result_product_id to find that product; product-array order is not a correlation mechanism. The returned product MUST retain the requested pricing_option_id and identical pricing terms. If the seller needs to substitute or reprice the option, it reports unable rather than computing a different budget-to-goal relationship. The seller binds effective discovery/top-level targeting to the returned ID and evaluates the package overlay with create’s inheritance rules. It MUST NOT turn package replacements or clears into additional inherent product scope, or report them as deviations from top-level discovery targeting in Product targeting_resolution. At create_media_buy, the buyer uses the mapped ID and unchanged package controls, including the same targeting overlay, budget, and pricing option, and materializes both resolved timestamps on the package. The media-buy flight must encompass those timestamps. Changing any forecast input requires a fresh check. For example, an entry for prod_homepage_takeover with omitted package times and date filters may produce this outcome after resolving the seller’s local date boundaries:
The buyer sets the create package’s product_id to prod_homepage_takeover_configured, and start_time/end_time to those exact timestamps. If its forecast package carried geo_countries: null, it repeats that clear in targeting_overlay; it does not restore the old product’s country default. Each entry is an independent read. Sellers MUST forecast each package against current inventory without subtracting the other proposed packages in the call. Products sharing inventory can therefore each report that inventory; buyers MUST NOT sum their forecasts as a jointly bookable plan. The existing one-entry-per-product rule still applies; compare two configurations of the same product in separate calls. The check MUST NOT create a media buy, book, hold, or reserve inventory, or upload or assign creatives carried in the package. Finalizing a proposal remains the inventory-hold operation. A later purchase still checks availability and can fail with PRODUCT_UNAVAILABLE. There is no separate structured-package capability flag. Buyers MUST negotiate AdCP 3.3 or later before treating an applied outcome as exact-package confirmation; a legacy seller may ignore an unfamiliar input. Check the served adcp_version echo: a downshift or missing echo does not confirm 3.3 support. An applied outcome is evidence for this entry only. Missing refinement_applied, a missing matching outcome, or a legacy seller rejecting the new field is no evidence that the exact package was forecast; buyers MUST NOT treat an ordinary product forecast as confirmation. Entries without package retain their existing behavior, including optional per-entry outcomes.

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":

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. The field is optional unless any entry carries package, in which case the seller MUST return it for the entire request. 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-text ask 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:
Buyers discover this legacy refinement mode when 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. For requests without structured package entries, 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

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 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.
For a committed proposal, expires_at is the inventory-hold deadline. The seller MUST persist the committed successor and its inventory reservation as one atomic state transition: no committed proposal may be observable without its hold, and a failed finalization may not leave an orphaned reservation. A seller that needs upstream or manual work uses the async task lifecycle and publishes the committed result only after the reservation succeeds. 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. The proposal contract does not define pDOOH booking horizons, finite frame/loop overlap identity, or an implementation-level concurrency-control algorithm for separate finalize calls. expires_at does not answer those questions; it only bounds an inventory hold once the proposal is committed.

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.
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 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 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 for 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_overlay and required_overlay_support. Changed effective targeting, forecast assumptions, non-pricing terms, resolution, or overlay support produces a new configured product_id; otherwise the seller keeps the prior ID stable until expiration. Changed commercial terms receive a new immutable pricing_option_id, so the selected pair remains unambiguous.
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" or "wholesale"
  • 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 use include (or omit action) when supplying a structured package; package is invalid on other scopes or product actions
Sales agents:
  • 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 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
  • MUST follow the exact-package forecast contract when a product entry carries package, including mandatory outcomes, exact inputs, independent availability forecasts, and no booking or inventory hold
  • MAY return proposals even when the buyer did not include proposal entries

See also