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

# Discover products

> Run discovery across sales agents and open a session

`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](/v2/buyer/discovery/tasks/browse-products) to page the cached result cheaply.

<Info>
  This is the legacy-compatible grouped/session projection over the same raw
  seller fan-out used by [`get_products`](/v2/buyer/discovery/multi-storefront-get-products).
  It adds `productGroups`, summary/budget context, browse, and SSE semantics; it
  does not run a separate product-ranking brain. Start new buyer-agent
  integrations with `get_products` unless you specifically need this session
  shape or an existing `discoveryId` workflow.
</Info>

## Request

<CodeGroup>
  ```bash curl theme={null}
  curl -X POST https://api.interchange.io/api/v2/buyer/discovery/discover-products \
    -H "Authorization: Bearer $SCOPE3_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "advertiserId": 12345,
      "brief": "Premium CTV inventory for a Q3 sports launch — adults 25-54, US only, video creative",
      "budget": 250000,
      "channels": ["ctv", "olv"],
      "countries": ["US"],
      "flightDates": { "startDate": "2026-07-01T00:00:00Z", "endDate": "2026-09-30T23:59:59Z" },
      "groupLimit": 10,
      "productsPerGroup": 10
    }'
  ```

  ```json Refine an existing session theme={null}
  {
    "advertiserId": 12345,
    "discoveryId": "disc_01HZX3YQ7K9R6V3M2P1E0F8B2T",
    "refine": [
      { "scope": "request", "ask": "Less display, more video. Drop anything below $10 CPM." }
    ]
  }
  ```
</CodeGroup>

## Parameters

| Field                           | Type       | Required | Notes                                                                                                                                                                                                                                                                                    |
| ------------------------------- | ---------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `advertiserId`                  | integer    | Yes      | Resolves the brand reference used to score agent results                                                                                                                                                                                                                                 |
| `discoveryId`                   | string     | No       | Reuse an existing session. **Required when sending `refine`**                                                                                                                                                                                                                            |
| `campaignId`                    | string     | No       | Seed brief, flight dates, and budget from a campaign; inline values override                                                                                                                                                                                                             |
| `proposalCode`                  | string     | No       | Storefront-issued code (e.g. `PRP-XK4A29`). Short-circuits discovery and returns the seller's saved snapshot; other filters are ignored                                                                                                                                                  |
| `brief`                         | string     | No       | Natural-language search context (max 5000 chars)                                                                                                                                                                                                                                         |
| `budget`                        | number     | No       | Total budget in account currency; drives budget context and proposals                                                                                                                                                                                                                    |
| `channels`                      | string\[]  | No       | `display`, `olv`, `ctv`, `social`, or `video` (alias for `olv`). Default `["display","olv","ctv","social"]`                                                                                                                                                                              |
| `countries`                     | string\[]  | No       | ISO 3166-1 alpha-2 codes. Defaults to brand agent countries                                                                                                                                                                                                                              |
| `flightDates`                   | object     | No       | `{ startDate, endDate }`, ISO 8601 datetimes; filters by agent availability                                                                                                                                                                                                              |
| `publisherDomain`               | string     | No       | Filter to a single publisher domain                                                                                                                                                                                                                                                      |
| `pricingModel`                  | enum       | No       | `cpm`, `vcpm`, `cpc`, `cpcv`, `cpv`, `cpp`, or `flat_rate`                                                                                                                                                                                                                               |
| `storefrontIds`                 | integer\[] | No       | Restrict to specific storefronts (max 50). OR-combined with `storefrontNames`                                                                                                                                                                                                            |
| `storefrontNames`               | string\[]  | No       | Restrict by case-insensitive name substring (max 50)                                                                                                                                                                                                                                     |
| `groupLimit`                    | integer    | No       | Max product groups returned on this page (max 10). Default 10. This does not limit which sellers are queried                                                                                                                                                                             |
| `productsPerGroup`              | integer    | No       | Max products per group (max 15). Default 10                                                                                                                                                                                                                                              |
| `groupOffset` / `productOffset` | integer    | No       | Pagination cursors. Default 0                                                                                                                                                                                                                                                            |
| `debug`                         | boolean    | No       | Include per-agent ADCP debug logs for failed agents                                                                                                                                                                                                                                      |
| `waitMode`                      | enum       | No       | `quick` waits up to the interactive default per storefront; `long` waits up to the maximum interactive window. Ask before using `long`                                                                                                                                                   |
| `waitSeconds`                   | integer    | No       | Explicit per-storefront wait in seconds, max 210. Overrides `waitMode`; ask before setting above 30                                                                                                                                                                                      |
| `progressive`                   | boolean    | No       | Return a fast first wave of already-answered sellers (`resultsComplete: false` + `pendingAgents`) while remaining sellers keep landing in the background; follow with [Browse products](/v2/buyer/discovery/tasks/browse-products) using `sinceRevision` until `resultsComplete` is true |
| `refine`                        | array      | No       | Refinement directives (1–100). Requires `discoveryId`                                                                                                                                                                                                                                    |

## Response

```json theme={null}
{
  "discoveryId": "disc_01HZX3YQ7K9R6V3M2P1E0F8B2T",
  "productGroups": [
    {
      "groupId": "ctx_disc_01HZX3-group-0",
      "groupName": "Acme Media Connected TV",
      "productCount": 4,
      "totalProducts": 12,
      "hasMoreProducts": true,
      "products": [
        {
          "productId": "prod_acme_ctv_sports",
          "name": "Acme Media Sports CTV - 30s",
          "channel": "ctv",
          "cpm": 32.5,
          "currency": "USD",
          "salesAgentId": "agent_acme_media",
          "deliveryType": "guaranteed",
          "pricingOptions": [
            { "pricingOptionId": "po_fixed_30", "pricingModel": "cpm", "isFixed": true, "fixedPrice": 32.5, "currency": "USD" }
          ]
        },
        {
          "productId": "prod_acme_ctv_sports_zar",
          "name": "Acme Media Sports CTV - 30s",
          "channel": "ctv",
          "cpm": 601.2,
          "currency": "ZAR",
          "expiresAt": "2026-07-06T00:00:00.000Z",
          "salesAgentId": "agent_acme_media",
          "deliveryType": "guaranteed"
        }
      ]
    }
  ],
  "totalGroups": 18,
  "hasMoreGroups": true,
  "summary": { "totalProducts": 142, "publishersCount": 18, "priceRange": { "min": 4.5, "max": 78.0, "avg": 21.4, "currency": "USD" } },
  "budgetContext": { "sessionBudget": 250000, "allocatedBudget": 0, "remainingBudget": 250000 },
  "incompleteAgents": [
    {
      "agentId": "agent_slow_media",
      "agentName": "Slow Media Storefront",
      "reason": "timeout",
      "waitSeconds": 30
    }
  ],
  "retryWithLongerWaitAvailable": true,
  "proposals": [
    {
      "proposalId": "proposal_acme_q3_sports",
      "name": "Q3 Sports - premium CTV mix",
      "allocations": [
        { "productId": "prod_acme_ctv_sports", "allocationPercentage": 60 },
        { "productId": "prod_acme_olv_sports", "allocationPercentage": 40 }
      ],
      "totalBudgetGuidance": { "min": 150000, "recommended": 250000, "max": 400000, "currency": "USD" }
    }
  ]
}
```

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`.

<Note>
  A product's `expiresAt` (present only when its price is an [FX-converted quote](/v2/concepts/cross-currency)) 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](/v2/buyer/discovery/tasks/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).
</Note>

<Note>
  `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.
</Note>

## 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:

The first page can therefore favor faster sellers, but the 10-group page limit
does not cap the fan-out. Continue through revisions until `resultsComplete` is
true, then use `groupOffset` to page through any groups beyond the first 10.

**1. Start the discovery.** Same request as above, plus the flag:

```json theme={null}
{ "advertiserId": 12345, "brief": "Premium CTV for a Q3 sports launch", "budget": 250000, "progressive": true }
```

**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:

```json theme={null}
{
  "discoveryId": "disc_01HZX3YQ7K9R6V3M2P1E0F8B2T",
  "productGroups": [ { "groupId": "ctx_disc-group-0", "groupName": "Acme Media", "products": [ "..." ] } ],
  "revision": 3,
  "resultsComplete": false,
  "pendingAgents": [
    { "agentId": "agent_slow_media", "agentName": "Slow Media Storefront" },
    { "agentId": "agent_ctv_net", "agentName": "CTV Network" }
  ],
  "guidance": "2 seller(s) still working — call browse_discovery with sinceRevision: 3 for newly landed results."
}
```

**3. Poll for newly landed sellers** with [Browse products](/v2/buyer/discovery/tasks/browse-products), passing the `revision` you last saw:

```bash theme={null}
curl "https://api.interchange.io/api/v2/buyer/discovery/disc_01HZX3.../discover-products?sinceRevision=3" \
  -H "Authorization: Bearer $SCOPE3_API_KEY"
```

Each poll returns **only seller groups that landed after that revision**, plus the current continuation state:

```json theme={null}
{
  "productGroups": [ { "groupId": "ctx_disc-group-2", "groupName": "CTV Network", "products": [ "..." ] } ],
  "revision": 5,
  "resultsComplete": false,
  "pendingAgents": [ { "agentId": "agent_slow_media", "agentName": "Slow Media Storefront" } ],
  "guidance": "1 seller(s) still working — call browse_discovery with sinceRevision: 5 for newly landed results."
}
```

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](/v2/buyer/discovery/tasks/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_FOUND` — `advertiserId` or `campaignId` not visible to the authenticated account.

See [Errors](/v2/reference/errors) for the full error contract.

## Related

<CardGroup cols={2}>
  <Card title="Discovery overview" href="/v2/guides/discovery" icon="rocket">
    Sessions, refinement, proposals, and storefronts
  </Card>

  <Card title="Browse products" href="/v2/buyer/discovery/tasks/browse-products" icon="layer-group">
    Page the cached result without a re-run
  </Card>

  <Card title="Stream discovery events" href="/v2/buyer/discovery/tasks/events" icon="tower-broadcast">
    Get progressive revisions pushed over SSE instead of polling
  </Card>

  <Card title="Webhooks" href="/v2/buyer/webhooks" icon="webhook">
    Get progressive revisions pushed to your own endpoint instead
  </Card>

  <Card title="Add products" href="/v2/buyer/discovery/tasks/add-products" icon="plus">
    Build a selection from discovered products
  </Card>

  <Card title="Apply proposal" href="/v2/buyer/discovery/tasks/apply-proposal" icon="wand-magic-sparkles">
    Accept a seller's full allocation
  </Card>
</CardGroup>
