Skip to main content

Overview

Product Discovery is the buyer-side loop for finding inventory across one or more sales agents (publishers, exchanges, content owners). You provide a brief — what you’re trying to accomplish, who you’re targeting, when it runs, and how much you can spend — and Scope3 fans the request out to every agent your seat has access to. The agents return products and (when supported) recommended proposals that allocate budget across those products. The flow:
1

Connect private storefronts (optional)

For storefronts that gate inventory, register per-source credentials first — see the Storefront object guide.
2

Run discovery

POST /api/v2/buyer/discovery/discover-products with a brief. Returns a discoveryId plus product groups, agent proposals, and budget context.
3

Browse and refine

Page through results with GET /api/v2/buyer/discovery/{id}/discover-products or iterate by sending refine instructions back to the same discoveryId.
4

Pick products or apply a proposal

Add specific products with POST /api/v2/buyer/discovery/{id}/products or apply a full proposal with POST /api/v2/buyer/discovery/{id}/apply-proposal.
5

Promote to a campaign

Run POST /api/v2/buyer/campaigns/{id}/auto-select-products for hands-off selection, or attach the discovery session’s selected products to a campaign you’ve already set up.
A discovery session is a long-lived workspace identified by discoveryId. You can re-open it, refine results, swap products in and out, and replay proposals without losing context.

Storefront agents in discovery

Discovery fans out to both third-party sales agents and Scope3 storefront agents. A storefront agent has salesAgentId: "storefront-{platform_id}", where platform_id is the storefront’s public platformId slug, and represents the storefront as a buyer-facing route for product discovery and media-buy execution. That ID identifies the storefront surface; it is not a source ID or underlying source agent ID. Buyers call the same discovery endpoint for all agents. They do not need to distinguish between storefronts that compose products from raw inventory and storefronts that passthrough to an upstream inventory source. Composition storefronts return products assembled from active inventory sources and active operating instructions. Passthrough storefronts proxy get_products to an active source and apply storefront metadata and buyer-instruction overlays before results are returned. Buyer instructions are resolved from operator domain, brand domain, and optional country. Use storefrontIds or storefrontNames when you want to restrict discovery to specific storefronts.
Public buyer discovery only includes Storefronts that have passed marketplace review and are listed. A Storefront can be live for known transactions while it is still pending review, but it will not appear in public discovery results or buyer marketplace browsing until an admin lists it.

Murph account analysis for adapter storefronts

Operators can ask Murph to run analyze_account for adapter-routed storefronts with delegated provider credentials. The tool analyzes a connected advertising account through the storefront adapter and can render an interactive account-analysis viewer in compatible MCP clients. Inputs:
FieldTypeNotes
providerstringOptional for self-service. Required when a super admin needs to disambiguate a client storefront. Supported values include amazon, audiostack, google, meta, pinterest, reddit, snap, spotify, and tiktok.
targetCustomerIdintegerSuper-admin-only customer ID whose adapter storefront should be analyzed.
storefrontPlatformIdstringSuper-admin-only public storefront platform ID to analyze instead of targetCustomerId.
accountIdstringOptional upstream provider account ID to request from the connected delegated credential.
The response includes an analysis object returned by the adapter, plus a recommendations[] array for the operator. Adapter implementations may include findings, metrics, account health signals, and other provider-specific details inside analysis. analyze_account is read-only but requires the storefront to be adapter-routed and connected with delegated provider auth. Without a stored delegated credential, Murph returns an auth-required response that includes the provider OAuth setup path. Cross-customer targeting is restricted to SuperAdmins; normal storefront operators can analyze only their own storefront.

Public vs private storefronts

Storefronts fall into two visibility tiers:
TierWho can discoverHow to access
PublicAny authenticated buyerAvailable by default after marketplace review
Private / gatedBuyers with credentials registered for the sourceRegister credentials per inventory source (API key, OAuth, or JWT)
Public discovery is also gated by marketplace review: pending-review and hidden Storefronts are omitted from public buyer discovery even if they are otherwise live. Private storefronts gate inventory behind per-source authentication. Scope3 speaks to all sources - public and private - over the Ad Context Protocol (ADCP); the difference is just whether the source requires the buyer to present credentials before products are returned. The buyer never needs an AAO key directly: AAO compliance gating is handled server-side by Scope3.

Step 1: Connect private storefronts (optional)

If you only need public inventory, skip ahead to Step 2. To unlock private storefronts, register credentials for the relevant inventory source. Three auth types are supported (API key, OAuth, JWT) — the source declares which it requires. Full walkthrough lives in the Storefront object guide; the short version:
curl -X POST \
  'https://api.interchange.io/api/v2/buyer/storefronts/42/sources/agt_premium_ctv/credentials' \
  -H 'Authorization: Bearer scope3_<your_api_key>' \
  -H 'Content-Type: application/json' \
  -d '{ "auth": { "type": "api_key", "token": "<source-issued-token>" } }'
For OAuth-secured sources, request an authorize URL and complete the buyer consent flow — Scope3 hosts the callback at /api/v2/storefront/oauth/callback so AI agents and back-end clients don’t need to handle redirects themselves.
Once credentials are registered for a source, every discovery call from your customer automatically queries it — no per-call configuration needed.

Step 2: Run discovery

POST /api/v2/buyer/discovery/discover-products is the main entry point. You can pass a brief inline or seed the request from an existing campaign with campaignId.

Request

curl -X POST 'https://api.interchange.io/api/v2/buyer/discovery/discover-products' \
  -H 'Authorization: Bearer scope3_<your_api_key>' \
  -H 'Content-Type: application/json' \
  -d '{
    "advertiserId": 12345,
    "brief": "Premium CTV inventory for 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
  }'
FieldTypeDescription
advertiserIdintegerRequired. Resolves the brand reference used to score agent results.
discoveryIdstringReuse an existing session. Required when sending refine.
campaignIdstringSeed brief, flight dates, and budget from a campaign. Inline values override.
briefstring (≤5000 chars)Natural-language search context.
budgetnumberTotal budget in account currency. Used for budget context + proposals.
channelsstring[]display, olv, ctv, social, or video (alias for olv). Defaults to ["display","olv","ctv","social"].
countriesstring[]ISO 3166-1 alpha-2 country codes. Defaults to brand agent countries.
flightDates{ startDate, endDate }ISO 8601 datetimes (e.g. 2026-07-01T00:00:00Z). Filters by agent availability.
publisherDomainstringFilter to a single publisher domain.
pricingModelstringFilter by AdCP pricing model (cpm, cpcv, etc.).
storefrontIdsinteger[]Restrict to specific storefronts (IDs from list_storefronts). Empty array = same as omitted (no request-level filter); falls back to the campaign-level pin if a campaignId is also provided.
storefrontNamesstring[]Restrict to storefronts whose name matches (case-insensitive substring). Empty array = same as omitted; falls back to the campaign-level pin if available.
groupLimitinteger (≤10)Max product groups per page. Default 10.
productsPerGroupinteger (≤15)Max products inside each group. Default 10.
groupOffset / productOffsetintegerPagination cursors.
debugbooleanInclude per-agent ADCP request/response logs in the response.
refinearrayRefinement directives (see Refining). Requires discoveryId.

Response

{
  "discoveryId": "disc_01HZX3YQ7K9R6V3M2P1E0F8B2T",
  "productGroups": [
    {
      "groupId": "ctx_disc_01HZX3-group-0",
      "groupName": "Acme Media Connected TV",
      "description": "Premium CTV across Acme Media's owned-and-operated apps",
      "productCount": 4,
      "totalProducts": 12,
      "hasMoreProducts": true,
      "products": [
        {
          "productId": "prod_acme_ctv_sports",
          "name": "Acme Sports CTV — 30s",
          "channel": "ctv",
          "formatTypes": ["video", "30s"],
          "cpm": 32.5,
          "salesAgentId": "agent_acme_media",
          "salesAgentName": "Acme Media Ad Sales",
          "deliveryType": "guaranteed",
          "briefRelevance": "Lives squarely on Acme Media's sports verticals — adults 25-54 over-index here",
          "pricingOptions": [
            { "pricingOptionId": "po_fixed_30", "pricingModel": "cpm", "isFixed": true, "fixedPrice": 32.5, "currency": "USD" }
          ],
          "estimatedExposures": 7700000
        }
      ]
    }
  ],
  "totalGroups": 18,
  "hasMoreGroups": true,
  "summary": {
    "totalProducts": 142,
    "publishersCount": 18,
    "priceRange": { "min": 4.5, "max": 78.0, "avg": 21.4 }
  },
  "budgetContext": {
    "sessionBudget": 250000,
    "allocatedBudget": 0,
    "remainingBudget": 250000
  },
  "proposals": [
    {
      "proposalId": "proposal_acme_q3_sports",
      "name": "Q3 Sports — premium CTV mix",
      "salesAgentName": "Acme Media Ad Sales",
      "briefAlignment": "Combines guaranteed sports with non-guaranteed sports-adjacent OLV to extend reach",
      "allocations": [
        { "productId": "prod_acme_ctv_sports", "allocationPercentage": 60, "rationale": "Anchors the plan with guaranteed delivery" },
        { "productId": "prod_acme_olv_sports", "allocationPercentage": 40 }
      ],
      "totalBudgetGuidance": { "min": 150000, "recommended": 250000, "max": 400000, "currency": "USD" }
    }
  ]
}
The first call always creates a new session unless you pass discoveryId. Persist the returned discoveryId — every subsequent call references it.
Discovery fans out across all reachable agents in parallel. Slow agents do not block fast ones; agents that fail or are skipped are surfaced under agentResults only when you pass debug: true.Agents whose advertised channel coverage does not overlap with the requested channels are skipped before fanout (no round-trip), and appear in agentResults with a skipReason like Agent does not sell requested channels (supports: display, ctv; requested: social). For agents that respond with an error, skipReason carries the human-readable rejection text from the agent (e.g. "We do not support the list of channels you specified"); prefer it over error when surfacing the reason in a UI. skipReason is agent-controlled content, sanitize before rendering as HTML.
Sellers operating third-party sales-agent inventory sources can inspect the source-side view in Diagnose third-party sales agents.

Step 3: Page through results

Use the GET endpoint to browse the same session without spending another LLM-enriched discovery call. Filters narrow the cached result set in place.
curl -X GET 'https://api.interchange.io/api/v2/buyer/discovery/disc_01HZX.../discover-products?groupOffset=10&publisherDomain=acmemedia&pricingModel=cpm' \
  -H 'Authorization: Bearer scope3_<your_api_key>'
Query parameters mirror the discover request: groupLimit, groupOffset, productsPerGroup, productOffset, publisherDomain, pricingModel, storefrontIds, storefrontNames, debug.

Refining results

Iterate on a previous response by sending refine instructions back to the same discoveryId. Refinements come in three scopes:
{
  "advertiserId": 12345,
  "discoveryId": "disc_01HZX...",
  "refine": [
    { "scope": "request", "ask": "Less display, more video. Drop anything below $10 CPM." }
  ]
}
The agent’s reply to each instruction comes back under refinementApplied (matched by position) with status: "applied" | "partial" | "unable" and an optional explanation.

Step 4: View specific products

Add products you want to evaluate to the session. Each selection records the productId, the salesAgentId it came from, and the group it was discovered in. Optionally pin a budget allocation, pricing option, or bid.
curl -X POST 'https://api.interchange.io/api/v2/buyer/discovery/disc_01HZX.../products' \
  -H 'Authorization: Bearer scope3_<your_api_key>' \
  -H 'Content-Type: application/json' \
  -d '{
    "products": [
      {
        "productId": "prod_acme_ctv_sports",
        "salesAgentId": "agent_acme_media",
        "groupId": "ctx_disc_01HZX3-group-0",
        "groupName": "Acme Media Connected TV",
        "pricingOptionId": "po_fixed_30",
        "budget": 150000
      }
    ]
  }'
For non-fixed pricing, include bidPrice (read it from the product’s pricingOptions[].rate or floorPrice in the discovery response). List the current session selection at any time:
curl -X GET 'https://api.interchange.io/api/v2/buyer/discovery/disc_01HZX.../products' \
  -H 'Authorization: Bearer scope3_<your_api_key>'
Pull a single product’s full detail (including extended specs from the sales agent) with the per-product detail endpoint:
curl -X GET 'https://api.interchange.io/api/v2/buyer/discovery/disc_01HZX.../products/prod_acme_ctv_sports/details?salesAgentId=agent_acme_media' \
  -H 'Authorization: Bearer scope3_<your_api_key>'
Remove products you no longer want:
curl -X DELETE 'https://api.interchange.io/api/v2/buyer/discovery/disc_01HZX.../products' \
  -H 'Authorization: Bearer scope3_<your_api_key>' \
  -H 'Content-Type: application/json' \
  -d '{ "productIds": ["prod_random_display"] }'

Step 5: Apply a proposal

When an agent returns a proposal, you can accept its full allocation in one call instead of adding products one at a time.
curl -X POST 'https://api.interchange.io/api/v2/buyer/discovery/disc_01HZX.../apply-proposal' \
  -H 'Authorization: Bearer scope3_<your_api_key>' \
  -H 'Content-Type: application/json' \
  -d '{
    "proposalId": "proposal_acme_q3_sports",
    "totalBudget": 250000,
    "replace": true
  }'
FieldDescription
proposalIdRequired. ID from the discover-products response.
totalBudgetOptional. Defaults to proposal.totalBudgetGuidance.recommended.
replaceWhen true, clears existing selected products before applying.
The response echoes the applied proposal, the budget actually distributed, the products that were added, and any products from the proposal that could not be matched back to the discovery results (productsSkipped).
productsSkipped is non-empty when an agent’s proposal references a product that has aged out of the cached discovery results. Re-run discover to refresh the session, then re-apply.

Auto-select on a campaign

For agentic / hands-off flows, attach an existing campaign and let Scope3 pick products for you. This wraps discovery + selection in a single call against the campaign’s existing brief, flight dates, and budget.
curl -X POST 'https://api.interchange.io/api/v2/buyer/campaigns/cmp_987654321/auto-select-products' \
  -H 'Authorization: Bearer scope3_<your_api_key>' \
  -H 'Content-Type: application/json' \
  -d '{
    "maxProducts": 5,
    "minBudgetPerProduct": 5000
  }'
The body is optional — omit it for a fully automated first pass. To iterate, send back the same refine directives accepted by discover-products:
{
  "refine": [
    { "scope": "request", "ask": "Drop anything social. Lean into CTV." },
    { "scope": "product", "id": "prod_low_quality", "action": "omit" }
  ],
  "maxProducts": 8
}
The response includes the underlying discoveryId so you can drop into the manual flow at any point to inspect or adjust the selection.

Best practices

  • Lead with the outcome, not just the demographic. Agents score against campaign objective + creative + audience together.
  • Include guardrails that matter: brand-safety needs, format constraints (16:9, :30s), exclusions.
  • Keep briefs under ~500 characters when possible — long briefs are auto-summarized for LLM enrichment but lose nuance.