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

# 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.apostra.com/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_talpa",
      "agentName": "Talpa Agent",
      "storefronts": [{ "storefrontId": "storefront_talpa", "storefrontName": "Talpa" }],
      "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:

In an interactive surface, keep this as one live result view: render products as
they land, label provisional cross-storefront totals “so far,” and replace a
re-delivered group by `groupId`. Defer price comparisons and recommendations
until the result is complete, unless the buyer explicitly continues with the
current subset. Status copy should use neutral storefront names and observed
states such as “still working” or “did not respond within 30 seconds.” Never
infer that a country, region, nationality, platform, or seller group is slow.

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,
  "progress": { "respondedStorefrontCount": 3, "totalStorefrontCount": 5, "waitSeconds": 30 },
  "pendingAgents": [
    { "agentId": "agent_talpa", "agentName": "Talpa Agent", "storefronts": [{ "storefrontId": "storefront_talpa", "storefrontName": "Talpa" }] },
    { "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.apostra.com/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,
  "progress": { "respondedStorefrontCount": 4, "totalStorefrontCount": 5, "waitSeconds": 30 },
  "pendingAgents": [ { "agentId": "agent_talpa", "agentName": "Talpa Agent", "storefronts": [{ "storefrontId": "storefront_talpa", "storefrontName": "Talpa" }] } ],
  "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>

  <Card title="Media Partners" href="/v2/buyer/storefronts/overview" icon="store">
    What a product's `ext.scope3_execution_readiness` candidates mean and when they surface
  </Card>
</CardGroup>
