Skip to main content
Discover available advertising products based on campaign requirements using natural language briefs or structured filters.
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.
Authentication: Optional (returns limited results without credentials) Response Time: ~60 seconds (AI inference with back-end systems) Request Schema: /schemas/v3/media-buy/get-products-request.json Response Schema: /schemas/v3/media-buy/get-products-response.json

Quick Start

Discover products with a natural language brief:

Using Structured Filters

You can also use structured filters instead of (or in addition to) a brief. In brief mode, filters act as hard constraints on top of the publisher’s curation — the brief describes intent, filters enforce requirements:

Request Parameters

Property GovernanceThe property_list filter references a property list created via 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.To use property list filtering:
  1. Call 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 property_list_id to get_products to filter inventory
The seller must declare features.property_list_filtering: true in get_adcp_capabilities to support this filter. See the Property Governance overview for the full workflow.

Filters Object

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: 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. Signal-targeting filter example:

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, returned product pricing_options MUST satisfy both filters: the option must be fixed-price when requested and its currency must be in pricing_currencies. Currency-only filter example:
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

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

scope: “product”

scope: “proposal”

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:

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 object used throughout AdCP. You can reference a synced catalog by catalog_id, provide inline items, or use selectors to 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.

Products Array

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:

Proposals Array (Optional)

Publishers may return proposals alongside products - structured media plans with budget allocations. See Proposals for details. 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: 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

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.

incomplete array

When the seller 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. Buyers can use estimated_wait to decide whether to retry with a larger budget.

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 feed; sync_catalogs manages buyer-provided campaign input feeds on the seller account. Unchanged response example: Request:
Response (wholesale product feed unchanged):
Response (wholesale product feed changed — full payload returned, abbreviated):
test=false
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, filters, buying_mode, 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_ids, 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. 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

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:
A response with incomplete data — products are returned but some scopes are missing:
test=false

Wholesale Product Discovery

Multi-Format Discovery

Budget and Date Filtering

Property Tag Resolution

Guaranteed Delivery Products

Standard Formats Only

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:
You can also use GTIN matching, reference a synced catalog, or discover products for other catalog types:

Property List Filtering

AdCP 3.0 - Property list filtering requires governance agent support.
Filter products to only those available on properties in your approved list:
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 for more details and Property Governance 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 for the full walkthrough: scope types, action semantics, seller responses, and common patterns. The parameter shape is defined in the Refine array section above. Minimal example:
test=false
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.
  • 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.
  • Product IDs are stable catalog identifiers. Custom products (is_custom: true) may have an expires_at timestamp, after which refinement returns PRODUCT_NOT_FOUND.

Error Handling

Authentication Comparison

See the difference between authenticated and unauthenticated access:
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 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 (legacy tasks/get); push_notification_config adds webhook notification for background workflows.

SDK Status Handling

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

Immediate Completion (Most Common)

Needs Clarification

When the brief is unclear, the system asks for more details:
Continue the conversation with the same context_id:

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

Status Overview

Note: For the complete status list see 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 to execute campaign
  3. Prepare Creatives: Use list_creative_formats to see format requirements
  4. Supply Assets: Use sync_creatives for library-backed sellers, or inline packages[].creatives for inline-only sellers

Learn More