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

# Get products across storefronts

> Use one canonical AdCP-shaped request to retrieve, evaluate, refine, and buy proposals across your connected storefronts

`get_products` is the canonical product door for buyer agents. Send one
AdCP-shaped request to selected storefronts—or all storefronts connected to an
advertiser—and receive one bounded canonical page as `products[]` plus optional
sibling `proposals[]`. Every identity remains qualified by its originating
storefront.

Apostra can optionally evaluate valid seller proposals before returning
them. You supply plain-language operating instructions. A managed model chooses
`accept`, `reject`, or `refine`, can attach buyer-owned enrichment, and can rank
the accepted cohort across sellers.

<Info title="AdCP at every seller edge; Apostra above it">
  AdCP remains a one-buyer-to-one-seller protocol. Apostra is the
  many-to-one buyer layer: it fans out the canonical request, validates each
  seller independently, qualifies identities, and returns a bounded aggregate
  page. Sellers do not see one another or receive an Apostra-specific
  request.
</Info>

## One protocol request, marketplace scale

```mermaid theme={null}
flowchart LR
  B[Buyer agent] -->|canonical request + ext.interchange| I[Apostra get_products]
  I -->|1:1 AdCP get_products| S1[Storefront A]
  I -->|1:1 AdCP get_products| S2[Storefront B]
  I -->|1:1 AdCP get_products| SN[Storefront N]
  S1 --> I
  S2 --> I
  SN --> I
  I -->|bounded canonical page + qualified IDs| B
```

The scaling layer does four things without forking the protocol:

* **Bilateral calls stay canonical.** Buying mode, brief, filters, proposals,
  and refine are ordinary AdCP semantics at each seller edge.
* **Identity survives aggregation.** `sf1:` product IDs and `sfp1:` proposal
  IDs prevent cross-storefront collisions and round-trip into purchase.
* **Slow sellers do not erase fast sellers.** Revisioned replacement snapshots
  expose seller progress while other agents remain pending. Candidates become
  selectable only after their complete seller/source identity is persisted.
* **Responses stay agent-sized.** `pagination.max_results` bounds candidates;
  proposal graphs remain whole, and the cursor is frozen only when the
  execution is complete.

## Which sellers are searched for an advertiser

Discovery for an advertiser searches only the media partners turned on for that
advertiser: partners selected automatically for its markets and channels,
partners included for the whole account, and partners turned **On** for that
advertiser. Media partners that are off for the advertiser are not contacted
and do not appear in `storefront_results[]`. They are outside the search, not
failed sellers.

When the request names no storefronts, `ext.interchange.storefronts_not_searched`
lists the media partners left out for the advertiser: each one's
`storefront_id`, `storefront_name` and `reason`. `NO_AUTOMATIC_MATCH` means it
does not match the advertiser's markets and channels, `ADVERTISER_DISABLED`
means it is turned off for the advertiser, `ACCOUNT_ALWAYS_EXCLUDE` means it is
excluded for the whole account, and `HARD_INELIGIBILITY` means it cannot
transact with you at all. At most 10 are listed; `total` counts every one. When
you can turn any of them on, `suggestion` says how, once. The `get_products`
text summary names them the same way. Each media partner appears once, even
when it is reached through more than one sales agent. The list is reported only
for an advertiser your account itself owns; for any other advertiser, such as
one another organization delegated to yours, it is omitted rather than
guessed.

A media partner that is off also changes the rest of the response in these
cases:

* **You named it.** When the request names specific storefronts, or refines a
  product or proposal from one, a named media partner that is off returns a
  `storefront_results[]` row with status `completed`, no products, and an
  `outcome` of kind `action_required` with `reason_code`
  `advertiser_activation`. Its `message` says why it was not searched and its
  `suggestion` says where to turn it on. A partner turned off for the
  advertiser is turned on in the advertiser's view of Media Partners (open the
  media partner, choose **Controls**, set **Availability** to **On**). A
  partner excluded for the whole account is changed in the account view, from
  **Always exclude** to **Default** or **Always include**.
* **No media partner is on.** When none is turned on for the advertiser,
  discovery searches nothing and `guidance` says so, with the same path to
  turn one on.
* **Otherwise**, an empty result's `guidance` also reports how many media
  partners were left out because they are not turned on for the advertiser.

A media partner that cannot transact at all is reported as `skipped` when you
named it; turning it on does not change that.

A media partner that is still finishing go-live is different. Some managed ad
servers need the media partner to pass a no-spend certification, such as a
test write to the ad server, before it takes its first booking. Until it
passes, the media partner is searched like any other: its products appear in
results, and you can add them to a buy you are planning. Creating or executing
a media buy with it is refused with `ACCESS_DENIED` and a message saying the
media partner has not finished going live. There is nothing to turn on; the
media partner finishes the certification on its side, and booking opens once
it passes.

Saved results follow the same rule. When a media partner is turned off for the
advertiser after a search, reading that saved discovery no longer returns its
products; an empty page's `guidance` says how many saved products were removed
for that reason. Adding one of its products to a buy, directly or by applying a
proposal, is refused with `ACCESS_DENIED` and a message saying where to turn
the media partner back on.

## Read each seller result honestly

`ext.interchange.storefront_results[].outcome` is present when a seller was
queried through its own agent, or when a seller you named was not searched. It
is one buyer-safe terminal outcome: `returned_products` (with its count),
`no_matching_products`, `no_inventory_for_request`, `action_required` (a named
seller that is off for the advertiser, with its fix), `skipped` (with a safe
reason), or `failed` (with a safe reason). A seller with no active wholesale inventory is
`no_inventory_for_request`, not a failed seller and not a retry instruction.
An empty seller result becomes `no_matching_products` only after that seller
has completed without an error. A seller response with an error and no products
is `failed` when no cached catalog can serve the request, even when the seller
marks its task completed. An incomplete result remains `incomplete` and can
carry a safe seller clarification instead.

When one agent covers several sellers, `outcome` and `filter_result` are
omitted because that agent-level result cannot be attributed honestly to an
individual seller. Those rows carry product-derived counts and
`property_coverage[]` only. Agent-level outcomes for that case are tracked in
AI-10300.

Returned products also produce `property_coverage[]`: each property domain and
the number of returned products carrying it. Treat this as coverage of the
seller response, not an exhaustive marketplace claim. Each seller row includes
at most 12 property domains, with `property_coverage_total` and
`property_coverage_truncated` when the buyer needs to recognise an abbreviated
list. When a seller reports
filter accounting or unfinished work, `filter_result` says whether no products
matched, filters excluded every candidate, or the result is partial. Seller
diagnostics and operational details are not exposed here.

The advertiser's property exclusion list is applied after seller products are
returned. `ext.interchange.excluded_product_count` is the total number removed,
and each storefront row exposes its own
`storefront_results[].excluded_product_count`. Each seller row also has a
bounded `excluded_products[]` list with the removed product ID, name, and the
matching domain or domains. These domains are the intersection with the
buyer's own exclusion list, never the rest of that list. If
`excluded_products_truncated` is true, use the count as the complete total and
describe the listed products as a bounded sample. If exclusion removes every
completed candidate, read the response guidance and report that **N products
from M sellers were removed because they included properties on the
advertiser's exclusion list**. If a legacy candidate has no seller identity,
the total still includes it but no seller row does; report the removed-product
count without inventing a seller count. Do not call either outcome "no matching
inventory".

<Note title="Fan-out breadth and response pagination are separate">
  `get_products` does not stop after the first 10 storefronts or choose a final
  cohort by response speed. When no storefront selector is supplied (or
  `storefronts: "all_connected"` is explicit), Apostra resolves every
  storefront in the buyer's eligible discovery scope and queries each backing
  sales agent once. An ordinary provisional revision contains only sellers that
  have answered so far and returns `pagination.has_more: false` with no
  `pagination.cursor`; poll the same `execution_id` with `since_revision`
  until `results_complete: true`. Only then follow `pagination.cursor` until
  `has_more: false` to consume the complete candidate set. This enforces the
  stable-cursor contract: incomplete ordinary results are replacement snapshots,
  not pages to append. `pagination.max_results` limits each completed response
  page, not the seller fan-out. The legacy Discover Products `groupLimit` and
  `groupOffset` fields do not apply to this surface.
</Note>

If the buyer's absolute wait budget ends while Apostra is still assembling the
canonical page (for example, managed evaluation is still running), Apostra
returns the latest persisted replacement snapshot when one exists. That page
keeps the products and storefront results from sellers that already answered,
sets `results_complete: false`, and names the remaining sellers as pending.
The requested processing mode is `degraded`: evaluation requests mark
`ext.interchange.evaluation.evaluated: false`, while screening requests mark
`ext.interchange.screening.evaluated: false`; those candidates were passed
through without the requested instructions. The usual `max_results` and its
server-marked deadline-snapshot pagination cursor still apply to that
replacement page. If no seller snapshot
has landed yet, the same incomplete response is an empty page and says so
without claiming that a seller failed to answer.

For a pure pass-through storefront, a country
filter also narrows the storefront's internal source fan-out after the staged
routing flag is enabled.
Apostra sends the request only to an active source whose own standard
`primary_channels` and `primary_countries` match the requested coverage. A
source with missing or malformed primary coverage is unknown and excluded from
filtered fan-out after activation; before activation, legacy fan-out is unchanged.
Declarations from two different sources are never combined to manufacture a
match. An unfiltered request remains storefront-wide.

Apostra rejects ambiguous seller-specific controls such as conditional
catalog versions on the aggregate surface. Query that seller's AdCP endpoint
directly when you need a seller-specific conditional request.

### Use canonical filter names

Apostra rejects known cross-operation fields inside `filters` before contacting
any seller. Use `filters.countries` to narrow products by country coverage. Put
delivery targeting such as `geo_countries` under `targeting_overlay`, not
`filters`. `excluded_content_categories` is not a supported `get_products`
filter and must be omitted.

Canonical filter fields from the AdCP versions supported by Apostra remain
available. Put seller-specific filter criteria under a vendor namespace in
`filters.ext`, for example `filters.ext.example_seller`. These unsupported
fields return a validation error that names the field and how to correct it.

## Canonical options from older seller formats

### Buyers

When a seller still uses named legacy formats, `get_products` can return the
seller's explicit mapping as a URL-free canonical `format_options[]` entry.
Buyer integrations do not need to change: existing canonical options and
legacy `format_ids[]` keep their current shapes.

When a buyer selects one of these canonical options, Apostra retains the
seller's exact publisher-scoped route for later `create_media_buy`,
`update_media_buy`, and assigned `sync_creatives` delivery. The route is never
reconstructed from a similar format kind or ID. If the exact seller mapping is
missing, malformed, or inconsistent with the returned option, delivery fails
closed and the buyer should refresh products before retrying.

### Sellers

Sellers opt in one format at a time through the catalog returned by
`list_creative_formats`. Add a `canonical` declaration to the legacy row. If
the option needs canonical parameters, include `canonical_parameters` on the
same row and use the same kind:

```json theme={null}
{
  "format_id": {
    "agent_url": "https://seller.example",
    "id": "seller_banner",
    "width": 300,
    "height": 250
  },
  "name": "Seller banner",
  "canonical": { "kind": "image" },
  "canonical_parameters": {
    "format_kind": "image",
    "params": { "width": 300, "height": 250 }
  }
}
```

Dimensions and duration in the canonical declaration must agree with the
legacy format reference. Apostra ignores missing, invalid, conflicting,
ambiguous, or unsupported mappings. If a returned product's older format
remains unresolved, the seller result includes details in
`projection.diagnostics`. Apostra never guesses a canonical kind from a
legacy ID or asset list, and one seller's mapping cannot apply to another
seller.

Products may publish canonical `format_options[]` directly or retain exact
legacy references from a shared format catalog. Use the catalog mapping to
replace seller-specific formats that Apostra cannot interpret.

### Product-format resolvability

When canonical-format compliance is enabled for a storefront, Apostra
surfaces a seller-visible blocker only when a format cannot be mapped to an
interpretable declaration. Exact legacy `{agent_url, id}` references remain
supported without a sunset, for both existing and newly connected sources.
Direct URL-free `format_options[]` are accepted but are not required yet. A
source with no persisted product-format evidence receives an evidence-pending
blocker until its initial catalog sync records declarations. Catalog age is a
separate freshness concern: an interpretable declaration does not become
noncompliant when its cache entry ages. An unresolved declaration remains
withheld until corrected. For a sales agent on the Agent-supplied path,
each live `get_products` response is a brief-specific observation,
not a complete catalog snapshot. The newest response for each public or buyer-account context
replaces that context's previous compliance observation, so products omitted
from a later brief do not keep the source withheld. A buyer that tries to
purchase a previously returned product still has that exact product's
declaration checked before the buy is forwarded. This
policy audits active external third-party sales-agent sources; managed and
modular inventory sources use their own readiness contracts.

A canonical-format blocker withholds the affected source from discovery and
media-buy admission until the seller replaces the unresolved custom reference
with an exact shared-catalog reference or an interpretable custom canonical
declaration. Other healthy sources in the storefront continue selling. If no
other source can serve, the storefront remains transaction-ineligible.

## Choose `get_products` or Discover Products

Both surfaces share the same seller fan-out implementation. They are different
buyer projections, not different integrations.

| Use | `get_products` | Discover Products |
| - | - | - |
| Primary contract | Canonical AdCP products and proposals, plus `ext.interchange` orchestration | Apostra discovery-session view |
| Best for | Buyer agents, protocol-compatible workflows, named/all storefront queries, seller proposals, screening/refine, direct purchase continuation | Existing discovery-session integrations that need grouped pagination, summary/budget context, browse, or SSE |
| Intelligence | Optional absolute proposal screen: accept, reject, or seller refine | The same optional screen; no separate ranking brain |
| Result shape | Flat, bounded `products[]` and optional `proposals[]`, with per-storefront progress metadata | `productGroups[]`, discovery summaries, budget context, and plans |
| Durable continuation | `execution_id` → `create_media_buys` → DRAFT campaign cart | `discoveryId` → legacy selection endpoints or the same campaign reconciliation path |

Start with `get_products` for new agent integrations. Use
[Discover Products](/v2/buyer/discovery/tasks/discover-products) when you need
its legacy grouped/session presentation or need to continue an existing
`discoveryId` workflow. Calling both for the same request is normally
unnecessary; Discover Products projects the same raw seller snapshot rather
than running another ranking implementation.

## Surfaces

* REST: `POST /api/v2/buyer/products/query`
* Buyer MCP: `get_products`
* Buyer `api_call`: operation `get_products`

The purchasable continuation is:

* REST: `POST /api/v2/buyer/media-buys/batch`
* Buyer MCP: `create_media_buys`
* Buyer `api_call`: operation `create_media_buys`

## Continue the selected product query

When the buyer selects returned proposals or products, reuse the exact
`ext.interchange.execution_id` and storefront-qualified IDs from that
`get_products` result. Do not call `get_products` or Discover Products again
merely to attach, re-fetch, or reconstruct the selection. A new discovery is a
different snapshot: its candidates may differ or be empty, and it does not
identify the query the buyer selected from. The documented legacy recovery for
an execution that predates its durable accepted-proposal set still requires a
fresh `get_products` call; treat that response as a new selection and ask the
buyer to select from it again.

If an agent host offers the typed `create_media_buys` tool, call it directly.
On a reduced or shared tool catalog where that typed tool is absent but
`buyer_api_call` is available, use `buyer_api_call` with
`operation: "create_media_buys"`. Both tool routes invoke the same continuation
contract. REST clients call the batch endpoint above. If the conversation no
longer contains the execution ID or a qualified selected ID, report the missing
handle and offer a new discovery as recovery; do not describe that new query as
the original attachment.

## Retrieval without evaluation

Omit `ext.interchange.evaluation` to receive valid seller responses without a
managed evaluation:

```json theme={null}
{
  "buying_mode": "wholesale",
  "filters": {},
  "pagination": { "max_results": 50 },
  "ext": {
    "interchange": {
      "advertiser_id": 123,
      "storefronts": "all_connected",
      "wait_mode": "quick"
    }
  }
}
```

Protocol validation always runs. Malformed seller responses, broken proposal
allocations, and invalid identities are rejected before evaluation and do not
consume evaluation work.

### Include products from verified AdCP 2.x sellers

Some sellers still run a verified AdCP 2.x response contract, which predates
the current required `reporting_capabilities` block. Their otherwise valid
products are excluded from the default response because Apostra does not
guess reporting cadence, delay, metrics, or webhook support.

REST and MCP callers can request the separate compatibility response by adding
the following field to `get_products`:

```json theme={null}
{
  "buying_mode": "wholesale",
  "filters": {},
  "ext": {
    "interchange": {
      "advertiser_id": 123,
      "response_contract": "interchange_product_compatibility_v2"
    }
  }
}
```

The response repeats that contract at
`ext.interchange.response_contract`. A projected 2.x product omits
`reporting_capabilities` and explains the omission explicitly:

```json theme={null}
{
  "ext": {
    "interchange": {
      "compatibility": {
        "status": "projected",
        "source_contract": "adcp_2_x",
        "source_adcp_version": "2.5",
        "unavailable_fields": ["reporting_capabilities"]
      }
    }
  }
}
```

Apostra accepts this projection only when protocol negotiation provides a
non-synthetic, seller-declared 2.x version. Unknown, conflicting, synthetic,
or current-version responses still fail closed. The exact seller product
payload—not the buyer-facing projection—is retained for any later purchase.

Omit `response_contract` to keep the original response schema, where every
product requires `reporting_capabilities`. Existing REST and MCP integrations
therefore need no changes. Sellers can remove the compatibility marker from
future discovery by upgrading their declared contract and returning the
current reporting block; no buyer-side account or purchase setting changes.

### Request a smaller product projection

Use the canonical `fields` array when the next step needs only part of each
product. Apostra validates the complete seller response before applying the
projection, then returns only the requested product fields. The qualified
`product_id` and `ext.interchange` provenance remain present so the result can
still be refined or passed to `create_media_buys`.

```json theme={null}
{
  "buying_mode": "wholesale",
  "filters": {},
  "fields": ["product_id", "name", "pricing_options", "delivery_type"],
  "pagination": { "max_results": 10 },
  "ext": {
    "interchange": {
      "advertiser_id": 123,
      "storefronts": "all_connected"
    }
  }
}
```

For this request, unrequested product blocks such as `format_options` and
`reporting_capabilities` are omitted. Omit `fields` when you need the complete
canonical product objects.

## Evaluate and negotiate proposals

Add evaluation instructions when you want Apostra to remove unsuitable
proposals, enrich accepted results, negotiate promising proposals, and
optionally rank the accepted cohort before it reaches your agent:

```json theme={null}
{
  "buying_mode": "brief",
  "brief": "Premium UK sports video with measurable reach",
  "filters": { "countries": ["GB"] },
  "pagination": { "max_results": 50 },
  "ext": {
    "interchange": {
      "advertiser_id": 123,
      "storefronts": "all_connected",
      "evaluation": {
        "instructions": "Accept proposals that fit the brief and include measurable reach. If CPM is above the stated budget, ask for a lower-cost option. Reject proposals without usable measurement.",
        "max_refinement_rounds": 1,
        "max_evaluation_passes": 20,
        "enrichment": true,
        "ranking": {
          "instructions": "Prefer stronger measurement and efficient expected reach."
        }
      }
    }
  }
}
```

The first evaluation pass is absolute:

* **Accept** returns the seller's unchanged proposal and referenced products,
  with optional buyer-owned enrichment.
* **Reject** suppresses the proposal from the normal result.
* **Refine** sends a canonical AdCP refine instruction back to that proposal's
  seller, then evaluates the seller's revised response again.

Apostra never rewrites seller pricing, allocations, availability, or terms.
Any changed proposal must come back from the seller.

Autonomous refinement is bounded by `max_refinement_rounds`, repeated-request
detection, the request deadline, and `max_evaluation_passes`. A proposal still
requesting refinement at the limit is rejected rather than silently accepted.

When `ranking` is present, a separate comparative pass scores accepted
candidates under its optional instructions. The response is ordered by score
and includes a rank and rationale. Provisional rankings replace earlier
snapshots as sellers settle; the final completed revision is frozen.

The platform selects the managed models. This version does not expose provider
model names, customer executable code, or a model picker.

<Note>
  `ext.interchange.screening` remains a deprecated compatibility alias for
  accept, reject, and refine. It does not enable enrichment or comparative
  ranking. Do not send `screening` and `evaluation` together.
</Note>

## Discover Products compatibility

`discover_products` is a convenience view over the same seller fan-out and
evaluation engine. Passing its optional `evaluation` field enables the same
disposition, enrichment, refinement, and ranking behavior, projected into the
Discover response shape. Without `evaluation`, Discover preserves its existing
enrichment, relevance, ordering, and refinement behavior. Existing Discover
clients do not need to migrate to keep their current behavior.

## Proposals and products

Canonical AdCP proposals reference sibling products through allocation
`product_id` values; they do not duplicate complete product objects. Evaluation
resolves that graph automatically. A top-level product that is not allocated by
any proposal remains a proposal-less candidate, principally for wholesale
catalogs.

`pagination.max_results` bounds the combined number of proposals and
proposal-less products. Products referenced by a paged proposal accompany that
proposal and do not consume another candidate slot, so the proposal graph is
never split across pages. Apostra accepts at most 50 allocated products in
one proposal. A response contains at most 100 total proposal and product objects;
when complete proposal graphs reach that limit, the page returns fewer proposal
candidates and the cursor continues from the next whole proposal.

Both IDs are opaque and storefront-qualified:

```json theme={null}
{
  "products": [
    {
      "product_id": "sf1:42:...",
      "ext": {
        "interchange": {
          "storefront_id": "42",
          "source_product_id": "seller-product-7"
        }
      }
    }
  ],
  "proposals": [
    {
      "proposal_id": "sfp1:42:...",
      "allocations": [{ "product_id": "sf1:42:..." }],
      "ext": {
        "interchange": {
          "storefront_id": "42",
          "source_proposal_id": "seller-proposal-9",
          "evaluation": {
            "disposition": "accept",
            "evaluated": true,
            "confidence": 0.86,
            "reasons": ["Fits the brief and measurement requirements"],
            "enrichment": {
              "summary": "Premium UK sports video with measurable reach.",
              "strengths": ["Strong measurement"],
              "concerns": [],
              "labels": ["video", "sports"]
            },
            "ranking": {
              "rank": 1,
              "score": 0.91,
              "rationale": "Best combination of measurement and reach.",
              "provisional": false
            }
          }
        }
      }
    }
  ]
}
```

Pass qualified IDs back unchanged in refine and media-buy creation calls.
When more than one seller or storefront is in scope, Apostra rejects an
item-scoped refine instruction that uses an unqualified product or proposal ID.
Request-scoped refine instructions still apply across the request.

<Warning title="Do not key results by source_product_id">
  Two storefronts can expose the same upstream product and therefore carry the
  same `ext.interchange.source_product_id`, including the same `wh:` value.
  Treat the top-level qualified `product_id` as the identity, and use the
  `storefront_id` and `storefront_name` from that same product when displaying
  provenance. Never merge or relabel results by `source_product_id` alone.
</Warning>

## Stage or buy the result

The `execution_id` in the response is also the durable search/refinement
record used by `create_media_buys`. Choose whole proposals and/or
proposal-less products from that execution. The operation stages them on a
DRAFT campaign—the campaign is the shopping cart and the parent of every
media buy.

Only proposals in the execution's durable accepted set can be selected. Run
`get_products` again before purchasing from an older execution that predates
that set.

Use an existing campaign cart:

```json theme={null}
{
  "productQueryId": "query-id",
  "campaign": { "campaignId": "campaign_123" },
  "mode": "stage",
  "selections": [
    {
      "kind": "proposal",
      "proposalId": "sfp1:42:...",
      "totalBudget": 40000
    },
    {
      "kind": "product",
      "productId": "sf1:57:...",
      "pricingOptionId": "cpm-standard",
      "budget": 10000,
      "bidPrice": 8.5
    }
  ]
}
```

Or provide `campaign.create` with the same required fields as a discovery-mode
campaign (`advertiserId`, `name`, `flightDates`, and `budget`). Apostra
creates the DRAFT campaign and returns its ID. Media buys are never standalone,
even when the caller did not create a campaign first.

* `mode: "stage"` updates the cart and prepares DRAFT media buys without
  contacting sellers.
* `mode: "execute"` performs the same preparation, then sends each resulting
  buy to its originating storefront through ordinary bilateral AdCP
  `create_media_buy` calls.
* `replace: true` replaces the cart selection before applying this batch;
  otherwise selections merge idempotently.

Use `mode: "execute"` when the selected result can be submitted immediately.
When you need to inspect or customize per-buy creatives, dates, pacing, or
optimization goals, use `mode: "stage"`, make those changes on the returned
DRAFT media buys, then call `execute_campaign` or repeat `create_media_buys`
with the same campaign and selections using `mode: "execute"`. Execution
submits the prepared DRAFT directly; it does not rebuild it from the product
query.

One marketplace call may reach several sellers, but the seller calls are not
one atomic transaction. The response reports `mediaBuysExecuted`, `success`,
and per-buy `errors`. Retry the same request after a partial failure; completed
buys are not dispatched again. A completed product query accepts only an
unchanged retry of its recorded batch. To change the selection after execution,
start a new `get_products` query.

See [Create media buys from a product query](/v2/buyer/discovery/tasks/create-media-buys)
for the complete request and response contract.

## Progressive responses

Inside Murph, the runtime retains the original execution and request arguments,
reads cumulative results with `since_revision: 0`, and follows producer-issued
frozen cursors before attaching a selection. Its bounded wait can end with an
incomplete result; that does not mean the query found no products. See
[Murph continuation and attachment](/v2/buyer/discovery/tasks/browse-products#response)
for the wait limit, confirmed retries, and recovery behavior.

Seller responses arrive independently. Each response is a replacement snapshot:

* `revision` increases as sellers settle or a refinement produces a new version;
* `provisional: true` means replace your previous displayed result;
* `pending_agents` identifies sellers still working;
* fresh provisional revisions may contain seller progress but no candidates
  while typed product identity is still being persisted;
* proposals being refined are withheld;
* ordinary incomplete responses set `pagination.has_more: false` and issue no
  cursor; poll the `execution_id` with `since_revision` instead. No stable
  cursor is issued until `results_complete: true`;
* `storefront_results[].message` preserves a seller's explanation for a
  successful empty response, including when readiness prevents that storefront
  from transacting; do not interpret that case as “no matching inventory”;
* `storefront_results[].error: "product_persistence_incomplete"` means the
  storefront answered, but one or more candidates could not be persisted with
  a complete seller/source identity. Those candidates and any proposals that
  allocate to them are withheld. Safely persisted candidates from the same
  execution are still returned; start a new query to retry the withheld
  inventory. If no safe candidate remains, the request returns a retryable
  service-unavailable error instead of an authoritative empty catalog;
* the final cursor is scoped to the execution, revision, evaluation instructions,
  and offsets.

Response speed affects which sellers are visible in an early provisional
revision, not which sellers are retained in the completed execution. Without
comparative ranking, completed products use deterministic storefront-qualified
identity ordering; when `evaluation.ranking` is present, the accepted cohort is
ordered by that ranking instead.

Poll with the returned execution identity:

```json theme={null}
{
  "buying_mode": "brief",
  "ext": {
    "interchange": {
      "execution_id": "query-id",
      "since_revision": 3
    }
  }
}
```

Do not append provisional pages. Replace the prior page with the new revision.

Evaluation is idempotent by candidate version and instruction fingerprint.
Polling an unchanged revision does not rerun or remeter it. A seller's changed
proposal is evaluated as a new candidate version.

## Evaluation metadata and cost

The response reports evaluation separately from seller progress:

```json theme={null}
{
  "ext": {
    "interchange": {
      "evaluation": {
        "enabled": true,
        "status": "complete",
        "model_class": "fast",
        "evaluation_passes": 2,
        "ranking_passes": 1,
        "candidates_evaluated": 17,
        "candidates_ranked": 8,
        "accepted": 8,
        "rejected": 7,
        "refining": 0,
        "refinement_limit_reached": 2,
        "estimated_ius": 3,
        "charged_ius": 0,
        "billing_status": "calibrating"
      }
    }
  }
}
```

One **Proposal Evaluation Pass** evaluates a bounded batch of at most ten
candidates. One **Comparative Proposal Ranking Pass** ranks at most 100 accepted
candidates. We measure these value units rather than provider tokens. Both are
currently calibrating, so `charged_ius` remains `0`. Retrieval without
evaluation reports `billing_status: "included"`.

If managed evaluation is unavailable, valid candidates pass through and the
evaluation status becomes `degraded`; an infrastructure failure does not silently
reject seller supply. Every pass-through candidate is explicitly marked
`ext.interchange.evaluation.evaluated: false` (with `confidence: 0`), and the
response `guidance` says so. Check `evaluated` before treating an accept as a
judgment: `evaluated: false` means your evaluation instructions were **not**
applied to that candidate — re-run the request or apply your own screening
before buying on it.


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