> ## Documentation Index
> Fetch the complete documentation index at: https://agenticadvertisingorg-adcp-issues-5537-5540.mintlify.site/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`](/dist/docs/3.1.0-rc.13/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.
</Tip>

## The refine array

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}
{
  "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" }
  ]
}
```

### Request-level direction

Use `scope: "request"` to describe what you want from the selection as a whole:

```json theme={null}
{
  "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}
{
  "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}
{
  "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.                                           |
| `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 | No                               | Seller explanation. Recommended when status is `"partial"` or `"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 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

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}
{
  "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}
{
  "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}
{
  "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" }
```

**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 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 `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` for proposals.
* **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](/dist/docs/3.1.0-rc.13/building/by-layer/L0/schemas) before sending.

## Error handling

| Error Code                   | When                                                                                                                  | Resolution                                                                                          |
| ---------------------------- | --------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| `PRODUCT_NOT_FOUND`          | One or more referenced product IDs are unknown or expired                                                             | Remove invalid IDs and retry, or re-discover with a `brief` request                                 |
| `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 finalized, wrong tenant, or evicted from cache)          | Re-issue `get_products` in `refine` mode with `action: 'finalize'` to obtain a current proposal\_id |
| `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](/dist/docs/3.1.0-rc.13/media-buy/task-reference/get_products#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](/dist/docs/3.1.0-rc.13/media-buy/specification#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

**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
* MAY return proposals even when the buyer did not include proposal entries

## See also

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