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
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
Passprogressive: 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:
guidance tells you (or your agent) the exact next call:
revision you last saw:
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
resultsCompletenever turns true after roughly your wait budget (default 30s), treat the remainingpendingAgentslikeincompleteAgentsand 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.
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— missingadvertiserId,refinesent withoutdiscoveryId, or malformedflightDates.404 NOT_FOUND—advertiserIdorcampaignIdnot visible to the authenticated account.
Related
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