Skip to main content

Documentation Index

Fetch the complete documentation index at: https://agenticadvertisingorg-changeset-release-main.mintlify.app/llms.txt

Use this file to discover all available pages before exploring further.

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:
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.
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:
ScopePurposeRequired fields
requestDirection for the selection as a wholeask
productAction on a specific productproduct_id
proposalAction on a specific proposalproposal_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":
ActionBehaviorask
include (default)Return this product with updated pricing and dataOptional — specific changes to request (e.g., “add 16:9 format”)
omitExclude this product from the responseIgnored
more_like_thisFind additional products similar to this one. The original product is also returned.Optional — what “similar” means (e.g., “same audience but video format”)
{
  "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:
{
  "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":
{
  "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:
{
  "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:
FieldTypeRequiredDescription
scopestringYesEchoes the scope ("request" / "product" / "proposal") from the corresponding refine entry.
product_idstringYes when scope is "product"Echoes product_id from the corresponding refine entry.
proposal_idstringYes when scope is "proposal"Echoes proposal_id from the corresponding refine entry.
statusstringYes"applied": ask fulfilled. "partial": partially fulfilled. "unable": could not fulfill.
notesstringNoSeller explanation. Recommended when status is "partial" or "unable".
{
  "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:
{
  "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:
{
  "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:
{
  "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 a commit, not a refinement — it transitions a draft proposal to committed with an expires_at hold window. 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.
# ✅ 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 before sending.

Error handling

Error CodeWhenResolution
PRODUCT_NOT_FOUNDOne or more referenced product IDs are unknown or expiredRemove invalid IDs and retry, or re-discover with a brief request
PROPOSAL_EXPIREDA referenced proposal ID has passed its expires_atRe-discover with a new brief or wholesale request
PROPOSAL_NOT_FOUNDThe 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_UNSUPPORTEDrefine[] carried multiple action: 'finalize' entries but the seller cannot guarantee atomic multi-proposal commitSequence single-proposal finalize calls (one finalize per get_products call)
INVALID_REQUESTrefine provided in brief or wholesale mode, empty refine array, or missing required fieldsCheck 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 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
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