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

# get_products

> get_products task — discover advertising inventory in AdCP using natural language campaign briefs or structured filters. Returns matched products with pricing and formats.

Discover available advertising products based on campaign requirements using natural language briefs or structured filters.

<Warning>
  `get_products` is deprecated for new integrations in AdCP 3.2, but remains fully supported throughout 3.x. Use [`list_products`](/dist/docs/3.2.0-beta.0/media-buy/task-reference/list_products), [`request_proposals`](/dist/docs/3.2.0-beta.0/media-buy/task-reference/request_proposals), [`refine_proposals`](/dist/docs/3.2.0-beta.0/media-buy/task-reference/refine_proposals), or [`decline_proposals`](/dist/docs/3.2.0-beta.0/media-buy/task-reference/decline_proposals). Its `idempotency_key` remains optional. Split-task retries keep the same tool name; they are not cross-name replays of this compatibility facade.
</Warning>

<Info>
  **Why this shape.** Targeting, pricing, and curation are folded into one round-trip — the brief drives discovery, the publisher curates against it, and `pricing_options` carry firm prices the buyer commits against via `pricing_option_id`. We rejected a separate `get_price_quote` step between products and buy creation: it splits one expert decision into two underspecified ones and breaks the brief→curation contract. Iteration is `buying_mode: "refine"` with a typed change array — not a new task. → [Design principle: the brief drives discovery](/dist/docs/3.2.0-beta.0/protocol/design-principles#3-the-brief-drives-discovery-targeting-is-an-input-not-a-step).
</Info>

**Authentication**: Optional (returns limited results without credentials)

**Response Time**: \~60 seconds (AI inference with back-end systems)

**Request Schema**: [`/schemas/3.2.0-beta.0/media-buy/get-products-request.json`](https://adcontextprotocol.org/schemas/3.2.0-beta.0/media-buy/get-products-request.json)
**Response Schema**: [`/schemas/3.2.0-beta.0/media-buy/get-products-response.json`](https://adcontextprotocol.org/schemas/3.2.0-beta.0/media-buy/get-products-response.json)

## Quick Start

Discover products with a natural language brief:

<CodeGroup>
  ```javascript JavaScript theme={null}
  import { testAgent } from '@adcp/sdk/testing';
  import { GetProductsResponseSchema } from '@adcp/sdk';

  const result = await testAgent.getProducts({
    idempotency_key: '550e8400-e29b-41d4-a716-446655441000',
    buying_mode: 'brief',
    brief: 'Premium athletic footwear with innovative cushioning',
    brand: {
      domain: 'acmecorp.com'
    }
  });

  if (!result.success) {
    throw new Error(`Request failed: ${result.error}`);
  }

  // Validate response against schema
  const validated = GetProductsResponseSchema.parse(result.data);
  if (validated.status === 'rejected') {
    console.log(`Seller declined the brief: ${validated.reason}`);
    for (const suggestion of validated.suggestions ?? []) {
      console.log(`- ${suggestion}`);
    }
  } else {
    console.log(`Found ${validated.products.length} products`);

    // Access validated product fields
    for (const product of validated.products) {
      console.log(`- ${product.name} (${product.delivery_type})`);
      console.log(`  Formats: ${product.format_options.map(option => option.format_kind).join(', ')}`);
    }
  }
  ```

  ```python Python theme={null}
  import asyncio
  from adcp.testing import test_agent

  async def discover_products():
      result = await test_agent.simple.get_products(
          idempotency_key='550e8400-e29b-41d4-a716-446655441001',
          buying_mode='brief',
          brief='Premium athletic footwear with innovative cushioning',
          brand={
              'domain': 'acmecorp.com'
          }
      )
      if result.status == 'rejected':
          print(f"Seller declined the brief: {result.reason}")
          for suggestion in result.suggestions or []:
              print(f"- {suggestion}")
      else:
          print(f"Found {len(result.products)} products")

  asyncio.run(discover_products())
  ```

  ```bash CLI requires-env=ADCP_AUTH_TOKEN theme={null}
  uvx adcp \
    https://test-agent.adcontextprotocol.org/sales/mcp \
    get_products \
    '{"idempotency_key":"550e8400-e29b-41d4-a716-446655441002","buying_mode":"brief","brief":"Premium athletic footwear with innovative cushioning","brand":{"domain":"acmecorp.com"}}' \
    --auth $ADCP_AUTH_TOKEN
  ```
</CodeGroup>

### Using Structured Filters

Use structured filters for hard product requirements such as channel, delivery type, format, currency, and reporting support. Filters are valid and have the same exclusion semantics in `brief`, `wholesale`, and `refine` modes; the mode changes curation and lifecycle behavior, not whether filters apply. Concrete delivery targeting belongs in `targeting_overlay`; future selectable targeting belongs in `required_overlay_support`.

For every deterministic predicate in `filters`, every returned product MUST
satisfy that predicate according to the field's documented match semantics.
Sellers MUST exclude non-matching products in all three buying modes; they MUST
NOT accept a schema-valid filter and return the unfiltered curated set, wholesale
feed, or refinement result. A filter can validly produce the same result as an
unfiltered request, so conformance is established from the returned products'
membership and field values—not merely by requiring two responses to differ.
Natural-language relevance to a `brief` remains a curation judgment and is not
part of this deterministic filter rule.

<CodeGroup>
  ```javascript JavaScript theme={null}
  import { testAgent } from '@adcp/sdk/testing';

  const result = await testAgent.getProducts({
    buying_mode: 'brief',
    brief: 'Premium connected-TV inventory for a national launch',
    brand: {
      domain: 'acmecorp.com'
    },
    filters: {
      channels: ['ctv'],
      delivery_type: 'guaranteed',
      standard_formats_only: true
    }
  });

  if (result.success && result.data) {
    console.log(`Found ${result.data.products.length} guaranteed CTV products`);
  }
  ```

  ```python Python theme={null}
  import asyncio
  from adcp.testing import test_agent

  async def discover_with_filters():
      result = await test_agent.simple.get_products(
          buying_mode='brief',
          brief='Premium connected-TV inventory for a national launch',
          brand={
              'domain': 'acmecorp.com'
          },
          filters={
              'channels': ['ctv'],
              'delivery_type': 'guaranteed',
              'standard_formats_only': True
          }
      )
      print(f"Found {len(result.products)} guaranteed CTV products")

  asyncio.run(discover_with_filters())
  ```
</CodeGroup>

## Request Parameters

| Parameter                   | Type                                                 | Required    | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| --------------------------- | ---------------------------------------------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `idempotency_key`           | string                                               | No          | Optional key for retry-safe use of the 3.x compatibility facade (16–255 characters; letters, digits, `_`, `.`, `:`, and `-`). When supplied, reuse the same key only when retrying the exact initial request. Poll the resulting task through [`get_task_status`](https://adcontextprotocol.org/schemas/3.2.0-beta.0/protocol/get-task-status-request.json) or MCP task APIs; each new polling request carries its own fresh key. Use a new key whenever any request parameter changes.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `buying_mode`               | string                                               | Yes         | `"brief"`, `"wholesale"`, or `"refine"`. `"brief"`: publisher curates products from the brief. `"wholesale"`: raw product feed access for buyer-directed targeting, `brief` must not be provided. `"refine"`: iterate on products and proposals from a previous response using the `refine` array of change requests. v3 clients MUST include `buying_mode`. Sellers receiving requests from pre-v3 clients without `buying_mode` SHOULD default to `"brief"`. **Timing semantics:** `"wholesale"` is a wholesale product feed read — sellers SHOULD return a synchronous response and MUST NOT route a `"wholesale"` request through the async/Submitted arm. Partial completion is signalled via [`incomplete[]`](#incomplete-array), not a task handoff. `"brief"` and `"refine"` MAY complete synchronously OR MAY return a `Submitted` envelope when curation requires upstream-system queries or HITL review the seller cannot complete inside `time_budget`. Buyers needing predictable fast wholesale product feed access MUST use `"wholesale"`. |
| `brief`                     | string                                               | Conditional | Natural-language campaign goals, context, preferences, and requirements without a structured representation. Required when `buying_mode` is `"brief"`. Must not be provided when `buying_mode` is `"wholesale"` or `"refine"`. Explicit hard requirements remain binding, but buyers SHOULD use an available structured field instead. When a seller's structured interpretation of hard prose materially affects eligibility, pricing, or forecasting, the response MUST confirm it in `targeting_resolution.brief_targeting`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `refine`                    | [Refine\[\]](#refine-array)                          | Conditional | Array of change requests for iterating on products and proposals. Required when `buying_mode` is `"refine"`. Must not be provided when `buying_mode` is `"brief"` or `"wholesale"`. See [Refine array](#refine-array) below.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `brand`                     | BrandRef                                             | No          | Brand reference (domain + optional brand\_id). Resolved to full identity at execution time.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `account`                   | AccountRef                                           | No          | Account reference for account-specific pricing. Returns products with pricing from this account's rate card.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `catalog`                   | [Catalog](/dist/docs/3.2.0-beta.0/creative/catalogs) | No          | Catalog of items the buyer wants to promote. The seller matches catalog items against its inventory and returns products where matches exist. Requires `brand`. See [Catalog discovery](#catalog-discovery) below.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `filters`                   | Filters                                              | No          | Offer filters such as channel, format, delivery type, dates, budget, currency, and reporting support. They decide which products may be returned but do not become package delivery targeting. Valid in `brief`, `wholesale`, and `refine`; non-matching products are excluded in every mode. Legacy targeting-like fields remain accepted during migration but are deprecated.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `targeting_overlay`         | TargetingOverlay                                     | No          | Concrete delivery constraints the buyer expects to carry into [`create_media_buy`](/dist/docs/3.2.0-beta.0/media-buy/task-reference/create_media_buy). Prefer this over equivalent brief prose: it is compact, deterministic, and avoids lossy extraction. Returned products, pricing, and forecasts MUST account for the effective targeting. A product that cannot honor the request exactly is omitted or returned with sparse Product `targeting_resolution.modifications`; absence of Product resolution confirms exact acceptance of the structured overlay for that product.                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `required_overlay_support`  | TargetingOverlayRequirements                         | No          | Buyer minimums for targeting dimensions whose values will be supplied on packages later. This requests selectable capability, not current values, value-specific availability or forecasts, and not one product per value. Requirement `true` matches support `true` or any valid support object; an object requirement matches `true` or a containing support object. Unrequested fields and numeric seller limits do not participate. Missing or unknown requirements exclude the product.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `fields`                    | string\[]                                            | No          | Specific product fields to include for lightweight discovery. `product_id` and `name` are always included. Request `audience_evidence` for the full evidence payload. Projection never suppresses required decision readback: modified products include `targeting_resolution` and `expires_at`; a request with `required_overlay_support` receives `overlay_support`; and when an audience-evidence policy affects inclusion or ranking, the seller returns `audience_evidence_selections`. When requesting signal metadata, buyers SHOULD request `included_signals` for bundled/planned signals and the `signal_targeting_*` fields for package-level selection.                                                                                                                                                                                                                                                                                                                                                                                       |
| `property_list`             | PropertyListRef                                      | No          | Deprecated discovery-only property filter. Use `targeting_overlay.property_list` for a concrete delivery constraint or `required_overlay_support.property_list` when the list will be selected later.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `pagination`                | PaginationRequest                                    | No          | Cursor-based pagination to cap returned `products[]` in curated/refined responses or walk wholesale product feeds (see below)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `if_wholesale_feed_version` | string                                               | No          | Opaque `wholesale_feed_version` token from a prior wholesale-mode `get_products` response from this agent. Only valid with `buying_mode: "wholesale"`. Version scope excludes `pagination.cursor`: `public` is keyed by (agent, `buying_mode`, `filters`, `targeting_overlay`, `required_overlay_support`, deprecated `property_list`, `catalog`); `account` adds account identity. See [Wholesale feed versioning](#wholesale-feed-versioning).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `if_pricing_version`        | string                                               | No          | Opaque `pricing_version` token from a prior response. MUST only be sent together with `if_wholesale_feed_version`. Evaluation order: `if_wholesale_feed_version` mismatch → full payload; `if_wholesale_feed_version` matches but `if_pricing_version` mismatches → full payload (so the buyer sees updated `pricing_options`); both match → seller MAY return `unchanged: true`. Sellers that don't track pricing separately ignore this.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `time_budget`               | Duration                                             | No          | Maximum time the buyer will commit to this request. The seller returns the best results achievable within this budget and does not start processes (human approvals, expensive external queries) that cannot complete in time. When omitted, the seller decides timing. Example: `{"interval": 30, "unit": "seconds"}`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `push_notification_config`  | PushNotificationConfig                               | No          | Optional webhook channel for async terminal completion/failure notifications on `brief` / `refine` curated discovery. `submitted` responses with a `task_id` remain pollable through [`get_task_status`](https://adcontextprotocol.org/schemas/3.2.0-beta.0/protocol/get-task-status-request.json) (legacy `tasks/get`) whether or not this field is present. If the request includes this field and the seller returns `submitted`, the seller MUST deliver at least the terminal completion/failure notification to the configured webhook; intermediate progress notifications are MAY. If the seller cannot honor the webhook channel, it MUST reject the request with a structured error instead of silently accepting. Ignored for `wholesale`; sellers MUST NOT route wholesale reads through the Submitted arm because this field is present.                                                                                                                                                                                                     |

<Note>
  ### Targeting-aware discovery

  #### Brief versus structured targeting

  Anything that can be expressed in a structured request field SHOULD be. For
  example, put “US only” and “ages 18–44” in `targeting_overlay`, not just in the
  brief. Structured values use fewer tokens, can be validated by code, and do not
  lose meaning through natural-language extraction. Keep the brief for campaign
  goals, semantic audience descriptions, preferences, and requirements for which
  AdCP has no structured field.

  Hard requirements remain hard wherever they appear. A seller MUST apply an
  explicit hard targeting requirement stated only in a brief. When the seller's
  structured interpretation materially affects product eligibility, pricing, or
  forecasting, it MUST confirm that shared interpretation once on the response;
  otherwise confirmation remains a best practice:

  ```json theme={null}
  {
    "targeting_resolution": {
      "brief_targeting": {
        "geo_countries": ["US"],
        "demographics": {
          "age": { "min": 18, "max": 44, "include_unknown": false }
        }
      }
    }
  }
  ```

  This is a confirmation of brief-derived targeting, not a reason to echo a
  structured overlay. When `targeting_overlay` already contains those values and
  the seller accepts them exactly, the response omits them. Selecting any product
  from the response accepts the confirmed request-level brief interpretation;
  product-specific alternatives remain sparse Product targeting modifications.

  Only predicates supported by the structured targeting vocabulary can appear in
  `brief_targeting`. Other hard prose requirements remain binding but must be
  confirmed in the seller's natural-language response until AdCP defines a typed
  representation.

  Provide exact targeting once during discovery so product availability, pricing,
  and forecasts already reflect the constraints that will be booked:

  ```json theme={null}
  {
    "buying_mode": "brief",
    "brief": "Premium national video for outdoor enthusiasts",
    "filters": {
      "channels": ["ctv"],
      "delivery_type": "guaranteed",
      "pricing_currencies": ["USD"]
    },
    "targeting_overlay": {
      "geo_countries": ["US"]
    },
    "required_overlay_support": {
      "geo_metros": { "systems": ["nielsen_dma"] },
      "placement_selection": true
    }
  }
  ```

  `targeting_overlay` is real targeting. The seller resolves it through inherent
  product scope or selectable execution, and every returned forecast is scoped to
  the resulting inventory. `required_overlay_support` says that the buyer needs to
  choose DMA and placement values on packages later; it does not request products
  or packages broken out by DMA or placement.

  Request requirements and product support deliberately use different schemas.
  The buyer request contains dimensions and required systems, never seller maxima.
  A requirement value of `true` matches product support `true` or any valid
  support object. An object requirement matches support `true` or a support object
  where every requested boolean is true and every requested array is a subset of
  the corresponding product array. For `geo_places` and `geo_places_exclude`,
  every requested identifier system and country key must exist, and requested
  place-type and catalog-version arrays must be subsets of the corresponding
  product support arrays. Unrequested object fields and numeric seller limits do
  not participate. Missing or unknown requirements do not match.

  Support guarantees the ability to select a value subject to disclosed limits;
  it does not guarantee inventory or forecast every possible value. Forecasts
  returned when only future support was requested describe the product's
  discovery/default scope. Rediscover with concrete `targeting_overlay` values for
  a value-specific forecast. Fixed prices and floors remain binding uniformly
  for supported selections; price guidance remains non-binding. Sellers with
  value-dependent rates must return a concretely targeted configured product,
  split the rate tiers into products, or withhold binding pricing until concrete
  rediscovery. They cannot reinterpret a selected `pricing_option_id` at a new
  price. If a supported selection has no current inventory, creation returns
  [`PRODUCT_UNAVAILABLE`](/dist/docs/3.2.0-beta.0/building/verification/compliance-catalog#error-code-product-unavailable)
  rather than silently substituting or repricing it.

  For ISO subdivisions, known values belong in
  `targeting_overlay.geo_regions` / `geo_regions_exclude` and therefore scope the
  configured product's price and aggregate forecast. Values intentionally chosen
  later use structured `required_overlay_support` independently for inclusion and
  exclusion. Each country requirement uses either `all_values: true` or an exact
  candidate `values` set. A matching Product `overlay_support` must contain every
  requested country and value; product support with `all_values: true` satisfies
  any finite requirement for that country. `all_values` is evaluated against the
  seller's support snapshot when the declaration is issued and does not
  automatically include values introduced by a later catalog revision.
  Configured discovery or refinement
  with the exact overlay is the authoritative value-level preflight—there is no
  separate region resolver task.

  If the seller can honor a structured overlay exactly, the returned product
  omits Product `targeting_resolution`. Request-level brief confirmation appears
  once on the response, not on each product. Every request-specific configured
  product issued through targeting-aware discovery carries `is_custom: true` and
  `expires_at`, including exact products without a resolution echo. The generic
  Product schema still accepts legacy custom products without an expiry. If a
  product offers a different executable structured
  constraint, it contains only the changed paths. Selecting that request-scoped
  `product_id` accepts the disclosed resolution.
</Note>

<Note>
  **Property governance**

  `targeting_overlay.property_list` references a property list created via [`create_property_list`](/dist/docs/3.2.0-beta.0/governance/property/tasks/property_lists#create_property_list) on a property governance agent. Property lists define which publisher properties meet compliance requirements — COPPA-certified sites, sustainability-scored inventory, brand-safe publishers, etc. The legacy top-level `property_list` discovery filter is deprecated.

  To use property list filtering:

  1. Call [`get_adcp_capabilities`](/dist/docs/3.2.0-beta.0/protocol/get_adcp_capabilities) on a property governance agent to discover available `property_features`
  2. Create a property list via `create_property_list` with your feature requirements
  3. Pass the resulting list reference in `targeting_overlay.property_list` so returned products and forecasts reflect the constrained inventory

  The seller-wide `media_buy.execution.targeting.property_list` capability is a
  routing rollup; each returned product must also make the requested constraint
  executable through inherent scope or selectable property targeting. The legacy
  `features.property_list_filtering` capability applies only to the deprecated
  top-level discovery filter. See the [Property Governance overview](/dist/docs/3.2.0-beta.0/governance/property/index) for the full workflow.
</Note>

### Filters Object

Filters decide which offers may be returned; they do not become package
delivery targeting. Exact
audience or inventory eligibility belongs in `targeting_overlay`. When the
buyer needs a targeting dimension but does not know its values yet, use
`required_overlay_support`.

During the compatibility window, sellers may translate a deprecated
targeting-like filter into its overlay equivalent. If both legacy and new forms
are present, they MUST be semantically identical; otherwise the seller rejects
the request with [`INVALID_REQUEST`](/dist/docs/3.2.0-beta.0/building/verification/compliance-catalog#error-code-invalid-request). This rule applies to every deprecated
targeting filter and the legacy top-level `property_list`.

| Parameter                        | Type                                                                                                                                    | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `delivery_type`                  | string                                                                                                                                  | Filter by `"guaranteed"` or `"non_guaranteed"`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `is_fixed_price`                 | boolean                                                                                                                                 | Legacy filter for fixed-price vs auction products. `true` returns options with `fixed_price`; `false` returns options established through `bid_price`. Contingent options such as `revenue_share` match neither value and are omitted whenever this filter is present.                                                                                                                                                                                                                                                                                                   |
| `pricing_structures`             | string\[]                                                                                                                               | Filter by how the payable price is determined: `fixed`, `auction`, or `contingent`. Sellers return only matching `pricing_options`. Use `contingent` to discover `revenue_share`. When combined with `is_fixed_price`, both filters apply.                                                                                                                                                                                                                                                                                                                               |
| `pricing_currencies`             | string\[]                                                                                                                               | Filter by ISO 4217 currencies the buyer can use for the media product transaction (e.g., `["USD"]`). Products match when they offer at least one product-level `pricing_options` entry in one of the requested currencies and any seller-applied or otherwise mandatory product-scoped signal charges are satisfiable in one of those currencies or have no incremental price. Sellers MUST return only matching product `pricing_options` so buyers can select deterministically from discovery. Optional signal or vendor add-on pricing is not pruned by this filter. |
| `format_kinds`                   | string\[]                                                                                                                               | Filter by canonical format kinds                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `format_option_refs`             | FormatOptionRef\[]                                                                                                                      | Filter by exact publisher- or product-scoped canonical format options                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `format_ids`                     | FormatID\[]                                                                                                                             | Deprecated 3.x named-format compatibility filter                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `standard_formats_only`          | boolean                                                                                                                                 | Only return products accepting IAB standard formats                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `min_exposures`                  | integer                                                                                                                                 | Minimum exposures needed for measurement validity                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `start_date`                     | string                                                                                                                                  | Campaign start date in ISO 8601 format (YYYY-MM-DD) for availability checks                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `end_date`                       | string                                                                                                                                  | Campaign end date in ISO 8601 format (YYYY-MM-DD) for availability checks                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `budget_range`                   | object                                                                                                                                  | Budget range to filter appropriate products (see Budget Range Object below)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `countries`                      | string\[]                                                                                                                               | Deprecated. Use `targeting_overlay.geo_countries`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `regions`                        | string\[]                                                                                                                               | Deprecated. Use `targeting_overlay.geo_regions`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `metros`                         | object\[]                                                                                                                               | Deprecated. Use `targeting_overlay.geo_metros` for known values or `required_overlay_support.geo_metros` for future selection.                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `channels`                       | string\[]                                                                                                                               | Filter by advertising channels (e.g., `["display", "ctv", "social", "streaming_audio"]`). See [Media Channel Taxonomy](/dist/docs/3.2.0-beta.0/reference/media-channel-taxonomy)                                                                                                                                                                                                                                                                                                                                                                                         |
| `video_placement_types`          | string\[]                                                                                                                               | Match product metadata when the declared video placement types intersect `instream`, `accompanying_content`, `interstitial`, or `standalone`. Classification only; it does not promise exclusive delivery on a requested type.                                                                                                                                                                                                                                                                                                                                           |
| `audio_distribution_types`       | string\[]                                                                                                                               | Match product metadata when declared audio distribution types intersect the requested values. Classification only; it does not promise exclusive delivery on a requested type.                                                                                                                                                                                                                                                                                                                                                                                           |
| `sponsored_placement_types`      | string\[]                                                                                                                               | Match retail-media product metadata when declared sponsored-placement types intersect the requested values. Classification only.                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `social_placement_surfaces`      | string\[]                                                                                                                               | Match social-product metadata when declared surfaces intersect `feed`, `stories`, `short_video`, `explore`, or `search`. Classification only; exact public-placement inventory uses `targeting_overlay.placement_selection`.                                                                                                                                                                                                                                                                                                                                             |
| `postal_areas`                   | object\[]                                                                                                                               | Deprecated. Use `targeting_overlay.geo_postal_areas` or `required_overlay_support.geo_postal_areas`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `geo_proximity`                  | object\[]                                                                                                                               | Deprecated. Use `targeting_overlay.geo_proximity`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `keywords`                       | object\[]                                                                                                                               | Deprecated. Use `targeting_overlay.keyword_targets`; broad thematic intent remains in `brief`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `signal_targeting`               | SignalTargeting\[]                                                                                                                      | Deprecated. Use `targeting_overlay.signal_targeting_groups` for known selections or `required_overlay_support.signal_targeting_groups` for later selection.                                                                                                                                                                                                                                                                                                                                                                                                              |
| `required_performance_standards` | [PerformanceStandard\[\]](/dist/docs/3.2.0-beta.0/media-buy/advanced-topics/pricing-models#measurement-terms-and-performance-standards) | Filter to products that can meet the buyer's performance standard requirements. Each entry specifies a metric, threshold, and vendor (e.g., "DoubleVerify for viewability at 70% MRC"). Products that cannot meet these thresholds or do not support the specified vendors are excluded.                                                                                                                                                                                                                                                                                 |
| `required_metrics`               | string\[] ([metric vocabulary](/dist/docs/3.2.0-beta.0/media-buy/media-buys/optimization-reporting))                                    | Filter to products whose `reporting_capabilities.available_metrics` is a superset of these metrics — i.e., products that commit to reporting all listed metrics in delivery. Use for capability discovery (e.g., `["completed_views"]` for a CTV CPCV buy). Sellers MUST silently exclude products that cannot meet the list — filter-not-fail; do not return an error. The product's declared `available_metrics` becomes the binding reporting contract carried into the resulting media buy.                                                                          |
| `required_vendor_metrics`        | object\[]                                                                                                                               | Filter to products whose `reporting_capabilities.vendor_metrics` covers vendor-defined metrics (proprietary attention, emissions, panel demographics, brand-lift surveys, etc.). Each entry pins `vendor` (BrandRef) and/or `metric_id` — at least one. Cross-vendor discovery (e.g., "any attention measurement") is the buyer agent's responsibility: resolve which vendors offer a category via the vendors' `brand.json` records, then enumerate them as filter entries. Same filter-not-fail semantics as `required_metrics`.                                       |
| `audience_evidence_requirements` | AudienceEvidenceRequirements                                                                                                            | Apply buyer-authored admissibility or ranking policy to `Product.audience_evidence`. `requirement_mode` distinguishes hard filtering from preference; `evidence_presence` distinguishes evidence-must-exist from apply-when-published.                                                                                                                                                                                                                                                                                                                                   |

### Audience evidence requirements

The reusable `audience_evidence_requirements` object attaches under `filters`, so the same contract applies to natural-language brief curation, wholesale feeds, and refine requests.

```json theme={null}
{
  "filters": {
    "audience_evidence_requirements": {
      "requirement_mode": "required",
      "evidence_presence": "required",
      "accepted_methodologies": ["observed", "declared", "derived", "projected"],
      "excluded_methodologies": ["inferred", "modeled"],
      "accepted_evidence_types": ["measured"],
      "accepted_subject_types": ["individual", "household"],
      "minimum_confidence": 0.8,
      "maximum_age": { "interval": 90, "unit": "days" },
      "methodology_documentation_required": true,
      "independent_attestation_required": true
    }
  }
}
```

Seller behavior is normative:

* `requirement_mode: "required"` excludes a product when it publishes evidence but no item satisfies every constraint. With `evidence_presence: "required"`, a product that publishes no evidence is also excluded. With `when_available`, a product with no evidence remains eligible.
* `requirement_mode: "preferred"` never turns a mismatch into a hard exclusion. Sellers rank products with matching evidence higher and explain the selected evidence when returning or constructing a package.
* When evidence affects product inclusion or rank, the returned product MUST carry the exact matching snapshots in `audience_evidence_selections[]`, with `decision_use` set to `eligibility` or `recommendation`, even if the request's `fields` projection omitted that field. A product admitted because `when_available` found no published evidence has no selection to report.
* Sellers evaluate freshness from `last_updated` at request evaluation time. They do not synthesize missing provider, confidence, methodology, documentation, or attestation data.
* Accepted lists and excluded lists are evaluated together; `excluded_methodologies` and `excluded_providers` always win when the same value appears in both lists.
* `independent_attestation_required` requires `accepted_attestation_issuers` and is satisfied only by one exact reference/evaluation pair. The reference must be published on the evidence, its issuer and claim type must match the buyer allowlists and seller's shared `adcp.attestations` policy, its subject digest must equal the evidence digest, the evaluation must cover that same reference with outcome `verified`, and its action-binding digest must equal the evidence digest. Buyer issuer and claim-type constraints narrow seller policy; they never broaden it.
* Buyers inspect `media_buy.audience_evidence` capabilities first. Sellers MUST NOT silently ignore unsupported hard requirement or presence modes; they return a structured unsupported/invalid-request error. A preferred mode the seller does not support may be omitted by the buyer or explicitly rejected by the seller, but is never misrepresented as applied.

Audience evidence does not populate `targeting_overlay`, `demographic_targeting`, or an age-verification field. If the buyer also needs exact execution, it requests that independently through the corresponding product and package targeting surfaces.

Known place IDs belong in `targeting_overlay`, so the returned product's
availability, pricing, and forecast are scoped to the effective place
constraint. If the buyer will choose IDs later, `required_overlay_support`
asks for the country, identifier system, place type, and optional catalog
version that the product must let the buyer select:

```json theme={null}
{
  "$schema": "/schemas/3.2.0-beta.0/media-buy/get-products-request.json",
  "buying_mode": "brief",
  "brief": "Local video inventory for a municipal services campaign",
  "targeting_overlay": {
    "geo_places": [{
      "country": "NL",
      "system": "geonames",
      "system_version": "2026-05",
      "place_type": "city",
      "values": ["2759794"]
    }]
  },
  "required_overlay_support": {
    "geo_places": {
      "systems": {
        "geonames": {
          "countries": { "NL": ["city"] },
          "system_versions": ["2026-05"]
        }
      }
    }
  }
}
```

The returned Product `overlay_support.geo_places` is binding permission to
supply matching place IDs on packages later, subject to disclosed limits. It
does not guarantee value-specific inventory or preserve an earlier forecast
before the IDs are provided. Fixed prices and floors retain their binding
semantics for every value within declared support; price guidance remains
non-binding. If the eventual values have no current inventory, create returns
[`PRODUCT_UNAVAILABLE`](/dist/docs/3.2.0-beta.0/building/verification/compliance-catalog#error-code-product-unavailable)
without silent substitution or repricing. Inclusion and exclusion permission
are independent; request `geo_places_exclude` separately when exclusions will
be chosen later.

### Placement fields

`get_products` returns product placement data when the seller includes `placements` or the buyer asks for it through `fields`. Placement IDs are publisher-scoped. Product placements should reference the publisher's public `adagents.json` placement declarations with `{publisher_domain, placement_id}` when a publisher declaration exists. Seller-private placement IDs, source/origin details, and delivery-system mappings must stay out of the response.

Each returned placement may carry:

| Field                       | Meaning                                                                                                                                                                                                                                                                                                                                |
| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `placement_id`              | Placement identifier in the publisher namespace. Buyers reference this with `publisher_domain` in `creative_assignments[].placement_refs`; legacy `placement_ids` strings are only unambiguous in single-publisher contexts.                                                                                                           |
| `publisher_domain`          | Domain whose `adagents.json` defines the publisher-referenced placement. New multi-publisher products SHOULD include it. When omitted on legacy products, buyers may interpret `placement_id` relative to the seller agent's own publisher domain.                                                                                     |
| `mode`                      | `targetable` means the buyer may purchase the publisher-scoped placement through `targeting_overlay.placement_selection`. `included` means fixed/default product inventory that cannot be independently selected. Creative routing is permitted only after the placement is purchased.                                                 |
| `video_placement_types`     | Declared video placement types for OLV and other video inventory, using the IAB Tech Lab/OpenRTB 2.6 `video.plcmt` definitions with AdCP-native names. Concrete placements usually declare one value; aggregate placements may declare multiple.                                                                                       |
| `audio_distribution_types`  | Declared audio distribution types for radio, streaming-audio, podcast, gaming, and other audio inventory, using the IAB Tech Lab/OpenRTB 2.6 `audio.feed` definitions with AdCP-native names. Concrete placements usually declare one value; aggregate placements may declare multiple.                                                |
| `sponsored_placement_types` | Declared sponsored-placement types for catalog-driven retail-media inventory. Concrete placements usually declare one value; aggregate placements may declare multiple.                                                                                                                                                                |
| `social_placement_surfaces` | Declared social-placement surfaces for social inventory. Concrete placements usually declare one value; aggregate placements may declare multiple.                                                                                                                                                                                     |
| `format_options`            | Placement-specific canonical creative support. Product-level formats are the upper bound; placement-level formats narrow the effective accepted set for that placement and must not add formats or `locale_policy` language ranges the product does not accept. Deprecated `format_ids` may appear only on the 3.x compatibility path. |

Publishers can authorize sales agents for specific publisher placements using `authorized_agents[].placement_ids` or `authorized_agents[].placement_tags` in `adagents.json`. Sellers should only return publisher-referenced placements they are authorized to sell.

`mode: "targetable"` means the placement may be selected through
`packages[].targeting_overlay.placement_selection`. A placement with
`mode: "included"` is part of the fixed/default product and cannot be selected
independently. A request whose complete selected set exactly equals the
product's complete included set is satisfied inherently and may be echoed on
the booked package. That exact-equality rule applies during discovery, create,
and update and does not require `overlay_support.placement_selection`; partial
selection does. Because `placement_selection` represents the complete purchased
set, a product with any included placement does not declare selectable placement
overlay support; sellers expose a separate selectable product when needed.
`creative_assignments[].placement_refs` only route different creatives across
placements already selected for the package; they do not purchase inventory.

Signal-targeting filter example:

```json theme={null}
{
  "$schema": "/schemas/3.2.0-beta.0/media-buy/get-products-request.json",
  "idempotency_key": "550e8400-e29b-41d4-a716-446655441005",
  "buying_mode": "wholesale",
  "filters": {
    "signal_targeting": [
      {
        "signal_ref": {
          "scope": "data_provider",
          "data_provider_domain": "pinnacle-data.example",
          "signal_id": "auto_intenders"
        },
        "value_type": "binary",
        "value": true,
        "targeting_mode": "include"
      }
    ]
  },
  "fields": [
    "product_id",
    "name",
    "included_signals",
    "signal_targeting_allowed",
    "signal_targeting_options",
    "signal_targeting_rules",
    "pricing_options"
  ]
}
```

### Currency filtering

Use `filters.pricing_currencies` when the buyer's constraint is "only show products whose media price I can transact in." Use `budget_range.currency` when the buyer is also providing a budget amount or range.

Buyers MAY send both. Sellers apply them conjunctively: `budget_range.currency` denominates the budget amounts, while `pricing_currencies` narrows which returned product `pricing_options` are eligible. If the two fields conflict, sellers SHOULD return zero matching products rather than reject the request solely because of the conflict. Because product-scoped signal pricing is a separate add-on surface, this filter only gates mandatory seller-applied signal charges; optional signal or vendor add-ons may still advertise other currencies, and buyers should not select unsupported add-on prices.

When combined with `is_fixed_price` or `pricing_structures`, returned product `pricing_options` MUST satisfy every filter: each retained option must have a requested pricing structure and its `currency` must be in `pricing_currencies`.

Currency-only filter example:

```json theme={null}
{
  "$schema": "/schemas/3.2.0-beta.0/media-buy/get-products-request.json",
  "idempotency_key": "550e8400-e29b-41d4-a716-446655441006",
  "buying_mode": "wholesale",
  "filters": {
    "pricing_currencies": ["USD"]
  },
  "fields": ["product_id", "name", "pricing_options"]
}
```

If a product has both USD and EUR media pricing and the buyer sends `pricing_currencies: ["USD"]`, the seller returns the product with only its USD product-level `pricing_options`. If the product also has a fixed or otherwise mandatory product-scoped signal charge, that mandatory charge must either be priced in USD or have no incremental price; otherwise the product does not match the filter. A mandatory `custom` signal price without `currency` is not satisfiable for this filter unless the seller can truthfully treat it as having no incremental price. Optional signal add-ons do not affect product matching.

### Budget Range Object

| Parameter  | Type   | Required | Description                                              |
| ---------- | ------ | -------- | -------------------------------------------------------- |
| `currency` | string | Yes      | ISO 4217 currency code (e.g., `"USD"`, `"EUR"`, `"GBP"`) |
| `min`      | number | No\*     | Minimum budget amount                                    |
| `max`      | number | No\*     | Maximum budget amount                                    |

\*At least one of `min` or `max` must be specified.

### Refine array

The `refine` array is a list of change requests. Each entry declares a `scope` and what the buyer is asking for. At least one entry is required. The seller considers all entries together when composing the response, and replies to each via `refinement_applied`.

Each entry is a discriminated union on `scope`:

#### scope: "request"

| Field   | Type   | Required | Description                                                                                                       |
| ------- | ------ | -------- | ----------------------------------------------------------------------------------------------------------------- |
| `scope` | string | Yes      | `"request"`                                                                                                       |
| `ask`   | string | Yes      | Direction for the selection as a whole (e.g., `"more video options"`, `"suggest how to combine these products"`). |

#### scope: "product"

| Field        | Type   | Required | Description                                                                                                                                                                                                                                         |
| ------------ | ------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `scope`      | string | Yes      | `"product"`                                                                                                                                                                                                                                         |
| `product_id` | string | Yes      | Product ID from a previous `get_products` response                                                                                                                                                                                                  |
| `action`     | string | No       | `"include"` (default): return this product with updated pricing and data. `"omit"`: exclude from the response. `"more_like_this"`: find similar products (the original is also returned). When omitted, the seller treats the entry as `"include"`. |
| `ask`        | string | No       | What the buyer is asking for. For `"include"`: specific changes (e.g., `"add 16:9 format"`). For `"more_like_this"`: what "similar" means (e.g., `"same audience but video format"`). Ignored when `action` is `"omit"`.                            |

#### scope: "proposal"

| Field         | Type   | Required | Description                                                                                                                                                                                                                                                           |
| ------------- | ------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `scope`       | string | Yes      | `"proposal"`                                                                                                                                                                                                                                                          |
| `proposal_id` | string | Yes      | Proposal ID from a previous `get_products` response                                                                                                                                                                                                                   |
| `action`      | string | No       | `"include"` (default): return with updated allocations and pricing. `"omit"`: exclude from the response. `"finalize"`: request firm pricing and inventory hold (transitions a draft proposal to committed). When omitted, the seller treats the entry as `"include"`. |
| `ask`         | string | No       | What the buyer is asking for (e.g., `"shift more budget toward video"`, `"reduce total by 10%"`). Ignored when `action` is `"omit"`.                                                                                                                                  |

### refinement\_applied (response)

When the seller receives a `refine` array, the response includes `refinement_applied` — an array matched by position. Each entry reports whether the ask was fulfilled:

| Field         | Type   | Required                         | Description                                                                                        |
| ------------- | ------ | -------------------------------- | -------------------------------------------------------------------------------------------------- |
| `scope`       | string | Yes                              | Echoes the scope (`"request"` / `"product"` / `"proposal"`) from the corresponding `refine` entry. |
| `product_id`  | string | Yes when `scope` is `"product"`  | Echoes `product_id` from the corresponding refine entry.                                           |
| `proposal_id` | string | Yes when `scope` is `"proposal"` | Echoes `proposal_id` from the corresponding refine entry.                                          |
| `status`      | string | Yes                              | `"applied"`: ask fulfilled. `"partial"`: partially fulfilled. `"unable"`: could not fulfill.       |
| `notes`       | string | No                               | Seller explanation. Recommended when status is `"partial"` or `"unable"`.                          |

### Catalog discovery

Pass a `catalog` to find advertising products that can promote your catalog items. The seller matches your catalog items against its inventory and returns products where matches exist. Supports all catalog types — a product catalog finds sponsored product slots, a job catalog finds job ad products, a flight catalog finds dynamic travel ads.

The `catalog` field uses the same [Catalog](/dist/docs/3.2.0-beta.0/creative/catalogs) object used throughout AdCP. You can reference a synced catalog by `catalog_id`, provide inline items, or use selectors to filter:

| Field        | Type        | Description                                                                     |
| ------------ | ----------- | ------------------------------------------------------------------------------- |
| `type`       | CatalogType | Catalog type (required) — `product`, `job`, `hotel`, `flight`, `offering`, etc. |
| `catalog_id` | string      | Reference a synced catalog by ID                                                |
| `ids`        | string\[]   | Filter to specific item IDs                                                     |
| `gtins`      | string\[]   | Filter by GTIN for cross-retailer matching (product type only)                  |
| `tags`       | string\[]   | Filter by tags (OR logic)                                                       |
| `category`   | string      | Filter by category                                                              |
| `query`      | string      | Natural language filter                                                         |

Products in the response include `catalog_types` (what catalog types they support) and `catalog_match` (which items matched).

## Response

Returns an array of `products` and optionally `proposals`. When the seller's
structured interpretation of hard brief targeting materially affects product
eligibility, pricing, or forecasting, it MUST include one response-level
`targeting_resolution.brief_targeting` confirmation shared by the curated
result set. Otherwise confirmation remains a best practice.

### Products Array

| Field                          | Type                                                                                                                                    | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `product_id`                   | string                                                                                                                                  | Opaque buyable identifier. A non-custom wholesale ID remains stable for the same logical offer within the seller and declared `cache_scope`, including across reads and webhooks. A custom ID is stable only in its issuing discovery/refinement lineage. Selecting it accepts any disclosed `targeting_resolution`.                                                                                                                                                                                                                                                                         |
| `is_custom`                    | boolean                                                                                                                                 | `true` for a request-specific configured offer. Products issued through targeting-aware discovery include `expires_at`, including exact targeting configurations with no Product resolution echo. Legacy custom products remain schema-valid when expiry is absent.                                                                                                                                                                                                                                                                                                                          |
| `name`                         | string                                                                                                                                  | Human-readable product name                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `description`                  | string                                                                                                                                  | Detailed product description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `publisher_properties`         | PublisherProperty\[]                                                                                                                    | Array of publisher entries, each with `publisher_domain` and either `property_ids` or `property_tags`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `format_options`               | ProductFormatDeclaration\[]                                                                                                             | Closed canonical creative contracts this product can deliver, including optional seller-enforced `locale_policy.accepted_language_ranges` that buyers use to preflight planned creative languages before purchase.                                                                                                                                                                                                                                                                                                                                                                           |
| `format_ids`                   | FormatID\[]                                                                                                                             | Deprecated 3.x compatibility projection, when emitted. Locale-constrained options cannot be represented on this path.                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `delivery_type`                | string                                                                                                                                  | `"guaranteed"` or `"non_guaranteed"`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `delivery_measurement`         | DeliveryMeasurement                                                                                                                     | (Optional) How delivery is measured (impressions, views, etc.)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `pricing_options`              | PricingOption\[]                                                                                                                        | Available pricing models (CPM, CPCV, etc.). Auction options may include `floor_price` and optional `price_guidance`. Bid-based auction models (CPM, vCPM, CPC, CPCV, CPV) may also include optional `max_bid` (boolean).                                                                                                                                                                                                                                                                                                                                                                     |
| `shows`                        | CollectionSelector\[]                                                                                                                   | (Optional) Collections available in this product. Each entry has `publisher_domain` and `collection_ids`. Buyers resolve full collection objects from the referenced `adagents.json`. See [Collections and installments](/dist/docs/3.2.0-beta.0/media-buy/product-discovery/collections-and-installments).                                                                                                                                                                                                                                                                                  |
| `collection_targeting_allowed` | boolean                                                                                                                                 | (Optional, default: false) Whether buyers can target a subset of this product's shows. When false, the product is a bundle.                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `overlay_support`              | TargetingOverlaySupport                                                                                                                 | Product-scoped targeting dimensions that may be selected later. Required when the discovery request includes `required_overlay_support`. This guarantees selectable capability subject to disclosed limits, not inventory or a value-specific forecast for every possible selection.                                                                                                                                                                                                                                                                                                         |
| `targeting_resolution`         | ProductTargetingResolution                                                                                                              | Sparse product-specific modifications between requested and executable structured targeting. Omitted when the product accepts the structured overlay exactly. When present, `expires_at` is also required.                                                                                                                                                                                                                                                                                                                                                                                   |
| `expires_at`                   | string                                                                                                                                  | Expiration of this request-scoped configured product or quote. A recognized expired ID issued to the authenticated account and lineage returns [`PRODUCT_EXPIRED`](/dist/docs/3.2.0-beta.0/building/verification/compliance-catalog#error-code-product-expired); after the seller no longer retains its tombstone, or when the ID belongs to another account/lineage, [`PRODUCT_NOT_FOUND`](/dist/docs/3.2.0-beta.0/building/verification/compliance-catalog#error-code-product-not-found) applies. The error choice never reveals cross-tenant existence. Buyers rediscover in either case. |
| `data_provider_signals`        | DataProviderSignalSelector\[]                                                                                                           | (Optional, deprecated) Legacy/non-selectable metadata for data-provider signals already bundled into or associated with this product. New implementations should use `included_signals`.                                                                                                                                                                                                                                                                                                                                                                                                     |
| `included_signals`             | SignalListing\[]                                                                                                                        | (Optional) Non-selectable signal metadata for signals already included in, bundled with, or planned into this product. These describe what the product is; buyers do not select them in package `signal_targeting_groups`. Data-provider and signal-source refs may be reference-only; product-local refs include inline `name` and `value_type`.                                                                                                                                                                                                                                            |
| `signal_targeting_allowed`     | boolean                                                                                                                                 | (Optional, default: false) Whether this product has a package-level signal targeting surface. Editability is controlled by `signal_targeting_rules`; fixed/default-only products still set this to true when applied signal groups are echoed.                                                                                                                                                                                                                                                                                                                                               |
| `signal_targeting_options`     | ProductSignalTargetingOption\[]                                                                                                         | (Optional) Inline product-scoped signal options the buyer may select, or the seller may apply when fixed/default, through `packages[].targeting_overlay.signal_targeting_groups`. May include per-signal `pricing_options`; product-scoped prices are authoritative for this product. Data-provider and signal-source refs may be reference-only; product-local refs include inline `name` and `value_type`.                                                                                                                                                                                 |
| `signal_targeting_rules`       | SignalTargetingRules                                                                                                                    | (Optional) Product-scoped composition rules for selectable signals, such as direct vs seller-planned resolution, optional, required, maximum, mutually exclusive, fixed selections, and group size limits. These limits belong on the product, not seller-wide [`get_adcp_capabilities`](/dist/docs/3.2.0-beta.0/protocol/get_adcp_capabilities), because products may be backed by different ad servers or seller planning layers. Fixed/default selections are applied by the seller and echoed on the resulting package state.                                                            |
| `demographic_targeting`        | DemographicTargetingCapability                                                                                                          | (Optional) Product-scoped exact execution for canonical demographic intent. `age.execution_modes` declares continuous bounds, enumerated seller intervals, and/or signal-backed resolution; continuous mode explicitly declares `supports_unbounded_min` and `supports_unbounded_max`, while `unknown_handling` states whether unknown age is selectable, always excluded, or always included. Buyers must inspect this field before sending `targeting_overlay.demographics`.                                                                                                               |
| `brief_relevance`              | string                                                                                                                                  | Why this product matches the brief (when brief provided)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `measurement_readiness`        | [MeasurementReadiness](/dist/docs/3.2.0-beta.0/media-buy/conversion-tracking/#measurement-readiness)                                    | (Optional) Whether the buyer's event setup is sufficient for this product's optimization. Only present when the seller can evaluate the buyer's account context.                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `measurement_terms`            | [MeasurementTerms](/dist/docs/3.2.0-beta.0/media-buy/advanced-topics/pricing-models#measurement-terms-and-performance-standards)        | (Optional) Seller's default billing measurement and makegood terms. Buyers may propose different terms at [`create_media_buy`](/dist/docs/3.2.0-beta.0/media-buy/task-reference/create_media_buy).                                                                                                                                                                                                                                                                                                                                                                                           |
| `performance_standards`        | [PerformanceStandard\[\]](/dist/docs/3.2.0-beta.0/media-buy/advanced-topics/pricing-models#measurement-terms-and-performance-standards) | (Optional) Seller's default performance standards (viewability, IVT, completion rate, brand safety, attention score). Buyers may propose different standards at `create_media_buy`.                                                                                                                                                                                                                                                                                                                                                                                                          |
| `cancellation_policy`          | [CancellationPolicy](/dist/docs/3.2.0-beta.0/media-buy/advanced-topics/pricing-models#cancellation-policy)                              | (Optional) Cancellation notice period and penalties for guaranteed products. Buyers accept these terms by creating a media buy against the product.                                                                                                                                                                                                                                                                                                                                                                                                                                          |

### Publisher properties for non-URL inventory

`publisher_properties[].publisher_domain` is the domain that anchors the publisher's `adagents.json` namespace. It is not required to be the URL where an ad is displayed, and it is not a placeholder for a physical venue, publication, station, screen network, or print title.

Use the same selector shape for digital and non-digital products:

* **Digital properties**: `publisher_domain` is usually the publisher domain whose `adagents.json` declares the website, app, channel, or CTV property.
* **Print, static OOH, radio, cinema, and local TV**: `publisher_domain` is the operating publisher or network domain that publishes the authoritative property catalog. The actual inventory is identified by `property_ids`, `property_tags`, placements, collections, product metadata, and channel fields.
* **Aggregated networks**: use `property_tags` when the product spans many properties, such as a tagged set of venues, publications, screens, stations, or local markets.

Do not invent values like `"print"` or `"ooh"` for `publisher_domain`. Put channel meaning in `channels`, property meaning in the referenced property declarations, and sellable-package meaning in the product itself.

For example, a product that spans tagged metro properties can use this selector inside its `publisher_properties` array:

```json theme={null}
[
  {
    "selection_type": "by_tag",
    "property_tags": ["metro", "station"]
  }
]
```

### Proposals Array (Optional)

Publishers may return proposals alongside products - structured media plans with budget allocations. See [Proposals](/dist/docs/3.2.0-beta.0/media-buy/product-discovery/media-products#proposals) for details.

| Field                   | Type                 | Description                                                                                                                                                                                                                                                                                                             |
| ----------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `proposal_id`           | string               | Unique identifier for finalizing this proposal and, once committed, executing it via `create_media_buy`                                                                                                                                                                                                                 |
| `proposal_status`       | string               | Lifecycle state. `draft` means the proposal must be finalized via `get_products` refine action `finalize` before create. `committed` means the proposal can be executed via `create_media_buy` before `expires_at`. When absent, treat the proposal as ready to buy for backward compatibility.                         |
| `name`                  | string               | Human-readable name for the media plan                                                                                                                                                                                                                                                                                  |
| `budget_allocation`     | BudgetAllocation     | Optional cross-product allocation configuration. Omission means a fixed proposal; `seller_optimized` delegates continuous allocation to the seller.                                                                                                                                                                     |
| `pacing`                | string               | Optional aggregate media-buy pacing recommendation. On a committed proposal this is part of the firm delivery terms.                                                                                                                                                                                                    |
| `allocations`           | ProductAllocation\[] | Fixed proposals use exact `allocation_percentage` values summing to 100. Seller-optimized proposals omit exact percentages and may use `min_spend_target_percentage` and `max_spend_percentage`. Each allocation may include optional flight dates and subordinate `pacing`, which becomes package pacing on execution. |
| `forecast`              | DeliveryForecast     | Aggregate delivery forecast for the proposal. Contains forecast points with metric ranges. See [Delivery Forecasts](/dist/docs/3.2.0-beta.0/media-buy/product-discovery/media-products#delivery-forecasts)                                                                                                              |
| `total_budget_guidance` | object               | Optional min/recommended/max budget guidance                                                                                                                                                                                                                                                                            |
| `brief_alignment`       | string               | How this proposal addresses the campaign brief                                                                                                                                                                                                                                                                          |
| `expires_at`            | string               | ISO 8601 timestamp when this proposal expires. For committed proposals, this is the inventory-hold deadline for `create_media_buy`.                                                                                                                                                                                     |

Each `ForecastPoint` is one forecast row. Composite slices are encoded by multiple `dimensions[]` items on the same point, such as placement x country. Sibling points are parallel rows, not nested children. Dimension order has no meaning; buyers normalize row identity from `(forecast_range_unit, budget if present, product_id if present, dimensions sorted by kind)`. Buyers may compare rows at the same grain, but MUST NOT sum them unless the seller documents that the returned rows form a complete, non-overlapping partition. Standard delivery reporting verifies one-dimensional marginals, not exact cross-dimensional intersections.

### Pagination

`pagination` is valid in all `get_products` modes, but its meaning follows the
buying mode:

* In `brief` mode, pagination bounds the seller's curated answer to the brief.
  A page is not a promise that every product matching the words in the brief has
  been enumerated.
* In `refine` mode, pagination bounds the refined `products[]` result implied by
  the `refine` array and current filters. Proposals may accompany the page as
  plan metadata, but `pagination.max_results`, `has_more`, `cursor`, and
  `total_count` are scoped to the product result set, not to a separate
  proposal list or a combined product/proposal count.
* In `wholesale` mode, pagination walks the wholesale product feed. This is the
  exhaustive/feed-style read and is the mode that pairs with wholesale feed
  versioning.

Use cursor-based pagination to cap returned products in curated/refined
responses or walk wholesale product feeds:

| Request Parameter        | Type    | Description                                    |
| ------------------------ | ------- | ---------------------------------------------- |
| `pagination.max_results` | integer | Maximum products per page (1-100, default: 50) |
| `pagination.cursor`      | string  | Cursor from previous response for next page    |

| Response Field           | Type    | Description                                                                                                                                                                         |
| ------------------------ | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `pagination.has_more`    | boolean | Whether more products are available                                                                                                                                                 |
| `pagination.cursor`      | string  | Cursor to pass for the next page                                                                                                                                                    |
| `pagination.total_count` | integer | Total products in this paginated result set (optional, not all backends support this). In `brief` / `refine`, this is the curated/refined product set, not the full seller catalog. |

Pagination is optional. When omitted, the server returns the complete result set
or a server-chosen default page. When the response includes
`pagination.has_more: true`, pass `pagination.cursor` in the next request to get
the next page using the same result-defining request context, except for the
updated `pagination.cursor`.

### Response Metadata

| Field                    | Type                                                 | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| ------------------------ | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `property_list_applied`  | boolean                                              | \[AdCP 3.0] `true` if the agent filtered products based on the provided `property_list`. Absent or `false` if not provided or not supported.                                                                                                                                                                                                                                                                                                                                                                                                           |
| `catalog_applied`        | boolean                                              | `true` if the seller filtered results based on the provided `catalog`. Absent or `false` if no catalog was provided or the seller does not support catalog matching.                                                                                                                                                                                                                                                                                                                                                                                   |
| `refinement_applied`     | [RefinementResult\[\]](#refinement_applied-response) | Seller acknowledgment of each `refine` entry, matched by position. Only present when `buying_mode` is `"refine"`. See [refinement\_applied](#refinement_applied-response) above.                                                                                                                                                                                                                                                                                                                                                                       |
| `incomplete`             | [IncompleteEntry\[\]](#incomplete-array)             | Declares what the seller could not finish within the `time_budget` or due to internal limits. Each entry identifies a scope with a human-readable explanation. Absent when the response is fully complete. See [incomplete array](#incomplete-array) below.                                                                                                                                                                                                                                                                                            |
| `filter_diagnostics`     | object                                               | Optional non-fatal observability block describing how `filters` narrowed the candidate set — `total_candidates` plus per-filter `excluded_by` counts (keyed by filter name). Disambiguates "no inventory" from "your filter excluded everything" when the result list is empty or unexpectedly small. Counts only — never product names — to avoid leaking competitive intelligence. See [filter\_diagnostics](#filter_diagnostics) below.                                                                                                             |
| `wholesale_feed_version` | string                                               | Opaque token representing the version of the wholesale product feed state used to compose this response. Sellers implementing conditional-fetch (`if_wholesale_feed_version`) MUST return this on every wholesale-mode response so buyers can cache and probe later. Treat as opaque — no format, no ordering, no inspection. See [Wholesale feed versioning](#wholesale-feed-versioning).                                                                                                                                                             |
| `pricing_version`        | string                                               | Optional opaque token representing the version of the pricing layer, including product `pricing_options` and nested `signal_targeting_options[].pricing_options`. When the seller supports independent pricing versioning, `pricing_version` changes when prices move but `wholesale_feed_version` changes only when structure/metadata moves. Sellers not separating these MAY omit `pricing_version` and use `wholesale_feed_version` for both.                                                                                                      |
| `cache_scope`            | string                                               | `"public"` or `"account"`. **REQUIRED on every response** (schema-enforced — the safety property of the two-layer cache depends on it). When the request had no `account`, MUST be `"public"`. When the request had `account`, the seller declares either `"public"` (account prices off the rate card — buyer dedupes) or `"account"` (account-specific overrides). See [Cache layering](#cache-layering).                                                                                                                                            |
| `unchanged`              | boolean                                              | Present and `true` ONLY when the request carried `if_wholesale_feed_version` (and/or `if_pricing_version`) matching the seller's current version for the buyer's `cache_scope`, in which case `products[]` MUST be omitted; `wholesale_feed_version`, `cache_scope`, and `pricing_version` (when used) MUST still be echoed. Sellers MUST NOT emit `unchanged: false` — absence of the field IS the "response carries products" signal (one shape per state). Buyers receiving `unchanged: true` MUST NOT mutate their local wholesale product mirror. |

### filter\_diagnostics

When the seller can attribute exclusions to specific filters, the response MAY include a `filter_diagnostics` block. This is observability — not error reporting; sellers still silently exclude unmatched products per the filter-not-fail convention. Buyers use this to triage empty/small results without depending on its presence. `total_candidates` and `excluded_by` are independently optional — sellers whose baseline candidate set size is sensitive MAY emit `excluded_by` without `total_candidates`.

| Field                         | Type    | Description                                                                                                                                                                                                                                                                                                                                                    |
| ----------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `semantics`                   | string  | `"only"` (deterministic; counts products that would have been included if not for *this* filter alone — recommended for triage), `"any"` (counts products excluded by any filter; counts may overlap), or `"approximate"` (seller can't cleanly attribute exclusions to a single filter). Buyers SHOULD inspect `semantics` before doing arithmetic on counts. |
| `total_candidates`            | integer | Number of products considered before filters were applied. May be sampled or capped when the candidate pool is large. Optional.                                                                                                                                                                                                                                |
| `excluded_by`                 | object  | Keys are filter property names from the request (`pricing_currencies`, `required_metrics`, `required_geo_targeting`, `budget_range`, etc.). Each value is `{ count, values?, notes? }`. Only filters that meaningfully narrowed the set need appear.                                                                                                           |
| `excluded_by.<filter>.count`  | integer | Count of products excluded by this filter, interpreted per the parent `semantics` field.                                                                                                                                                                                                                                                                       |
| `excluded_by.<filter>.values` | array   | Optional list of the specific filter values that contributed to exclusions (e.g., `["completed_views"]` for `required_metrics`). Items are strings or objects depending on filter shape; opaque without filter-specific knowledge.                                                                                                                             |
| `excluded_by.<filter>.notes`  | string  | Optional human-readable note about the narrowing.                                                                                                                                                                                                                                                                                                              |

```json theme={null}
{
  "products": [],
  "filter_diagnostics": {
    "semantics": "only",
    "total_candidates": 47,
    "excluded_by": {
      "required_metrics": { "count": 31, "values": ["completed_views"] },
      "required_geo_targeting": { "count": 9 },
      "pricing_currencies": { "count": 3, "values": ["USD"] },
      "budget_range": { "count": 7 }
    }
  }
}
```

### incomplete array

When the seller returns usable results but cannot complete all work within the `time_budget` (or due to its own internal limits), the response includes `incomplete` — an array declaring what is missing. `incomplete` is a completeness statement, not a retry classification: buyers MUST NOT infer that retrying the same request will succeed. `estimated_wait`, when present, lets a buyer decide whether to make a new attempt with a larger time budget.

| Field            | Type     | Required | Description                                                                                                                                                                                                                                                                                                                              |
| ---------------- | -------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `scope`          | string   | Yes      | `"products"`: not all inventory sources were searched. `"pricing"`: products returned but pricing is absent or unconfirmed. `"forecast"`: products returned but forecast data is absent. `"proposals"`: proposals were not generated or are incomplete. `"wholesale_feed"`: in wholesale mode, full feed enumeration could not complete. |
| `description`    | string   | Yes      | Human-readable explanation of what is missing and why.                                                                                                                                                                                                                                                                                   |
| `estimated_wait` | Duration | No       | Additional time expected to help resolve this scope. This is planning guidance, not a retry instruction or guarantee.                                                                                                                                                                                                                    |

### Wholesale feed versioning

A buyer that just synced a seller's wholesale product feed can ask "has anything changed since version X?" in one cheap call, regardless of feed size. Sellers return an opaque `wholesale_feed_version` on every wholesale-mode response; buyers pass it back via `if_wholesale_feed_version` on the next call and the seller MAY short-circuit with `unchanged: true` — no products payload, no per-page diff. Patterned on HTTP `ETag` / `If-None-Match`.

This is the seller-side wholesale product feed returned by `get_products`. It is not a [`sync_catalogs`](/dist/docs/3.2.0-beta.0/media-buy/task-reference/sync_catalogs) feed; `sync_catalogs` manages buyer-provided campaign input feeds on the seller account.

**Unchanged response example:**

Request:

```json theme={null}
{
  "$schema": "/schemas/3.2.0-beta.0/media-buy/get-products-request.json",
  "idempotency_key": "550e8400-e29b-41d4-a716-446655441007",
  "buying_mode": "wholesale",
  "if_wholesale_feed_version": "v2026-05-18T08:00:00Z-acme-rev412"
}
```

Response (wholesale product feed unchanged):

```json theme={null}
{
  "$schema": "/schemas/3.2.0-beta.0/media-buy/get-products-response.json",
  "status": "completed",
  "message": "Wholesale product feed unchanged since v2026-05-18T08:00:00Z-acme-rev412.",
  "context_id": "ctx-abc-789",
  "unchanged": true,
  "wholesale_feed_version": "v2026-05-18T08:00:00Z-acme-rev412",
  "pricing_version": "v2026-05-18T08:00:00Z-acme-rev412",
  "cache_scope": "public"
}
```

Response (wholesale product feed changed — full payload returned, abbreviated):

```json test=false theme={null}
{
  "message": "Returning 50 of 312 products (wholesale feed version advanced).",
  "context_id": "ctx-abc-790",
  "wholesale_feed_version": "v2026-05-18T10:15:00Z-acme-rev415",
  "pricing_version": "v2026-05-18T10:15:00Z-acme-rev415",
  "cache_scope": "public",
  "products": [
    {
      "product_id": "prod_premium_ctv_us",
      "name": "Premium CTV — US",
      "description": "Run-of-network CTV inventory across premium publishers.",
      "publisher_properties": [{ "publisher_domain": "streamhaus.example.com", "property_ids": ["primetime_ctv"] }],
      "format_options": [{ "format_option_id": "video_ctv_1080p_30s", "format_kind": "video_hosted", "params": { "width": 1920, "height": 1080, "duration_ms_exact": 30000 } }],
      "delivery_type": "guaranteed",
      "pricing_options": [
        { "pricing_option_id": "po_cpm_v2", "pricing_model": "cpm", "currency": "USD", "fixed_price": 18.50 }
      ]
    }
  ],
  "pagination": { "has_more": true, "cursor": "eyJvIjo1MH0=", "total_count": 312 }
}
```

**Rules**

* Tokens are **opaque**. No format, no ordering, no inspection.
* A returned `wholesale_feed_version` is scoped to the request parameters that produced it. Buyers MUST cache the version alongside the `(account, buying_mode, filters, targeting_overlay, required_overlay_support, deprecated property_list, catalog)` tuple used.
* `pricing_version` is an optional finer-grained token: when present, it changes when prices move but `wholesale_feed_version` changes only when structure/metadata moves. Common for rate-card sweeps that don't change product metadata.
* **`if_pricing_version` requires `if_wholesale_feed_version`.** Pricing has no structural baseline of its own. Sending `if_pricing_version` without `if_wholesale_feed_version` is a schema-level error. The seller's evaluation is two-stage: wholesale feed mismatch returns the full payload (pricing is implicitly stale); wholesale feed match with pricing mismatch also returns the full payload (so the buyer sees updated `pricing_options`); both match → `unchanged: true`.
* **`filters` canonicalization.** Sellers MUST treat the `filters` object as canonicalized before hashing into the `wholesale_feed_version` keyspace: keys MUST be sorted lexicographically, omitted-and-default values MUST be treated identically (a missing `delivery_type` key is the same scope as `delivery_type: null`), array values MUST be sorted where the filter has set semantics (e.g., `channels`, `format_kinds`, `format_option_refs`, `required_metrics`) and preserved-order where the filter has sequence semantics (e.g., `preferred_delivery_types`). Buyers that pass equivalent-but-differently-shaped filter objects MUST receive the same `wholesale_feed_version` from the seller. This rule prevents silent stale-mirror bugs from key-order or default-elision differences between buyer SDKs. **Forward-compat default:** new filter fields added in 3.x minor versions MUST declare set-vs-sequence semantics in their schema (via `x-canonicalization: set | sequence` or equivalent prose); absent an explicit declaration, the rule defaults to **set-semantics** (sort before hashing). Sellers and SDKs that drift on this default produce cache misses that consumers can't explain.
* **Pagination interaction.** `wholesale_feed_version` describes the wholesale product feed as a whole, not individual pages. Sellers MUST return `wholesale_feed_version` on every paginated page (not only the first) when they declare `wholesale_feed_versioning.supported: true`; sellers that do not declare versioning SHOULD do the same. When the wholesale feed mutates between pages, the new version surfaces on the next page and the buyer MUST restart pagination from `cursor: null` — the partial pages they've already received describe a stale version. Sellers MAY alternatively snapshot the feed at the start of pagination and serve all pages from that snapshot under the original version; either implementation is conformant as long as `wholesale_feed_version` on a given page is the version that page belongs to.
* **`unchanged: true` and in-progress pagination.** A buyer that is mid-pagination on `cursor: X` MAY send `if_wholesale_feed_version` matching the version their pages so far were drawn from. If the seller confirms `unchanged: true`, the response omits `products[]` and pagination envelope entirely; the buyer abandons their in-progress walk under that version with confidence that no further pages would have produced new data. Sellers MAY NOT use the conditional-fetch short-circuit to skip individual pages within an active pagination — `unchanged` is feed-versus-cached-version, not per-page.
* Pre-v3.1 sellers that ignore `if_wholesale_feed_version` simply return the full payload — semantically correct, just inefficient (same as the unchanged-server path in HTTP).

For pushed change tracking beyond conditional fetch, see `specs/wholesale-feed-webhooks.md`. Wholesale feed webhooks carry the changed product payload, pricing payload, removal tombstone, or bulk-change summary; `get_products` remains the repair and reconciliation read.

### Cache layering

Sellers publish two notional layers: a **public layer** (the rate-card / structural view) and **per-account overlays** (custom deals, account-specific rate cards). The conditional-fetch path is layer-aware via `cache_scope`.

**Why this matters.** A buyer mirroring wholesale products across N accounts at one seller doesn't want to hold N copies of inventory that's actually identical for every buyer. The public layer is the seller's published rate card; most accounts at most sellers price off it directly. Premium custom deals are the exception.

**Two-layer cache.**

| Layer           | Cache key                                                                                                                   | What's stored                                                                                                                                  |
| --------------- | --------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| Public          | `(agent, buying_mode, filters, targeting_overlay, required_overlay_support, deprecated property_list, catalog)`             | `wholesale_feed_version_public`, the wholesale product feed payload as seen without an account ref                                             |
| Account overlay | `(agent, buying_mode, filters, targeting_overlay, required_overlay_support, deprecated property_list, catalog, account_id)` | `wholesale_feed_version_account`, the wholesale product feed payload as seen WITH this account ref, when `cache_scope: "account"` was returned |

**Behavior.**

* Requests without `account` always return `cache_scope: "public"`. Buyers cache under the public key.
* Requests with `account` return `cache_scope: "public"` OR `"account"` (seller MUST declare; no default).
  * `"public"`: this account prices off the rate card. Buyer MAY dedupe — the version and payload are the same as the unauthenticated view. The buyer can serve subsequent requests for any account in `"public"` cache\_scope from a single public-layer entry.
  * `"account"`: this response carries account-specific overrides. Buyer caches under the account overlay key.
* Sellers MAY downgrade an account from `"account"` back to `"public"` by returning `cache_scope: "public"` on a request that previously got `"account"` — buyers SHOULD interpret this as "this account no longer has overrides" and drop their account overlay.

**Conditional fetch with `if_wholesale_feed_version`.** Send the token paired with whichever scope it was returned in. The seller compares against the current version for that scope. If the buyer's token belongs to an `"account"` scope but the seller responds with `cache_scope: "public"`, that's the downgrade signal — buyer drops the overlay.

**Webhook invalidation.** Wholesale feed webhook events declare `applies_to.scope` on `*.priced` and `*.updated` payloads. Sellers MUST apply the same account/caller authorization predicate used by `get_products buying_mode: "wholesale"` when deciding which subscribers receive product webhooks:

* `applies_to: { scope: "public" }` → invalidate the public-layer cache for the entity. All account overlays referencing that public version are also stale and SHOULD be refetched.
* `applies_to: { scope: "account", account_ids: [...] }` → invalidate only the named accounts' overlays. The public layer is unaffected.
* `applies_to: { scope: "account" }` without `account_ids` → the seller is withholding the affected set; the per-subscriber scope filter routes the event only to subscribers whose principal is in the affected set. Receiving the event means "your overlay is stale."

See `specs/wholesale-feed-webhooks.md` §"Cache layering and event scoping" for the full webhook-side spec.

**See schema for complete field list**: [`get-products-response.json`](https://adcontextprotocol.org/schemas/3.2.0-beta.0/media-buy/get-products-response.json)

## Common Scenarios

### Time-budgeted discovery

Declare a time budget when you need fast results and can accept partial data. The seller returns what it can within the budget and declares what is incomplete:

<CodeGroup>
  ```javascript JavaScript theme={null}
  import { testAgent } from '@adcp/sdk/testing';

  const result = await testAgent.getProducts({
    idempotency_key: '550e8400-e29b-41d4-a716-446655441008',
    buying_mode: 'brief',
    brief: 'CTV and display for brand awareness',
    brand: {
      domain: 'acmecorp.com'
    },
    time_budget: {
      interval: 10,
      unit: 'seconds'
    }
  });

  if (result.success && result.data) {
    console.log(`Found ${result.data.products.length} products`);

    if (result.data.incomplete) {
      for (const entry of result.data.incomplete) {
        console.log(`Incomplete: ${entry.scope} — ${entry.description}`);
        if (entry.estimated_wait) {
          console.log(`  Seller estimates more time may help: ${entry.estimated_wait.interval} ${entry.estimated_wait.unit}`);
        }
      }
    }
  }
  ```

  ```python Python theme={null}
  import asyncio
  from adcp.testing import test_agent

  async def discover_with_time_budget():
      result = await test_agent.simple.get_products(
          idempotency_key='550e8400-e29b-41d4-a716-446655441009',
          buying_mode='brief',
          brief='CTV and display for brand awareness',
          brand={
              'domain': 'acmecorp.com'
          },
          time_budget={
              'interval': 10,
              'unit': 'seconds'
          }
      )
      print(f"Found {len(result.products)} products")

      for entry in result.get('incomplete', []):
          print(f"Incomplete: {entry['scope']} — {entry['description']}")
          if 'estimated_wait' in entry:
              wait = entry['estimated_wait']
              print(f"  Seller estimates more time may help: {wait['interval']} {wait['unit']}")

  asyncio.run(discover_with_time_budget())
  ```
</CodeGroup>

A response with incomplete data — products are returned but some scopes are missing:

```json test=false theme={null}
{
  "products": [
    {
      "product_id": "prog-display-ros",
      "name": "Programmatic Display — Run of Site",
      "delivery_type": "non_guaranteed",
      "pricing_options": [{ "pricing_option_id": "cpm-ros", "pricing_model": "cpm", "currency": "USD", "fixed_price": 12.00 }]
    }
  ],
  "incomplete": [
    {
      "scope": "products",
      "description": "Premium inventory not searched — requires publisher approval",
      "estimated_wait": { "interval": 60, "unit": "minutes" }
    },
    {
      "scope": "forecast",
      "description": "Forecast model did not complete within budget",
      "estimated_wait": { "interval": 45, "unit": "seconds" }
    }
  ]
}
```

### Wholesale Product Discovery

<CodeGroup>
  ```javascript JavaScript theme={null}
  import { testAgent } from '@adcp/sdk/testing';

  // wholesale mode: buyer applies their own audiences, no publisher curation
  const result = await testAgent.getProducts({
    idempotency_key: '550e8400-e29b-41d4-a716-446655441010',
    buying_mode: 'wholesale',
    brand: {
      domain: 'acmecorp.com'
    },
    filters: {
      delivery_type: 'non_guaranteed'
    }
  });

  if (result.success && result.data) {
    console.log(`Found ${result.data.products.length} standard wholesale products`);
  }
  ```

  ```python Python theme={null}
  import asyncio
  from adcp.testing import test_agent

  async def discover_standard_wholesale_products():
      # wholesale mode: buyer applies their own audiences, no publisher curation
      result = await test_agent.simple.get_products(
          idempotency_key='550e8400-e29b-41d4-a716-446655441011',
          buying_mode='wholesale',
          brand={
              'domain': 'acmecorp.com'
          },
          filters={
              'delivery_type': 'non_guaranteed'
          }
      )
      print(f"Found {len(result.products)} standard wholesale products")

  asyncio.run(discover_standard_wholesale_products())
  ```
</CodeGroup>

### Multi-Format Discovery

<CodeGroup>
  ```javascript JavaScript theme={null}
  import { testAgent } from '@adcp/sdk/testing';

  // Find products supporting both video and display
  const result = await testAgent.getProducts({
    idempotency_key: '550e8400-e29b-41d4-a716-446655441012',
    buying_mode: 'brief',
    brief: 'Brand awareness campaign with video and display',
    brand: {
      domain: 'acmecorp.com'
    },
    filters: {
      channels: ['display', 'ctv']
    }
  });

  if (result.success && result.data) {
    console.log(`Found ${result.data.products.length} products supporting video and display`);
  }
  ```

  ```python Python theme={null}
  import asyncio
  from adcp.testing import test_agent

  async def discover_multi_format():
      # Find products supporting both video and display
      result = await test_agent.simple.get_products(
          idempotency_key='550e8400-e29b-41d4-a716-446655441013',
          buying_mode='brief',
          brief='Brand awareness campaign with video and display',
          brand={
              'domain': 'acmecorp.com'
          },
          filters={
              'channels': ['display', 'ctv']
          }
      )
      print(f"Found {len(result.products)} products supporting video and display")

  asyncio.run(discover_multi_format())
  ```
</CodeGroup>

### Budget and Date Filtering

<CodeGroup>
  ```javascript JavaScript theme={null}
  import { testAgent } from '@adcp/sdk/testing';

  // Find products within budget and date range for specific countries and channels
  const result = await testAgent.getProducts({
    idempotency_key: '550e8400-e29b-41d4-a716-446655441014',
    buying_mode: 'brief',
    brief: 'Q2 campaign for athletic footwear in North America',
    brand: {
      domain: 'acmecorp.com'
    },
    filters: {
      start_date: '2025-04-01',
      end_date: '2025-06-30',
      budget_range: {
        min: 50000,
        max: 100000,
        currency: 'USD'
      },
      countries: ['US', 'CA'],
      channels: ['display', 'ctv', 'podcast'],
      delivery_type: 'guaranteed'
    }
  });

  if (result.success && result.data) {
    console.log(`Found ${result.data.products.length} products for Q2 within budget`);
  }
  ```

  ```python Python theme={null}
  import asyncio
  from adcp.testing import test_agent

  async def discover_with_budget_and_dates():
      # Find products within budget and date range for specific countries and channels
      result = await test_agent.simple.get_products(
          idempotency_key='550e8400-e29b-41d4-a716-446655441015',
          buying_mode='brief',
          brief='Q2 campaign for athletic footwear in North America',
          brand={
              'domain': 'acmecorp.com'
          },
          filters={
              'start_date': '2025-04-01',
              'end_date': '2025-06-30',
              'budget_range': {
                  'min': 50000,
                  'max': 100000,
                  'currency': 'USD'
              },
              'countries': ['US', 'CA'],
              'channels': ['display', 'ctv', 'podcast'],
              'delivery_type': 'guaranteed'
          }
      )
      print(f"Found {len(result.products)} products for Q2 within budget")

  asyncio.run(discover_with_budget_and_dates())
  ```
</CodeGroup>

### Property Tag Resolution

<CodeGroup>
  ```javascript JavaScript theme={null}
  import { testAgent } from '@adcp/sdk/testing';

  // Get products with property tags
  const result = await testAgent.getProducts({
    idempotency_key: '550e8400-e29b-41d4-a716-446655441016',
    buying_mode: 'brief',
    brief: 'Sports content',
    brand: {
      domain: 'acmecorp.com'
    }
  });

  if (result.success && result.data) {
    // Products with property_tags in publisher_properties represent large networks
    // Use get_adcp_capabilities to discover the agent's portfolio
    const productsWithTags = result.data.products.filter(p =>
      p.publisher_properties?.some(pub => pub.property_tags && pub.property_tags.length > 0)
    );
    console.log(`${productsWithTags.length} products use property tags (large networks)`);
  }
  ```

  ```python Python theme={null}
  import asyncio
  from adcp.testing import test_agent

  async def discover_property_tags():
      # Get products with property tags
      result = await test_agent.simple.get_products(
          idempotency_key='550e8400-e29b-41d4-a716-446655441017',
          buying_mode='brief',
          brief='Sports content',
          brand={
              'domain': 'acmecorp.com'
          }
      )

      # Products with property_tags in publisher_properties represent large networks
      # Use get_adcp_capabilities to discover the agent's portfolio
      products_with_tags = [p for p in result.products
          if any(pub.get('property_tags') for pub in p.get('publisher_properties', []))]
      print(f"{len(products_with_tags)} products use property tags (large networks)")

  asyncio.run(discover_property_tags())
  ```
</CodeGroup>

### Guaranteed Delivery Products

<CodeGroup>
  ```javascript JavaScript theme={null}
  import { testAgent } from '@adcp/sdk/testing';

  // Find guaranteed delivery products for measurement
  const result = await testAgent.getProducts({
    idempotency_key: '550e8400-e29b-41d4-a716-446655441018',
    buying_mode: 'brief',
    brief: 'Guaranteed delivery for lift study',
    brand: {
      domain: 'acmecorp.com'
    },
    filters: {
      delivery_type: 'guaranteed',
      min_exposures: 100000
    }
  });

  if (result.success && result.data) {
    console.log(`Found ${result.data.products.length} guaranteed products with 100k+ exposures`);
  }
  ```

  ```python Python theme={null}
  import asyncio
  from adcp.testing import test_agent

  async def discover_guaranteed():
      # Find guaranteed delivery products for measurement
      result = await test_agent.simple.get_products(
          idempotency_key='550e8400-e29b-41d4-a716-446655441019',
          buying_mode='brief',
          brief='Guaranteed delivery for lift study',
          brand={
              'domain': 'acmecorp.com'
          },
          filters={
              'delivery_type': 'guaranteed',
              'min_exposures': 100000
          }
      )
      print(f"Found {len(result.products)} guaranteed products with 100k+ exposures")

  asyncio.run(discover_guaranteed())
  ```
</CodeGroup>

### Standard Formats Only

<CodeGroup>
  ```javascript JavaScript theme={null}
  import { testAgent } from '@adcp/sdk/testing';

  // Find products that only accept IAB standard formats
  const result = await testAgent.getProducts({
    idempotency_key: '550e8400-e29b-41d4-a716-446655441020',
    buying_mode: 'wholesale',
    brand: {
      domain: 'acmecorp.com'
    },
    filters: {
      standard_formats_only: true
    }
  });

  if (result.success && result.data) {
    console.log(`Found ${result.data.products.length} products with standard formats only`);
  }
  ```

  ```python Python theme={null}
  import asyncio
  from adcp.testing import test_agent

  async def discover_standard_formats():
      # Find products that only accept IAB standard formats
      result = await test_agent.simple.get_products(
          idempotency_key='550e8400-e29b-41d4-a716-446655441021',
          buying_mode='wholesale',
          brand={
              'domain': 'acmecorp.com'
          },
          filters={
              'standard_formats_only': True
          }
      )
      print(f"Found {len(result.products)} products with standard formats only")

  asyncio.run(discover_standard_formats())
  ```
</CodeGroup>

### Catalog-driven discovery

Use `catalog` with a brand to discover advertising products that can promote your catalog items. The seller matches your items against its inventory and returns products where matches exist:

<CodeGroup>
  ```javascript JavaScript theme={null}
  import { testAgent } from '@adcp/sdk/testing';

  // Discover retail media products for specific catalog items
  const result = await testAgent.getProducts({
    idempotency_key: '550e8400-e29b-41d4-a716-446655441022',
    buying_mode: 'wholesale',
    brand: {
      domain: 'acmecorp.com'
    },
    catalog: {
      type: 'product',
      tags: ['ketchup', 'organic'],
      category: 'food/condiments'
    },
    filters: {
      channels: ['retail_media']
    }
  });

  if (result.success && result.data) {
    if (result.data.catalog_applied) {
      console.log(`Found ${result.data.products.length} products with catalog matches`);
    } else {
      console.log('Seller does not support catalog matching');
    }
  }
  ```

  ```python Python theme={null}
  import asyncio
  from adcp.testing import test_agent

  async def discover_commerce_products():
      # Discover retail media products for specific catalog items
      result = await test_agent.simple.get_products(
          idempotency_key='550e8400-e29b-41d4-a716-446655441023',
          buying_mode='wholesale',
          brand={
              'domain': 'acmecorp.com'
          },
          catalog={
              'type': 'product',
              'tags': ['ketchup', 'organic'],
              'category': 'food/condiments'
          },
          filters={
              'channels': ['retail_media']
          }
      )
      if result.get('catalog_applied'):
          print(f"Found {len(result.products)} products with catalog matches")
      else:
          print("Seller does not support catalog matching")

  asyncio.run(discover_commerce_products())
  ```

  ```bash CLI requires-env=ADCP_AUTH_TOKEN theme={null}
  uvx adcp \
    https://test-agent.adcontextprotocol.org/sales/mcp \
    get_products \
    '{"idempotency_key":"550e8400-e29b-41d4-a716-446655441024","buying_mode":"wholesale","brand":{"domain":"acmecorp.com"},"catalog":{"type":"product","tags":["ketchup","organic"],"category":"food/condiments"},"filters":{"channels":["retail_media"]}}' \
    --auth $ADCP_AUTH_TOKEN
  ```
</CodeGroup>

You can also use GTIN matching, reference a synced catalog, or discover products for other catalog types:

```json theme={null}
{
  "catalog": {
    "type": "product",
    "gtins": ["00013000006040", "00013000006057"]
  }
}
```

```json theme={null}
{
  "catalog": {
    "catalog_id": "gmc-primary",
    "type": "product"
  }
}
```

```json theme={null}
{
  "catalog": {
    "type": "job",
    "catalog_id": "chef-vacancies"
  }
}
```

### Property List Filtering

<Info>
  **AdCP 3.0** - Property list filtering requires governance agent support.
</Info>

Filter products to only those available on properties in your approved list:

<CodeGroup>
  ```javascript JavaScript theme={null}
  import { testAgent } from '@adcp/sdk/testing';

  // Filter products by property list from governance agent
  const result = await testAgent.getProducts({
    idempotency_key: '550e8400-e29b-41d4-a716-446655441025',
    buying_mode: 'brief',
    brief: 'Brand-safe inventory for family brand',
    brand: {
      domain: 'acmecorp.com'
    },
    property_list: {
      agent_url: 'https://governance.example.com',
      list_id: 'pl_brand_safe_2024'
    }
  });

  if (result.success && result.data) {
    // Check if filtering was actually applied
    if (result.data.property_list_applied) {
      console.log(`Found ${result.data.products.length} products on approved properties`);
    } else {
      console.log('Agent does not support property list filtering');
      console.log(`Found ${result.data.products.length} products (unfiltered)`);
    }
  }
  ```

  ```python Python theme={null}
  import asyncio
  from adcp.testing import test_agent

  async def discover_with_property_list():
      # Filter products by property list from governance agent
      result = await test_agent.simple.get_products(
          idempotency_key='550e8400-e29b-41d4-a716-446655441026',
          buying_mode='brief',
          brief='Brand-safe inventory for family brand',
          brand={
              'domain': 'acmecorp.com'
          },
          property_list={
              'agent_url': 'https://governance.example.com',
              'list_id': 'pl_brand_safe_2024'
          }
      )

      # Check if filtering was actually applied
      if result.get('property_list_applied'):
          print(f"Found {len(result['products'])} products on approved properties")
      else:
          print("Agent does not support property list filtering")
          print(f"Found {len(result['products'])} products (unfiltered)")

  asyncio.run(discover_with_property_list())
  ```
</CodeGroup>

**Note**: If `property_list_applied` is absent or `false`, the sales agent did not filter products. This can happen if:

* The agent doesn't support property governance features
* The agent couldn't access the property list
* The property list had no effect on the available inventory

#### Property Targeting Behavior

Products have a `property_targeting_allowed` flag that affects filtering:

* **`property_targeting_allowed: false` (default)**: Product is "all or nothing" - excluded unless your list contains all of its properties
* **`property_targeting_allowed: true`**: Product is included if there's any intersection between its properties and your list

This allows publishers to offer run-of-network products that can't be cherry-picked alongside flexible inventory that buyers can filter.

See [Property Targeting](/dist/docs/3.2.0-beta.0/media-buy/product-discovery/media-products#property-targeting) for more details and [Property Governance](/dist/docs/3.2.0-beta.0/governance/property/specification) for more on property lists.

## Refinement

After initial discovery, use `buying_mode: "refine"` to iterate on specific products and proposals. The `refine` array is a list of change requests — each entry declares a scope and what the buyer is asking for. The seller returns updated products with revised pricing and configurations, plus `refinement_applied` acknowledging each ask.

See the [Refinement guide](/dist/docs/3.2.0-beta.0/media-buy/product-discovery/refinement) for the full walkthrough: scope types, action semantics, seller responses, and common patterns. The parameter shape is defined in the [Refine array](#refine-array) section above.

Minimal example:

```json test=false theme={null}
{
  "idempotency_key": "550e8400-e29b-41d4-a716-446655441027",
  "buying_mode": "refine",
  "refine": [
    { "scope": "request",                                             "ask": "more video, less display" },
    { "scope": "product",  "product_id":  "prod_premium_video",       "ask": "add 16:9 format option" },
    { "scope": "product",  "product_id":  "prod_display_run_of_site", "action": "omit" },
    { "scope": "proposal", "proposal_id": "prop_awareness_q2",        "ask": "reallocate display budget to video" }
  ],
  "filters": {
    "start_date": "2026-04-01",
    "end_date": "2026-04-30",
    "budget_range": { "min": 200000, "max": 200000, "currency": "USD" }
  }
}
```

Key rules to know before sending:

* **`refine` is only valid in `refine` mode.** Requests that include this field in `brief` or `wholesale` mode are rejected with [`INVALID_REQUEST`](/dist/docs/3.2.0-beta.0/building/verification/compliance-catalog#error-code-invalid-request).
* **Filters are absolute**, not deltas. Always send the full filter set you want applied.
* **Proposals are actionable through status.** `proposal_status: "draft"` requires finalization before create; `proposal_status: "committed"` can be executed with `create_media_buy(proposal_id)` before `expires_at`; absent status is legacy ready-to-buy.
* **Proposals are ephemeral.** Proposals typically include an `expires_at` timestamp. After expiration, the seller returns [`PROPOSAL_EXPIRED`](/dist/docs/3.2.0-beta.0/building/verification/compliance-catalog#error-code-proposal-expired).
* **Wholesale product IDs are scoped stable identifiers.** A non-custom product keeps the same ID for the same logical offer within the seller and declared `cache_scope`, including across reads and webhooks. Feed and pricing versions communicate mutation; retirement or replacement may end that identity.
* **Targeting-aware custom product IDs are ephemeral configurations.** A request-specific product issued by this flow sets `is_custom: true`, is usable only in the authenticated account and discovery/refinement lineage that issued it, and includes `expires_at`. The generic Product schema remains compatible with earlier custom products that omitted expiry. After expiration, a seller that still recognizes the caller-authorized ID returns [`PRODUCT_EXPIRED`](/dist/docs/3.2.0-beta.0/building/verification/compliance-catalog#error-code-product-expired); after the seller no longer retains a tombstone—or when the ID is outside the caller's account/lineage—it returns [`PRODUCT_NOT_FOUND`](/dist/docs/3.2.0-beta.0/building/verification/compliance-catalog#error-code-product-not-found) without revealing whether another tenant has that ID.

## Seller Declines

A seller that understands a well-formed `brief` or `refine` request but deliberately declines to offer products returns the `GetProductsRejected` arm. This is a successful business outcome, not a malformed request or technical failure:

```json theme={null}
{
  "status": "rejected",
  "reason": "The requested budget is below the minimum for this inventory.",
  "suggestions": [
    "Increase the campaign budget.",
    "Consider display inventory for this flight."
  ]
}
```

| Situation                                                                                  | Response                                                                                                                                                                                   |
| ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Search completed and no inventory matched; seller takes no position                        | `status: "completed"`, `products: []`                                                                                                                                                      |
| Structured request filters excluded candidates                                             | Completed response, optionally with `filter_diagnostics`                                                                                                                                   |
| Seller understood the request and deliberately declines it                                 | `status: "rejected"`, `reason`, optional `suggestions[]`                                                                                                                                   |
| Seller needs clarification                                                                 | `status: "input-required"`                                                                                                                                                                 |
| Seller accepted work that will finish later                                                | `status: "submitted"` / `"working"`                                                                                                                                                        |
| Seller could not produce a usable answer because a dependency timed out or was unavailable | [`SERVICE_UNAVAILABLE`](/dist/docs/3.2.0-beta.0/building/verification/compliance-catalog#error-code-service-unavailable), normally with `recovery: "transient"` and optional `retry_after` |

`reason` and `suggestions[]` MAY be sanitized to protect confidential merchandising, inventory, policy, and partner rules. Sellers MUST NOT include internal rule text, candidate product identifiers, stack traces, credentials, or private upstream details. Buyers MUST treat these values as untrusted seller-authored plain text: escape them before rendering and sanitize or isolate them before including them in an LLM prompt context.

Retry behavior belongs to the error channel, not this arm or `incomplete[]`. In particular, an upstream outage that leaves no usable product result is a technical failure, normally `SERVICE_UNAVAILABLE` with `error.recovery: "transient"`; buyers then follow `retry_after` or exponential backoff. A rejection without `suggestions[]` is final for that brief, and a rejection with suggestions invites a changed brief rather than an unchanged automatic retry.

The rejection arm carries no `products`, `proposals`, `incomplete`, `filter_diagnostics`, `refinement_applied`, `cache_scope`, `errors`, or envelope-level `adcp_error`. HTTP and MCP keep success markers (`200`, `isError: false`). A2A maps the transport task to `completed`; the AdCP artifact payload carries `status: "rejected"`:

```json theme={null}
{
  "status": { "state": "TASK_STATE_COMPLETED" },
  "artifacts": [
    {
      "parts": [
        {
          "data": {
            "status": "rejected",
            "reason": "The requested budget is below the minimum for this inventory."
          }
        }
      ]
    }
  ]
}
```

## Error Handling

| Error Code                                                                                                                             | Description                                                                                                           | Resolution                                                                                                                                                                               |
| -------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`AUTH_MISSING`](/dist/docs/3.2.0-beta.0/building/verification/compliance-catalog#error-code-auth-missing)                             | No credentials presented                                                                                              | Provide credentials via auth header                                                                                                                                                      |
| [`AUTH_INVALID`](/dist/docs/3.2.0-beta.0/building/verification/compliance-catalog#error-code-auth-invalid)                             | Credentials rejected (expired / revoked)                                                                              | Human credential rotation required; do not auto-retry                                                                                                                                    |
| [`INVALID_REQUEST`](/dist/docs/3.2.0-beta.0/building/verification/compliance-catalog#error-code-invalid-request)                       | Brief too long or malformed filters                                                                                   | Check request parameters                                                                                                                                                                 |
| [`PRODUCT_NOT_FOUND`](/dist/docs/3.2.0-beta.0/building/verification/compliance-catalog#error-code-product-not-found)                   | One or more referenced product IDs are unknown                                                                        | Remove invalid IDs and retry, or re-discover with a `brief` request                                                                                                                      |
| [`PRODUCT_EXPIRED`](/dist/docs/3.2.0-beta.0/building/verification/compliance-catalog#error-code-product-expired)                       | One or more recognized configured products passed `expires_at`                                                        | Re-discover with a new `brief` or `wholesale` request                                                                                                                                    |
| [`PROPOSAL_EXPIRED`](/dist/docs/3.2.0-beta.0/building/verification/compliance-catalog#error-code-proposal-expired)                     | A referenced proposal ID has passed its `expires_at` timestamp                                                        | Re-discover with a new `brief` or `wholesale` request                                                                                                                                    |
| [`PROPOSAL_NOT_FOUND`](/dist/docs/3.2.0-beta.0/building/verification/compliance-catalog#error-code-proposal-not-found)                 | The referenced `proposal_id` is unknown to the seller (never issued, wrong tenant, or evicted from cache)             | Re-issue `get_products` in `brief` or `wholesale` mode to obtain a fresh draft; finalize that recognized proposal before creating the media buy                                          |
| [`MULTI_FINALIZE_UNSUPPORTED`](/dist/docs/3.2.0-beta.0/building/verification/compliance-catalog#error-code-multi-finalize-unsupported) | `refine[]` carried multiple `action: 'finalize'` entries but the seller cannot guarantee atomic multi-proposal commit | Sequence single-proposal finalize calls — one finalize entry per `get_products` call                                                                                                     |
| [`POLICY_VIOLATION`](/dist/docs/3.2.0-beta.0/building/verification/compliance-catalog#error-code-policy-violation)                     | The request itself violates an applicable policy and must be corrected                                                | Review the structured policy details and revise the request. A seller making a deliberate business decision to decline an otherwise well-formed brief uses `status: "rejected"` instead. |

### Authentication Comparison

See the difference between authenticated and unauthenticated access:

<CodeGroup>
  ```javascript JavaScript theme={null}
  import { testAgent, testAgentNoAuth } from '@adcp/sdk/testing';

  // WITH authentication - full product results with pricing
  const fullProducts = await testAgent.getProducts({
    idempotency_key: '550e8400-e29b-41d4-a716-446655441028',
    buying_mode: 'brief',
    brief: 'Premium CTV inventory for brand awareness',
    brand: {
      domain: 'acmecorp.com'
    }
  });

  if (!fullProducts.success) {
    throw new Error(`Failed to get products: ${fullProducts.error}`);
  }

  console.log(`With auth: ${fullProducts.data.products.length} products`);
  console.log(`First product pricing: ${fullProducts.data.products[0].pricing_options.length} options`);

  // WITHOUT authentication - limited public product results
  const publicProducts = await testAgentNoAuth.getProducts({
    idempotency_key: '550e8400-e29b-41d4-a716-446655441029',
    buying_mode: 'brief',
    brief: 'Premium CTV inventory for brand awareness',
    brand: {
      domain: 'acmecorp.com'
    }
  });

  if (!publicProducts.success) {
    throw new Error(`Failed to get products: ${publicProducts.error}`);
  }

  console.log(`Without auth: ${publicProducts.data.products.length} products`);
  console.log(`First product pricing: ${publicProducts.data.products[0].pricing_options?.length || 0} options`);
  ```

  ```python Python theme={null}
  import asyncio
  from adcp.testing import test_agent, test_agent_no_auth

  async def compare_auth():
      # WITH authentication - full product results with pricing
      full_products = await test_agent.simple.get_products(
          idempotency_key='550e8400-e29b-41d4-a716-446655441030',
          buying_mode='brief',
          brief='Premium CTV inventory for brand awareness',
          brand={
              'domain': 'acmecorp.com'
          }
      )

      print(f"With auth: {len(full_products['products'])} products")
      print(f"First product pricing: {len(full_products['products'][0]['pricing_options'])} options")

      # WITHOUT authentication - limited public product results
      public_products = await test_agent_no_auth.simple.get_products(
          idempotency_key='550e8400-e29b-41d4-a716-446655441031',
          buying_mode='brief',
          brief='Premium CTV inventory for brand awareness',
          brand={
              'domain': 'acmecorp.com'
          }
      )

      print(f"Without auth: {len(public_products['products'])} products")
      print(f"First product pricing: {len(public_products['products'][0].get('pricing_options', []))} options")

  asyncio.run(compare_auth())
  ```
</CodeGroup>

**Key Differences:**

* **Product Count**: Authenticated access returns more products, including private/custom offerings
* **Pricing Information**: Only authenticated requests receive detailed pricing options (CPM, CPCV, etc.)
* **Targeting Details**: Custom targeting capabilities may be restricted to authenticated users
* **Rate Limits**: Unauthenticated requests have lower rate limits

## Authentication Behavior

* **Without credentials**: Returns limited public product results, no pricing, no custom offerings
* **With credentials**: Returns complete product results with pricing and custom products

See [Authentication Guide](/dist/docs/3.2.0-beta.0/building/by-layer/L2/authentication) for details.

## Asynchronous Operations

Most product searches complete immediately, but some scenarios require asynchronous processing. When this happens, you'll receive a status other than `completed`. A `submitted` response with `task_id` is always pollable through [`get_task_status`](https://adcontextprotocol.org/schemas/3.2.0-beta.0/protocol/get-task-status-request.json) (legacy `tasks/get`); `push_notification_config` adds webhook notification for background workflows.

#### SDK Status Handling

```typescript theme={null}
import { randomUUID } from 'node:crypto';

const params = {
  idempotency_key: randomUUID(),
  buying_mode: 'brief',
  brief: 'Premium CTV inventory for brand awareness',
};

const initial = await agent.getProducts(params);
const final =
  initial.status === 'submitted'
    ? await initial.submitted!.waitForCompletion(30000)
    : initial;

if (final.status === 'failed') {
  throw new Error(final.error?.message ?? 'get_products failed');
} else if (final.status === 'rejected') {
  console.log(`Seller declined the brief: ${final.reason}`);
  console.log(final.suggestions ?? []);
} else if (final.status !== 'completed') {
  throw new Error(`Unhandled get_products status: ${final.status}`);
} else {
  for (const product of final.products) {
    console.log(product.name);
  }
}
```

### When Search Runs Asynchronously

Product search may require async processing in these situations:

* **Complex searches**: Searching across multiple inventory sources or custom curation
* **Needs clarification**: Your brief is vague and the system needs more information
* **Custom products**: Bespoke product packages that require human review

### Async Status Flow

<Tabs>
  <Tab title="MCP">
    #### Immediate Completion (Most Common)

    ```json theme={null}
    POST /api/mcp/call_tool

    {
      "name": "get_products",
      "arguments": {
        "idempotency_key": "550e8400-e29b-41d4-a716-446655441032",
        "buying_mode": "brief",
        "brief": "CTV inventory for sports audience",
        "brand": { "domain": "acmecorp.com" }
      }
    }

    Response (200 OK):
    {
      "status": "completed",
      "message": "Found 3 products matching your requirements",
      "products": [...]
    }
    ```

    #### Needs Clarification

    When the brief is unclear, the system asks for more details:

    ```json theme={null}
    Response (200 OK):
    {
      "status": "input-required",
      "message": "I need a bit more information. What's your budget range and campaign duration?",
      "task_id": "task_789",
      "context_id": "ctx_123",
      "reason": "CLARIFICATION_NEEDED",
      "partial_results": [],
      "suggestions": ["$50K-$100K", "1 month", "Q1 2024"]
    }
    ```

    Continue the conversation with the same `context_id`:

    ```json theme={null}
    POST /api/mcp/continue

    {
      "context_id": "ctx_123",
      "message": "Budget is $75K for a 3-week campaign in March"
    }

    Response (200 OK):
    {
      "status": "completed",
      "message": "Perfect! Found 5 products within your budget",
      "products": [...]
    }
    ```

    #### Complex Search (With Webhook and Polling)

    For searches requiring deep inventory analysis, configure a webhook for terminal completion/failure notification. The returned `task_id` remains valid for polling via `get_task_status` (legacy `tasks/get`).

    ```json theme={null}
    POST /api/mcp/call_tool

    {
      "name": "get_products",
      "arguments": {
        "idempotency_key": "550e8400-e29b-41d4-a716-446655441033",
        "buying_mode": "brief",
        "brief": "Premium inventory across all formats for luxury automotive brand",
        "brand": { "domain": "acmecorp.com" },
        "push_notification_config": {
          "url": "https://buyer.com/webhooks/adcp/get_products",
          "authentication": {
            "schemes": ["Bearer"],
            "credentials": "secret_token_32_chars"
          }
        }
      }
    }

    Response (200 OK):
    {
      "status": "submitted",
      "message": "Custom curation queued; typical turnaround 10-30 minutes",
      "task_id": "task_456",
      "context_id": "ctx_123",
      "estimated_completion": "2025-01-22T10:30:00Z"
    }

    // Later, poll get_task_status/tasks/get with task_id, or receive webhook POST to https://buyer.com/webhooks/adcp/get_products
    {
      "task_id": "task_456",
      "task_type": "get_products",
      "status": "completed",
      "timestamp": "2025-01-22T10:30:00Z",
      "message": "Found 12 premium products across all formats",
      "result": {
        "products": [...]
      }
    }
    ```
  </Tab>

  <Tab title="A2A">
    #### Immediate Completion (Most Common)

    Send the profile URI in `A2A-Extensions` and use the structured invocation body:

    ```json theme={null}
    {
      "message": {
        "messageId": "msg-get-products-001",
        "role": "ROLE_USER",
        "parts": [{
          "data": {
            "skill": "get_products",
            "input": {
              "idempotency_key": "550e8400-e29b-41d4-a716-446655441034",
              "buying_mode": "brief",
              "brief": "CTV inventory for sports audience",
              "brand": { "domain": "acmecorp.com" }
            }
          }
        }]
      }
    }
    ```

    The non-streaming `SendMessage` response selects its `task` branch. The Task is
    completed and carries the direct AdCP response in its artifact:

    ```json theme={null}
    {
      "task": {
        "id": "a2a-task-123",
        "contextId": "ctx_456",
        "status": { "state": "TASK_STATE_COMPLETED" },
        "artifacts": [{
          "artifactId": "adcp-result",
          "parts": [
            { "text": "Found 3 products matching your requirements" },
            { "data": { "status": "completed", "cache_scope": "account", "products": [] } }
          ]
        }]
      }
    }
    ```

    #### Needs Clarification

    Real-time updates via SSE when clarification is needed:

    ```json theme={null}
    {
      "task": {
        "id": "task_789",
        "contextId": "ctx_123",
        "status": {
          "state": "TASK_STATE_INPUT_REQUIRED",
          "message": {
            "messageId": "msg-clarification-001",
            "taskId": "task_789",
            "contextId": "ctx_123",
            "role": "ROLE_AGENT",
            "parts": [
              { "text": "I need a bit more information. What's your budget range and campaign duration?" },
              {
                "data": {
                  "reason": "CLARIFICATION_NEEDED",
                  "suggestions": ["$50K-$100K", "1 month", "Q1 2024"]
                }
              }
            ]
          }
        }
      }
    }
    ```

    Send a new activated profile invocation in the same context, with the refined typed request:

    ```json theme={null}
    {
      "message": {
        "messageId": "msg-get-products-002",
        "taskId": "task_789",
        "contextId": "ctx_123",
        "role": "ROLE_USER",
        "parts": [{
          "data": {
            "skill": "get_products",
            "input": {
              "buying_mode": "brief",
              "brief": "CTV sports inventory with a $75K budget for three weeks in March"
            }
          }
        }]
      }
    }
    ```

    The resulting Task uses the same completed-artifact mapping as the immediate response:

    ```json theme={null}
    {
      "task": {
        "id": "task_789",
        "contextId": "ctx_123",
        "status": { "state": "TASK_STATE_COMPLETED" },
        "artifacts": [{
          "artifactId": "adcp-result",
          "parts": [
            { "text": "Found 5 products within your budget" },
            { "data": { "status": "completed", "cache_scope": "account", "products": [] } }
          ]
        }]
      }
    }
    ```

    #### Complex Search (AdCP Submitted and Polling)

    Activate the [AdCP A2A Profile Extension v3](/dist/docs/3.2.0-beta.0/building/by-layer/L0/a2a-profile-extension) on the request:

    ```http theme={null}
    POST /api/a2a HTTP/1.1
    A2A-Version: 1.0
    A2A-Extensions: https://adcontextprotocol.org/extensions/adcp/v3
    Content-Type: application/json
    ```

    ```json theme={null}
    {
      "message": {
        "messageId": "msg-get-products-003",
        "role": "ROLE_USER",
        "parts": [{
          "data": {
            "skill": "get_products",
            "input": {
              "idempotency_key": "550e8400-e29b-41d4-a716-446655441035",
              "buying_mode": "brief",
              "brief": "Premium inventory across all formats for a luxury automotive brand",
              "brand": { "domain": "acmecorp.com" }
            }
          }
        }]
      }
    }
    ```

    If custom curation is queued, the A2A invocation is still complete. The AdCP Submitted response and its durable handle are inside the artifact DataPart:

    ```json theme={null}
    {
      "task": {
        "id": "a2a-task-456",
        "contextId": "ctx_789",
        "status": { "state": "TASK_STATE_COMPLETED" },
        "artifacts": [{
          "artifactId": "adcp-result",
          "parts": [{
            "data": {
              "status": "submitted",
              "task_id": "adcp-task-789",
              "message": "Custom curation queued; typical turnaround 10-30 minutes"
            }
          }]
        }]
      }
    }
    ```

    Later, send a fresh activated invocation of `get_task_status`. Do not poll `a2a-task-456`; it identifies the already-completed transport invocation.

    ```json theme={null}
    {
      "message": {
        "messageId": "msg-get-products-poll-001",
        "role": "ROLE_USER",
        "parts": [{
          "data": {
            "skill": "get_task_status",
            "input": {
              "task_id": "adcp-task-789",
              "include_result": true
            }
          }
        }]
      }
    }
    ```
  </Tab>
</Tabs>

### Status Overview

| Status           | When It Happens                                          | What You Do                                                                                                        |
| ---------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `completed`      | Search finished successfully                             | Process the product results                                                                                        |
| `input-required` | Need clarification on the brief                          | Answer the question and continue                                                                                   |
| `working`        | Searching across multiple sources                        | Wait on the open connection / transport progress stream                                                            |
| `submitted`      | Custom curation queued                                   | Poll `get_task_status` with the AdCP `task_id`; on MCP, the seller may advertise the legacy AdCP `tasks/get` alias |
| `rejected`       | Seller understood the brief but deliberately declined it | Read the sanitized reason; revise and resubmit only when `suggestions[]` offers a recovery path                    |
| `failed`         | Search couldn't complete                                 | Check error message, adjust brief                                                                                  |

**Note:** For the complete status list see [Task Lifecycle](/dist/docs/3.2.0-beta.0/building/by-layer/L3/task-lifecycle).

**Most searches complete immediately.** Async processing is only needed for complex scenarios or when the system needs your input.

## Next Steps

After discovering products:

1. **Review Options**: Compare products, pricing, and targeting capabilities
2. **Create Media Buy**: Use [`create_media_buy`](/dist/docs/3.2.0-beta.0/media-buy/task-reference/create_media_buy) to execute campaign
3. **Prepare Creatives**: Use each returned product's canonical `format_options[]` to see format requirements
4. **Supply Assets**: Use [`sync_creatives`](/dist/docs/3.2.0-beta.0/creative/task-reference/sync_creatives) for library-backed sellers, or inline `packages[].creatives` for inline-only sellers

## Learn More

* [Product Discovery Guide](/dist/docs/3.2.0-beta.0/media-buy/product-discovery/) - Understanding briefs and products
* [Pricing Models](/dist/docs/3.2.0-beta.0/media-buy/advanced-topics/pricing-models) - CPM, CPCV, CPP explained
* [Brief Expectations](/dist/docs/3.2.0-beta.0/media-buy/product-discovery/brief-expectations) - How to write effective briefs
* [Media Products](/dist/docs/3.2.0-beta.0/media-buy/product-discovery/media-products) - Product structure and fields
