> ## Documentation Index
> Fetch the complete documentation index at: https://docs.adcontextprotocol.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Refinement

> AdCP product refinement — iterate on discovered products and proposals using refine mode in get_products. Adjust selections and request changes before creating a media buy.

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`](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/media-buy/task-reference/create_media_buy).

## The refinement lifecycle

A typical media buying workflow follows this pattern:

```
discover → refine → refine → ... → buy
```

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`.

> [!TIP]
>
> 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

> [!NOTE]
>
> This is the deprecated `get_products` compatibility grammar. New compact-lifecycle integrations use [`refine_proposals`](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/media-buy/task-reference/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:

| Scope | Purpose | Required fields |
| - | - | - |
| `request` | Direction for the selection as a whole | `ask` |
| `product` | Action on a specific product | `product_id` |
| `proposal` | Action on a specific proposal | `proposal_id` |

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

| Action | Behavior | `ask` |
| - | - | - |
| `include` (default) | Return this product with updated pricing and data | Optional — specific changes to request (e.g., "add 16:9 format") |
| `omit` | Exclude this product from the response | Ignored |
| `more_like_this` | Find additional products similar to this one. The original product is also returned. | Optional — what "similar" means (e.g., "same audience but video format") |

```json theme={null}
{
  "idempotency_key": "550e8400-e29b-41d4-a716-446655442013",
  "buying_mode": "refine",
  "refine": [
    { "scope": "product", "product_id": "prod_video_premium", "ask": "add 16:9 format option" },
    { "scope": "product", "product_id": "prod_display_ros", "action": "omit" },
    { "scope": "product", "product_id": "prod_native", "action": "more_like_this", "ask": "same audience but video format" }
  ]
}
```

### 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.

```json theme={null}
{
  "$schema": "https://adcontextprotocol.org/schemas/3.3.0-beta.1/media-buy/get-products-request.json",
  "adcp_version": "3.3",
  "adcp_major_version": 3,
  "buying_mode": "refine",
  "refine": [
    {
      "scope": "product",
      "product_id": "prod_homepage_takeover",
      "action": "include",
      "package": {
        "pricing_option_id": "cpm_guaranteed",
        "budget": 25000,
        "start_time": "2026-11-01T00:00:00Z",
        "end_time": "2026-11-30T23:59:59Z",
        "targeting_overlay": {}
      }
    }
  ]
}
```

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

```json theme={null}
{
  "scope": "product",
  "product_id": "prod_homepage_takeover",
  "result_product_id": "prod_homepage_takeover_configured",
  "resolved_start_time": "2026-11-01T00:00:00-05:00",
  "resolved_end_time": "2026-11-30T23:59:59-05:00",
  "status": "applied"
}
```

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:

```json theme={null}
{
  "idempotency_key": "550e8400-e29b-41d4-a716-446655442014",
  "buying_mode": "refine",
  "refine": [
    { "scope": "request", "ask": "good selection but I want more video options and less display" },
    { "scope": "product", "product_id": "prod_video_premium" },
    { "scope": "product", "product_id": "prod_display_ros" },
    { "scope": "product", "product_id": "prod_native" }
  ]
}
```

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

```json theme={null}
{
  "idempotency_key": "550e8400-e29b-41d4-a716-446655442015",
  "buying_mode": "refine",
  "refine": [
    { "scope": "product",  "product_id":  "prod_video_premium" },
    { "scope": "product",  "product_id":  "prod_display_ros" },
    { "scope": "proposal", "proposal_id": "prop_balanced_v1", "ask": "shift 20% from display to video" }
  ]
}
```

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

```json theme={null}
{
  "idempotency_key": "550e8400-e29b-41d4-a716-446655442016",
  "buying_mode": "refine",
  "refine": [
    { "scope": "request",                                         "ask": "increase emphasis on video across the plan" },
    { "scope": "product",  "product_id":  "prod_video_premium" },
    { "scope": "product",  "product_id":  "prod_display_ros" },
    { "scope": "product",  "product_id":  "prod_native",          "action": "omit" },
    { "scope": "product",  "product_id":  "prod_audio_spot" },
    { "scope": "proposal", "proposal_id": "prop_awareness_q2",    "ask": "reallocate native budget to video products" }
  ],
  "filters": {
    "budget_range": { "min": 200000, "max": 200000, "currency": "USD" }
  }
}
```

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

| Field | Type | Required | Description |
| - | - | - | - |
| `scope` | string | Yes | Echoes the scope (`"request"` / `"product"` / `"proposal"`) from the corresponding `refine` entry. |
| `product_id` | string | Yes when `scope` is `"product"` | Echoes `product_id` from the corresponding refine entry. |
| `result_product_id` | string | For an applied package forecast | Identifies exactly one returned Product carrying the exact forecast; may differ from the echoed request ID. |
| `resolved_start_time`, `resolved_end_time` | date-time | With `result_product_id` | Exact forecast flight boundaries, copied to the package at create. |
| `proposal_id` | string | Yes when `scope` is `"proposal"` | Echoes `proposal_id` from the corresponding refine entry. |
| `status` | string | Yes | `"applied"`: ask fulfilled. `"partial"`: partially fulfilled. `"unable"`: could not fulfill. |
| `notes` | string | Conditional | Seller explanation. Recommended when status is `"partial"` or `"unable"`; required and non-empty for a structured package entry with status `"unable"`. |

```json theme={null}
{
  "products": ["..."],
  "proposals": ["..."],
  "refinement_applied": [
    { "scope": "request",                                            "status": "applied", "notes": "Added 3 video products. No CTV inventory for those dates." },
    { "scope": "product",  "product_id":  "prod_video_premium",      "status": "applied" },
    { "scope": "product",  "product_id":  "prod_display_ros",        "status": "applied" },
    { "scope": "product",  "product_id":  "prod_native",             "status": "applied" },
    { "scope": "product",  "product_id":  "prod_audio_spot",         "status": "partial", "notes": "16:9 not available for this placement — returning 4:3 and 1:1" },
    { "scope": "proposal", "proposal_id": "prop_awareness_q2",       "status": "applied", "notes": "Shifted 22% to video (nearest allocation boundary)" }
  ]
}
```

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:

```json theme={null}
{
  "buying_mode": "refine",
  "refine": [
    {
      "scope": "proposal",
      "proposal_id": "prop_awareness_q2",
      "ask": "reduce the CPM by 10% while keeping the same inventory and dates"
    }
  ]
}
```

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:

```json theme={null}
{
  "idempotency_key": "550e8400-e29b-41d4-a716-446655442017",
  "buying_mode": "refine",
  "refine": [
    { "scope": "product", "product_id": "prod_video_premium", "action": "more_like_this", "ask": "same premium audience but different formats" }
  ]
}
```

### Adjust filters

Filters on a refine request represent the complete target state, not a delta. Always send the full filter set you want applied:

```json theme={null}
{
  "idempotency_key": "550e8400-e29b-41d4-a716-446655442018",
  "buying_mode": "refine",
  "refine": [
    { "scope": "product", "product_id": "prod_video_premium" },
    { "scope": "product", "product_id": "prod_display_ros" }
  ],
  "filters": {
    "start_date": "2026-04-01",
    "end_date": "2026-06-30",
    "budget_range": { "min": 150000, "max": 150000, "currency": "USD" }
  }
}
```

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

```json theme={null}
{
  "idempotency_key": "550e8400-e29b-41d4-a716-446655442019",
  "buying_mode": "refine",
  "refine": [
    { "scope": "product",  "product_id":  "prod_video_premium" },
    { "scope": "product",  "product_id":  "prod_display_ros" },
    { "scope": "proposal", "proposal_id": "prop_balanced_v1", "ask": "rebalance for just these two products" }
  ]
}
```

## 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.

```yaml theme={null}
# ✅ Refine only — no finalize present, mixed scopes allowed
refine:
  - { scope: proposal, proposal_id: p1, ask: "shift more to ctv" }
  - { scope: request, ask: "frequency cap 3/day across all products" }

# ✅ Finalize only — multiple finalize entries against different proposals
refine:
  - { scope: proposal, proposal_id: p1, action: finalize }
  - { scope: proposal, proposal_id: p2, action: finalize }

# ❌ Rejected — finalize mixed with non-finalize
refine:
  - { scope: proposal, proposal_id: p1, action: finalize }
  - { scope: proposal, proposal_id: p2, ask: "shift more to ctv" }
```

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](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/building/schemas-and-sdks.md) before sending.

## Error handling

| Error Code | When | Resolution |
| - | - | - |
| `PRODUCT_NOT_FOUND` | One or more referenced product IDs are unknown, tombstones were evicted, or IDs belong to another account/lineage | Remove invalid IDs and retry, or re-discover with a `brief` request |
| `PRODUCT_EXPIRED` | One or more caller-authorized configured product IDs are still recognized and passed `expires_at` | Re-discover with a new `brief` or `wholesale` request; inaccessible IDs and IDs whose tombstones are no longer retained return `PRODUCT_NOT_FOUND` |
| `PROPOSAL_EXPIRED` | A referenced proposal ID has passed its `expires_at` | Re-discover with a new `brief` or `wholesale` request |
| `PROPOSAL_NOT_FOUND` | The referenced `proposal_id` is unknown to the seller (never issued, wrong tenant, or evicted from cache) | Re-issue `get_products` in `brief` or `wholesale` mode to obtain a fresh draft; finalize that recognized proposal before creating the media buy |
| `MULTI_FINALIZE_UNSUPPORTED` | `refine[]` carried multiple `action: 'finalize'` entries but the seller cannot guarantee atomic multi-proposal commit | Sequence single-proposal finalize calls (one finalize per `get_products` call) |
| `INVALID_REQUEST` | `refine` provided in `brief` or `wholesale` mode, empty `refine` array, or missing required fields | Check `buying_mode` and required fields |

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

```
/refine/0: must NOT have additional properties { additionalProperty: "id" }
/refine/0: must match oneOf schema { required: ["product_id"] }
```

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](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/media-buy/task-reference/get_products.md#refine-array) 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](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/media-buy/specification.md#get_products) 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](#check-an-exact-package-before-booking) 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

* [`get_products` task reference](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/media-buy/task-reference/get_products#refinement) — API reference with request/response schemas
* [Media Products](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/media-buy/product-discovery/media-products.md) — product model and proposal structure
* [Media Buy Specification](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/media-buy/specification.md#get_products) — normative requirements
* [Orchestrator Design](https://docs.adcontextprotocol.org/dist/docs/3.3.0-beta.1/building/operating/orchestrator-design.md) — building buyer-side agents


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.