Skip to main content
POST /api/v2/buyer/discovery/discover-products Fans a brief out to every reachable sales agent and returns grouped products, summary stats, budget context, and any seller proposals. If you omit discoveryId, a new session is created and its ID is returned. Pass an existing discoveryId to re-run or refine in place. This first call does the heavy work (the agent fan-out; descriptive copy is generated in the background); use Browse products to page the cached result cheaply.

Request

Parameters

Response

Persist the returned discoveryId — every subsequent operation references it. When you send refine, the seller’s per-instruction reply comes back under refinementApplied matched by position, each with status of applied, partial, or unable.
A product’s expiresAt (present only when its price is an FX-converted quote) is a hard deadline, not advisory metadata. If you cache products yourself, drop or re-fetch any item once its expiresAt has passed — do not select or apply a proposal against an expired quote; re-run discovery (or browse products) to get a fresh price. Products with no expiresAt are natively priced and can be cached indefinitely (subject to your own freshness needs).
pricingScope tells you whether a product’s price is the seller’s public rate card ("public") or reflects a discount/rate specific to your account ("account"). Absent is equivalent to "public". If you cache discovery results yourself, only share or reuse "public" items across a shared or anonymous cache partition — never store or re-serve an "account" item outside the account it was quoted for.

Progressive delivery

Pass progressive: true and sellers’ answers arrive as they land instead of the response waiting for the slowest storefront. The full exchange, exactly as it happens: 1. Start the discovery. Same request as above, plus the flag:
2. First response arrives within ~2.5 seconds with whichever sellers have already answered. Three fields carry the continuation state, and guidance tells you (or your agent) the exact next call:
3. Poll for newly landed sellers with Browse products, passing the revision you last saw:
Each poll returns only seller groups that landed after that revision, plus the current continuation state:
Groups use replace semantics: a group you already received may be re-delivered at a later revision with more products — replace it by groupId, never append. Polling every 1–2 seconds is appropriate; each poll is a cheap cache read. Prefer a push model over polling? Stream discovery events opens a Server-Sent Events connection that emits the same revision payloads automatically — no poll loop required. Both are the same continuation contract; SSE just delivers it as a stream instead of a series of sinceRevision calls. 4. Stop when resultsComplete is true. The final poll’s response also carries incompleteAgents for any seller that timed out. Two other exits:
  • If resultsComplete never turns true after roughly your wait budget (default 30s), treat the remaining pendingAgents like incompleteAgents and proceed with what you have.
  • The very first response may already be resultsComplete: true (warm caches, all-fast sellers, or a repeat of a recent identical query) — that is the fast-and-complete case, not an error.
A plain browse without sinceRevision always returns the full current result set. pendingAgents (still answering) is distinct from incompleteAgents (timed out or answered partially at the final revision). MCP clients follow the identical flow through the api_call tool: { "operation": "discover_products", "body": { ..., "progressive": true } } then { "operation": "browse_discovery", "pathParams": { "discoveryId": "..." }, "params": { "sinceRevision": 3 } }. progressive is ignored when refine is present — refine responses are terminal-only. MCP clients that pass the protocol’s _meta.progressToken on the discover_products call also receive a notifications/progress message per seller as it answers during the initial call (progressive or not), e.g. “Magnite answered — 11 products (3 of 14)”. Murph defaults plain discover_products calls to progressive quick discovery (progressive: true, waitMode: "quick") so chat results render as storefronts answer. If an MCP caller supplies progressive, waitMode, or waitSeconds, that explicit choice is preserved. Elevated/admin contexts may also receive diagnostics.trace on discovery responses. It contains traceId, spanId, and traceparent for support audits of where discovery time was spent. If one or more storefronts timed out or returned partial results, the response includes incompleteAgents even when debug is not enabled. When retryWithLongerWaitAvailable is true, you can retry the same discovery with waitMode: "long" or a larger waitSeconds value after confirming the buyer wants to wait longer. If retryWithLongerWaitAvailable is false, the response is already at the maximum interactive wait window.

Errors

  • 400 VALIDATION_ERROR — missing advertiserId, refine sent without discoveryId, or malformed flightDates.
  • 404 NOT_FOUNDadvertiserId or campaignId not visible to the authenticated account.
See Errors for the full error contract.

Discovery overview

Sessions, refinement, proposals, and storefronts

Browse products

Page the cached result without a re-run

Stream discovery events

Get progressive revisions pushed over SSE instead of polling

Webhooks

Get progressive revisions pushed to your own endpoint instead

Add products

Build a selection from discovered products

Apply proposal

Accept a seller’s full allocation