Documentation Index
Fetch the complete documentation index at: https://agenticadvertisingorg-changeset-release-main.mintlify.app/llms.txt
Use this file to discover all available pages before exploring further.
Task: Discover signals based on description, with details about where they are deployed.
Response Time: ~60 seconds (inference/RAG with back-end systems)
Request Schema: https://adcontextprotocol.org/schemas/v3/signals/get-signals-request.json
Response Schema: https://adcontextprotocol.org/schemas/v3/signals/get-signals-response.json
The get_signals task returns both signal metadata and real-time deployment status across platforms, allowing agents to understand availability and guide the activation process.
Request Parameters
| Parameter | Type | Required | Description |
|---|
discovery_mode | string | No (v3.1+ SHOULD include) | "brief" (default) or "wholesale". "brief": existing behavior — signal_spec or signal_ids is required, agent performs inference/RAG. "wholesale": raw wholesale signals feed enumeration — signal_spec and signal_ids MUST NOT be provided; agent returns its full priced signals feed, paginated, scoped by filters / account / destinations / countries when present. Timing semantics: "wholesale" is a wholesale signals feed read — agents SHOULD respond synchronously and MUST NOT route through the async/Submitted arm; partial completion uses incomplete[]. Agents receiving requests from pre-v3.1 clients without discovery_mode MUST default to "brief". Probe support via get_adcp_capabilities (signals.discovery_modes). |
signal_spec | string | Conditional | Natural language description of the desired signals. Required when discovery_mode is "brief" (and signal_ids is absent). MUST NOT be provided when discovery_mode is "wholesale". |
signal_ids | SignalID[] | Conditional | Specific signals to look up by data provider and ID. Required when discovery_mode is "brief" (and signal_spec is absent). MUST NOT be provided when discovery_mode is "wholesale". |
account | AccountRef | No | Account for this request. When provided, the signals agent returns per-account pricing options if configured. In wholesale mode, this is the rate-card scope; when omitted the agent returns default rate-card pricing or omits pricing_options entirely. |
destinations | Destination[] | No | Filter signals to those activatable on specific agents/platforms. When omitted, returns all signals available on the current agent. See Destination Object below. |
countries | string[] | No | Countries where signals will be used (ISO 3166-1 alpha-2 codes) |
filters | Filters | No | Filters to refine results (see Filters Object below). In wholesale mode, filters constrain the enumerated signals feed (e.g., filters.data_providers: ["acme-data"] returns only that provider’s signals). |
if_wholesale_feed_version | string | No | Opaque wholesale_feed_version token from a prior get_signals response. When provided, the agent compares against its current wholesale signals feed version for the caller’s cache_scope and MAY return unchanged: true (with signals omitted) if nothing has changed. See Wholesale feed versioning and Cache layering. |
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 caller sees updated pricing_options); both match → agent MAY return unchanged: true. Agents that don’t track pricing separately ignore this. |
max_results | number | No | Deprecated. Use pagination.max_results instead. When both are present, pagination.max_results takes precedence. Will be removed in AdCP 4.0. |
pagination | object | No | Pagination envelope. pagination.max_results (max: 100, default: 50) controls page size; pagination.cursor (opaque token from previous response) advances pages. Required for wholesale (signals feeds may be large). |
Destination Object
Each deployment target uses a type field to discriminate between platform-based and agent-based deployments:
| Parameter | Type | Required | Description |
|---|
type | string | Yes | Discriminator: “platform” for DSPs, “agent” for sales agents |
platform | string | Conditional* | Platform identifier (e.g., ‘the-trade-desk’, ‘amazon-dsp’). Required when type=“platform” |
agent_url | string (URI) | Conditional* | URL identifying the sales agent. Required when type=“agent” |
account | string | No | Account identifier on the platform or agent |
*platform is required when type="platform", agent_url is required when type="agent".
Destination filtering: Signals are returned if they are available on any of the requested destinations (OR semantics). Destinations where a signal is not available are omitted from that signal’s response deployments array. A PARTIAL_COVERAGE warning may be included when some destinations don’t support the signal.
Activation Keys: If the authenticated caller has access to any of the destinations in the request, the signal agent will include activation_key fields in the response for those deployments (when is_live: true).
Permission Model: The signal agent determines key inclusion based on the caller’s authentication and authorization. For example:
- A sales agent receives keys for deployments matching its
agent_url
- A buyer with credentials for multiple DSP platforms receives keys for all those deployments
- Access is determined by the signal agent’s permission system, not by flags in the request
Filters Object
| Parameter | Type | Required | Description |
|---|
catalog_types | string[] | No | Filter by catalog type (“marketplace”, “custom”, “owned”) |
data_providers | string[] | No | Filter by specific data providers |
max_cpm | number | No | Maximum CPM price filter. Excludes signals where all CPM-based pricing options exceed this value. Signals without CPM-based pricing options are not affected by this filter. |
min_coverage_percentage | number | No | Minimum coverage requirement |
Response Structure
All AdCP responses include:
- message: Human-readable summary of the operation result
- context_id: Session continuity identifier for follow-up requests
- data: Task-specific payload (see Response Data below)
The response structure is identical across protocols, with only the transport wrapper differing:
- MCP: Returns complete response as flat JSON
- A2A: Returns as artifacts with message in text part, data in data part
Response Data
{
"signals": [
{
"signal_agent_segment_id": "string",
"name": "string",
"description": "string",
"signal_type": "string",
"data_provider": "string",
"coverage_percentage": "number",
"deployments": [
{
"type": "agent",
"agent_url": "string",
"account": "string",
"is_live": "boolean",
"activation_key": {
"type": "segment_id",
"segment_id": "string"
},
"estimated_activation_duration_minutes": "number"
}
],
"pricing_options": [
{
"pricing_option_id": "string",
"model": "cpm | percent_of_media | flat_fee | per_unit | custom",
"...": "..."
}
]
}
]
}
Field Descriptions
- signals: Array of matching signals
- signal_agent_segment_id: Unique identifier for the signal
- name: Human-readable signal name
- description: Detailed signal description
- signal_type: Type of signal. One of:
marketplace — resold third-party segment (provider authorization verifiable via the provider’s adagents.json)
owned — first-party segment derived from data the signal agent directly owns
custom — agent-native segment built on demand from models, composites, or buyer inputs (not attributable to a standing upstream provider)
- data_provider: Name of the data provider
- coverage_percentage: Percentage of audience coverage
- deployments: Array of destination deployments
- agent_url: URL identifying the destination agent
- account: Account identifier if applicable
- is_live: Whether signal is currently active on this deployment
- activation_key: The key to use for targeting (see Activation Key below). Only present when
is_live=true and the authenticated caller has access to this deployment.
- estimated_activation_duration_minutes: Time to activate if not live
- pricing_options: Array of pricing options for this signal. Pass the selected
pricing_option_id in report_usage for billing verification.
- pricing_option_id: Unique identifier for this pricing option
- model: Pricing model —
cpm, percent_of_media, flat_fee, per_unit, or custom
model: "cpm" — cpm (number, cost per thousand impressions), currency (ISO 4217)
model: "percent_of_media" — percent (0–100), currency (ISO 4217), max_cpm (optional CPM cap: effective charge = min(percent × media_spend_per_mille, max_cpm))
model: "flat_fee" — amount (fixed charge), currency (ISO 4217), period (monthly, quarterly, annual, or campaign)
model: "per_unit" — unit (what is counted), unit_price (cost per one unit), currency (ISO 4217)
model: "custom" — description (human-readable), metadata (structured parameters), currency (optional). Escape hatch for performance kickers, tiered volume, hybrid formulas, or any construct the standard models cannot express. Buyers SHOULD route custom pricing through operator review before commitment.
Select the pricing option that matches your billing model and pass its pricing_option_id in report_usage for billing verification. If a signal offers multiple models (e.g., CPM and flat fee), choose based on your expected delivery volume and campaign structure.
| Field | Type | Description |
|---|
wholesale_feed_version | string | Opaque token representing the version of the wholesale signals feed state used to compose this response. Treat as opaque — no format, no ordering, no inspection. Returned on every response by agents implementing conditional-fetch. See Wholesale feed versioning. |
pricing_version | string | Optional finer-grained token. Changes when prices move while wholesale_feed_version only changes for structure/metadata. Agents not separating these MAY omit pricing_version. |
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 agent declares either "public" (account prices off the rate card — caller dedupes) or "account" (account-specific overrides). See Cache layering. |
unchanged | boolean | Present and true ONLY when the request carried if_wholesale_feed_version (and/or if_pricing_version) matching the agent’s current version for the caller’s cache_scope, in which case signals[] MUST be omitted; wholesale_feed_version, cache_scope, and pricing_version (when used) MUST still be echoed. Agents MUST NOT emit unchanged: false — absence of the field IS the “response carries signals” signal (one shape per state). Callers receiving unchanged: true MUST NOT mutate their local wholesale signals mirror. |
incomplete | IncompleteEntry[] | Declares what the agent could not finish within the caller’s time_budget or due to internal limits. See Incomplete array. |
pagination | PaginationResponse | has_more, cursor, optional total_count. Required for wholesale mode. |
Activation Key Object
The activation key represents how to use the signal on a deployment target. It can be either a segment ID or a key-value pair:
Segment ID format:
{
"type": "segment_id",
"segment_id": "ttd_segment_12345"
}
Key-Value format:
{
"type": "key_value",
"key": "audience_segment",
"value": "luxury_auto_intenders"
}
Protocol-Specific Examples
The AdCP payload is identical across protocols. Only the request/response wrapper differs.
MCP Request - Sales Agent Requesting Signals
A sales agent querying for signals. Because the authenticated caller is wonderstruck.salesagents.com, the signal agent will include activation keys in the response:
{
"tool": "get_signals",
"arguments": {
"signal_spec": "High-income households interested in luxury goods",
"destinations": [
{
"type": "agent",
"agent_url": "https://wonderstruck.salesagents.com"
}
],
"countries": ["US"],
"filters": {
"max_cpm": 5.0,
"catalog_types": ["marketplace"]
},
"pagination": {
"max_results": 5
}
}
}
MCP Response - With Activation Key
Because the authenticated caller matches the deployment target, the response includes the activation key:
{
"$schema": "/schemas/signals/get-signals-response.json",
"status": "completed",
"message": "Found 1 luxury segment matching your criteria. Already activated for your sales agent.",
"context_id": "ctx-signals-123",
"cache_scope": "public",
"wholesale_feed_version": "sig_v2026-05-18T08:00:00Z-public-rev12",
"signals": [
{
"signal_id": {
"source": "catalog",
"data_provider_domain": "experian.com",
"id": "luxury_auto_intenders"
},
"signal_agent_segment_id": "luxury_auto_intenders",
"name": "Luxury Automotive Intenders",
"description": "High-income individuals researching luxury vehicles",
"signal_type": "marketplace",
"data_provider": "Experian",
"coverage_percentage": 12,
"deployments": [
{
"type": "agent",
"agent_url": "https://wonderstruck.salesagents.com",
"is_live": true,
"activation_key": {
"type": "key_value",
"key": "audience_segment",
"value": "luxury_auto_intenders_v2"
}
}
],
"pricing_options": [
{
"pricing_option_id": "po_cpm_usd",
"model": "cpm",
"cpm": 3.50,
"currency": "USD"
}
]
}
]
}
MCP Response - Multiple Pricing Options
Some signals offer multiple pricing models. The buyer selects one and passes its pricing_option_id in report_usage:
{
"$schema": "/schemas/signals/get-signals-response.json",
"status": "completed",
"message": "Found 1 segment matching your criteria. Three pricing options are available: CPM at $3.50, 15% of media spend, or $5,000/month flat fee.",
"context_id": "ctx-signals-456",
"cache_scope": "public",
"wholesale_feed_version": "sig_v2026-05-18T08:00:00Z-public-rev12",
"signals": [
{
"signal_id": {
"source": "catalog",
"data_provider_domain": "acmedata.com",
"id": "eco_conscious_shoppers"
},
"signal_agent_segment_id": "eco_conscious_shoppers",
"name": "Eco-Conscious Shoppers",
"description": "Users with demonstrated interest in sustainable and eco-friendly products",
"signal_type": "marketplace",
"data_provider": "Acme Data",
"coverage_percentage": 18,
"deployments": [
{
"type": "agent",
"agent_url": "https://wonderstruck.salesagents.com",
"is_live": true,
"activation_key": {
"type": "segment_id",
"segment_id": "eco_seg_789"
}
}
],
"pricing_options": [
{
"pricing_option_id": "po_eco_cpm",
"model": "cpm",
"cpm": 3.50,
"currency": "USD"
},
{
"pricing_option_id": "po_eco_pom",
"model": "percent_of_media",
"percent": 15,
"max_cpm": 1.50,
"currency": "USD"
},
{
"pricing_option_id": "po_eco_flat",
"model": "flat_fee",
"amount": 5000,
"period": "monthly",
"currency": "USD"
}
]
}
]
}
A buyer checking availability across multiple DSP platforms:
{
"tool": "get_signals",
"arguments": {
"signal_spec": "High-income households interested in luxury goods",
"destinations": [
{
"type": "platform",
"platform": "the-trade-desk",
"account": "agency-123"
},
{
"type": "platform",
"platform": "amazon-dsp"
}
],
"countries": ["US"],
"filters": {
"max_cpm": 5.0,
"catalog_types": ["marketplace"]
},
"pagination": {
"max_results": 5
}
}
}
A buyer with credentials for both The Trade Desk and Amazon DSP receives keys for both platforms:
{
"$schema": "/schemas/signals/get-signals-response.json",
"status": "completed",
"message": "Found 1 luxury segment matching your criteria. Already activated on The Trade Desk, pending activation on Amazon DSP.",
"context_id": "ctx-signals-123",
"cache_scope": "public",
"wholesale_feed_version": "sig_v2026-05-18T08:00:00Z-public-rev12",
"signals": [
{
"signal_id": {
"source": "catalog",
"data_provider_domain": "experian.com",
"id": "luxury_auto_intenders"
},
"signal_agent_segment_id": "luxury_auto_intenders",
"name": "Luxury Automotive Intenders",
"description": "High-income individuals researching luxury vehicles",
"signal_type": "marketplace",
"data_provider": "Experian",
"coverage_percentage": 12,
"deployments": [
{
"type": "platform",
"platform": "the-trade-desk",
"account": "agency-123",
"is_live": true,
"activation_key": {
"type": "segment_id",
"segment_id": "ttd_agency123_exp_lux_auto"
}
},
{
"type": "platform",
"platform": "amazon-dsp",
"is_live": false,
"estimated_activation_duration_minutes": 60
}
],
"pricing_options": [
{
"pricing_option_id": "po_cpm_usd",
"model": "cpm",
"cpm": 3.50,
"currency": "USD"
}
]
}
]
}
A2A Request
Natural Language Invocation
await a2a.send({
message: {
parts: [{
kind: "text",
text: "Find me signals for high-income households interested in luxury goods that can be deployed on The Trade Desk and Amazon DSP in the US, with a maximum CPM of $5.00."
}]
}
});
Explicit Skill Invocation
await a2a.send({
message: {
parts: [{
kind: "data",
data: {
skill: "get_signals",
parameters: {
signal_spec: "High-income households interested in luxury goods",
destinations: [
{
type: "agent",
agent_url: "https://thetradedesk.com",
account: "agency-123"
},
{
type: "agent",
agent_url: "https://advertising.amazon.com/dsp"
}
],
countries: ["US"],
filters: {
max_cpm: 5.0,
catalog_types: ["marketplace"]
},
pagination: {
max_results: 5
}
}
}
}]
}
});
A2A Response
A2A returns results as artifacts with the same data structure:
{
"artifacts": [{
"artifactId": "artifact-signal-discovery-def456",
"name": "signal_discovery_result",
"parts": [
{
"kind": "text",
"text": "Found 1 luxury segment matching your criteria. Available on The Trade Desk, pending activation on Amazon DSP."
},
{
"kind": "data",
"data": {
"context_id": "ctx-signals-123",
"signals": [
{
"signal_agent_segment_id": "luxury_auto_intenders",
"name": "Luxury Automotive Intenders",
"description": "High-income individuals researching luxury vehicles",
"signal_type": "marketplace",
"data_provider": "Experian",
"coverage_percentage": 12,
"deployments": [
{
"type": "agent",
"agent_url": "https://thetradedesk.com",
"account": "agency-123",
"is_live": true
},
{
"type": "agent",
"agent_url": "https://advertising.amazon.com/dsp",
"is_live": false,
"estimated_activation_duration_minutes": 60
}
],
"pricing_options": [
{
"pricing_option_id": "po_cpm_usd",
"model": "cpm",
"cpm": 3.50,
"currency": "USD"
}
]
}
]
}
}
]
}]
}
Protocol Transport
- MCP: Direct tool call with arguments, returns complete response as flat JSON
- A2A: Skill invocation with input, returns structured artifacts with message and data separated
- Data Consistency: Both protocols contain identical AdCP data structures and version information
Scenarios
Discover all available deployments across platforms:
{
"$schema": "/schemas/signals/get-signals-request.json",
"signal_spec": "Contextual segments for luxury automotive content",
"destinations": [
{ "type": "platform", "platform": "index-exchange", "account": "agency-123-ix" },
{ "type": "platform", "platform": "openx" },
{ "type": "platform", "platform": "pubmatic", "account": "brand-456-pm" }
],
"countries": ["US"],
"filters": {
"data_providers": ["Peer39"],
"catalog_types": ["marketplace"]
}
}
Response
Message: “Found luxury automotive contextual segment from Peer39 with 15% coverage. Live on Index Exchange and OpenX, pending activation on Pubmatic.”
Payload:
{
"$schema": "/schemas/signals/get-signals-response.json",
"status": "completed",
"cache_scope": "public",
"wholesale_feed_version": "sig_v2026-05-18T08:00:00Z-public-rev12",
"signals": [{
"signal_id": {
"source": "catalog",
"data_provider_domain": "peer39.com",
"id": "peer39_luxury_auto"
},
"signal_agent_segment_id": "peer39_luxury_auto",
"name": "Luxury Automotive Context",
"description": "Pages with luxury automotive content and high viewability",
"signal_type": "marketplace",
"data_provider": "Peer39",
"coverage_percentage": 15,
"deployments": [
{
"type": "platform",
"platform": "index-exchange",
"account": "agency-123-ix",
"is_live": true,
"activation_key": {
"type": "segment_id",
"segment_id": "ix_agency123_peer39_lux_auto"
}
},
{
"type": "platform",
"platform": "index-exchange",
"is_live": true,
"activation_key": {
"type": "segment_id",
"segment_id": "ix_peer39_luxury_auto_gen"
}
},
{
"type": "platform",
"platform": "openx",
"is_live": true,
"activation_key": {
"type": "segment_id",
"segment_id": "ox_peer39_lux_auto_456"
}
},
{
"type": "platform",
"platform": "pubmatic",
"account": "brand-456-pm",
"is_live": false,
"estimated_activation_duration_minutes": 60
}
],
"pricing_options": [
{
"pricing_option_id": "po_cpm_usd",
"model": "cpm",
"cpm": 2.50,
"currency": "USD"
}
]
}]
}
Response Fields
- context_id (string): Context identifier for session persistence
- signals (array): Array of matching signals
- signal_agent_segment_id (string): Universal identifier for the signal
- name (string): Human-readable signal name
- description (string): Detailed signal description
- signal_type (string):
marketplace (resold third-party), owned (agent’s first-party data), or custom (agent-native segment built on demand)
- data_provider (string): Provider of the signal data
- coverage_percentage (number): Estimated reach percentage
- deployments (array): Platform-specific deployment information
- platform (string): Target platform name
- account (string, nullable): Specific account if account-specific
- is_live (boolean): Whether signal is currently active
- activation_key (object): The key to use for targeting. Only present when
is_live=true and the caller has access. See Activation Key Object above.
- estimated_activation_duration_minutes (number, optional): Time to activate if not live
- pricing_options (array): Array of pricing options available for this signal. Select one and pass its
pricing_option_id in report_usage.
- pricing_option_id (string): Unique identifier for this pricing option
- model (string): Pricing model —
cpm, percent_of_media, flat_fee, per_unit, or custom
Error Codes
Discovery Errors
REFERENCE_NOT_FOUND: Referenced signal_agent_segment_id doesn’t exist,
OR a private signal agent is not visible to this account. The same code is
returned whether the resource exists but is unauthorized or truly does not
exist — sellers MUST NOT distinguish the two (see error.field to
identify which typed parameter failed to resolve). See the uniform-response
MUST in error-handling.mdx.
AGENT_ACCESS_DENIED: Authenticated agent’s credentials did not authorize access to this signal agent
Discovery Warnings
PRICING_UNAVAILABLE: Pricing data temporarily unavailable for one or more platforms
PARTIAL_COVERAGE: Some requested platforms don’t support this signal type
STALE_DATA: Some signal metadata may be outdated due to provider refresh delays
Usage Notes
- Authentication-Based Keys: Activation keys are only returned when the authenticated caller matches one of the deployment targets
- Permission Security: The signal agent determines key inclusion based on caller identity, not request flags
- Deployment Status: Check
is_live to determine if activation is needed
- Multiple Deployments: Query multiple deployment targets to check availability across platforms
- Activation Required: If
is_live is false, use the activate_signal task
- The message field provides a quick summary of the most relevant findings
Wholesale signals feed
When a consumer (storefront, federated marketplace, registry) needs to mirror a signals agent’s full priced signals feed, set discovery_mode: "wholesale" and omit signal_spec / signal_ids. Wholesale enumeration is symmetric with get_products buying_mode: "wholesale": synchronous, paginated, firm-priced, with partial completion declared via incomplete[].
Request
{
"$schema": "/schemas/signals/get-signals-request.json",
"discovery_mode": "wholesale",
"account": { "account_id": "acct_123" },
"filters": {
"catalog_types": ["marketplace", "owned"],
"data_providers": ["acme-data", "nova-insights"]
},
"pagination": { "max_results": 50 }
}
Response
{
"$schema": "/schemas/signals/get-signals-response.json",
"status": "completed",
"message": "Returning 50 of 312 signals in wholesale mode.",
"context_id": "ctx-wholesale-001",
"cache_scope": "public",
"wholesale_feed_version": "v2026-05-18T08:00:00Z-acme-rev412",
"signals": [
{
"signal_id": { "source": "catalog", "data_provider_domain": "acme-data.com", "id": "luxury_auto_intenders" },
"signal_agent_segment_id": "sigagent_seg_4421",
"name": "Luxury Auto Intenders",
"description": "Households researching premium vehicles in the last 30 days.",
"signal_type": "marketplace",
"data_provider": "Acme Data",
"coverage_percentage": 18.4,
"deployments": [
{ "type": "platform", "platform": "the-trade-desk", "is_live": true,
"activation_key": { "type": "segment_id", "segment_id": "ttd_seg_99821" } }
],
"pricing_options": [
{ "pricing_option_id": "po_cpm_1", "model": "cpm", "cpm": 2.50, "currency": "USD" }
]
}
],
"pagination": { "has_more": true, "cursor": "eyJvIjo1MH0=", "total_count": 312 }
}
Authorization and provenance
Marketplace signals (signal_type: "marketplace") remain attributable to their upstream data provider. Wholesale enumeration does not collapse provenance:
- Each marketplace signal carries
data_provider.
- Consumers SHOULD verify provider authorization via the provider’s
adagents.json — the signals agent’s URL must appear in the provider’s authorization list for that signal class.
- Storefronts and registries MAY use wholesale enumeration plus
adagents.json cross-reference to materialize a data-publisher signal view: for each known data provider, the set of signals available via authorized signals agents, with prices.
Pricing
pricing_options[] MUST be populated for authenticated callers when the agent has firm prices to declare. When account is omitted, the agent returns default rate-card pricing or omits pricing_options[] (in which case the caller MUST re-query with account before composing). Unauthenticated callers MAY receive signal metadata without pricing.
Capability declaration
Signals agents declare wholesale support in get_adcp_capabilities:
{
"signals": {
"discovery_modes": ["brief", "wholesale"]
}
}
Agents not declaring "wholesale" MAY return INVALID_REQUEST for wholesale calls. The capability declaration is the canonical signal — callers SHOULD probe before issuing wholesale requests.
Wholesale feed versioning
Even with wholesale enumeration, a consumer mirroring an agent’s signals feed would re-fetch every paginated page on each poll just to detect changes. To avoid that, get_signals supports an opaque wholesale_feed_version token returned on every response. Pass it back via if_wholesale_feed_version on a subsequent call and the agent MAY short-circuit with unchanged: true — no signal payload, no per-page diff.
This is the seller-side wholesale signals feed returned by get_signals. It is not a sync_catalogs feed; sync_catalogs manages buyer-provided campaign input feeds on a seller account.
Unchanged response
Request:
{
"$schema": "/schemas/signals/get-signals-request.json",
"discovery_mode": "wholesale",
"if_wholesale_feed_version": "v2026-05-18T08:00:00Z-acme-rev412"
}
Response (wholesale signals feed unchanged):
{
"$schema": "/schemas/signals/get-signals-response.json",
"status": "completed",
"message": "Wholesale signals 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"
}
When unchanged: true, signals[] MUST be omitted and consumers MUST NOT mutate their local wholesale signals mirror.
Wholesale signals feed changed — full payload returned (abbreviated)
{
"message": "Returning 312 signals (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",
"signals": [
{
"signal_id": { "source": "catalog", "data_provider_domain": "acme-data.com", "id": "luxury_auto_intenders" },
"signal_agent_segment_id": "sigagent_seg_4421",
"name": "Luxury Auto Intenders",
"description": "Households researching premium vehicles in the last 30 days.",
"signal_type": "marketplace",
"data_provider": "Acme Data",
"coverage_percentage": 18.4,
"deployments": [
{ "type": "platform", "platform": "the-trade-desk", "is_live": true,
"activation_key": { "type": "segment_id", "segment_id": "ttd_seg_99821" } }
],
"pricing_options": [
{ "pricing_option_id": "po_cpm_1", "model": "cpm", "cpm": 2.50, "currency": "USD" }
]
}
],
"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 scope-keyed via cache_scope. Callers cache (cache_scope, wholesale_feed_version) pairs alongside the (account, filters, discovery_mode, destinations, countries) tuple used. See Cache layering for the two-layer model.
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 segment 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 agent’s evaluation is two-stage: wholesale feed mismatch returns the full payload; wholesale feed match with pricing mismatch also returns the full payload (so the caller sees updated pricing_options); both match → unchanged: true.
filters canonicalization. Agents MUST treat the filters object as canonicalized before hashing into the wholesale_feed_version keyspace: keys sorted lexicographically, omitted-and-default values treated identically, array values sorted where the filter has set semantics (e.g., catalog_types, data_providers). Callers that pass equivalent-but-differently-shaped filter objects MUST receive the same wholesale_feed_version. Prevents silent stale-mirror bugs from key-order or default-elision differences. Forward-compat default: new filter fields added in 3.x minor versions MUST declare set-vs-sequence semantics; absent an explicit declaration, the rule defaults to set-semantics.
- Pagination interaction. Agents MUST return
wholesale_feed_version on every paginated page (not only the first) when they declare wholesale_feed_versioning.supported: true; agents that do not declare versioning SHOULD do the same. If the wholesale feed mutates between pages, the new version surfaces on the next page and the caller MUST restart pagination from cursor: null — the partial pages already received describe a stale version.
unchanged: true and in-progress pagination. A caller mid-pagination MAY send if_wholesale_feed_version matching the version their pages so far were drawn from. If the agent confirms unchanged: true, the response omits signals[] and pagination envelope entirely; the caller abandons their in-progress walk under that version. Agents 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 agents that ignore
if_wholesale_feed_version simply return the full payload — semantically correct, just inefficient.
For pushed change tracking beyond conditional fetch, see specs/wholesale-feed-webhooks.md. Wholesale feed webhooks carry the changed signal payload, pricing payload, removal tombstone, or bulk-change summary; get_signals remains the repair and reconciliation read.
Cache layering
Signals agents publish two notional layers: a public layer (the rate-card / structural view) and per-account overlays (account-specific pricing for premium buyers). The conditional-fetch path is layer-aware via cache_scope.
Two-layer cache.
| Layer | Cache key | What’s stored |
|---|
| Public | (agent, discovery_mode, filters, destinations, countries) | wholesale_feed_version_public, the wholesale signals feed payload as seen without an account ref |
| Account overlay | (agent, discovery_mode, filters, destinations, countries, account_id) | wholesale_feed_version_account, the wholesale signals feed payload when cache_scope: "account" was returned |
Behavior.
- Requests without
account always return cache_scope: "public". Callers cache under the public key.
- Requests with
account return cache_scope: "public" OR "account" (agent MUST declare; no default).
"public": this account prices off the rate card. Caller MAY dedupe — the version and payload are the same as the unauthenticated view.
"account": this response carries account-specific overrides. Caller caches under the account overlay key.
- Agents MAY downgrade an account from
"account" back to "public" — callers SHOULD interpret this as “this account no longer has overrides” and drop their overlay.
Conditional fetch with if_wholesale_feed_version. Send the token paired with whichever scope it was returned in. The agent compares against the current version for that scope. If the caller’s token belongs to an "account" scope but the agent responds with cache_scope: "public", that’s the downgrade signal.
Webhook invalidation. Wholesale feed webhook events declare applies_to.scope on *.priced and *.updated payloads. Agents MUST apply the same account/caller authorization predicate used by get_signals discovery_mode: "wholesale" when deciding which subscribers receive signal webhooks:
applies_to: { scope: "public" } → invalidate the public-layer cache; all account overlays referencing that public version are also stale.
applies_to: { scope: "account", account_ids: [...] } → invalidate only the named accounts’ overlays.
applies_to: { scope: "account" } without account_ids → seller withholds the affected set; the per-subscriber scope filter routes the event only to subscribers whose principal is in the affected set.
See specs/wholesale-feed-webhooks.md §“Cache layering and event scoping” for the full webhook-side spec.
Incomplete array
When the agent cannot complete all work within the caller’s time_budget (or due to internal limits), the response includes incomplete — an array declaring what is missing. Callers can use estimated_wait to decide whether to retry with a larger budget.
| Field | Type | Required | Description |
|---|
scope | string | Yes | "signals": not all matching signals were returned. "pricing": signals returned but pricing is absent or unconfirmed. "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 | How much additional time would resolve this scope. |
Iterative refinement
get_signals supports iterative refinement without a separate mode flag. The combination of signal_spec and signal_ids determines the operation:
| Fields provided | Behavior |
|---|
signal_spec only | Discovery — find signals matching the description |
signal_ids only | Exact lookup — return specific signals by ID |
signal_ids + signal_spec | Refinement — start from known signals, adjust per the spec |
discovery_mode: "wholesale" (neither signal_spec nor signal_ids) | Wholesale — enumerate the agent’s full priced signals feed. See Wholesale signals feed. |
To refine previous results, pass back the signal_id values from signals you want to keep, and provide an updated signal_spec describing what to change:
{
"$schema": "/schemas/signals/get-signals-request.json",
"signal_spec": "Same audience but with broader coverage, ideally above 20%",
"signal_ids": [
{
"source": "catalog",
"data_provider_domain": "experian.com",
"id": "luxury_auto_intenders"
}
],
"destinations": [
{
"type": "agent",
"agent_url": "https://wonderstruck.salesagents.com"
}
],
"countries": ["US"]
}
The signal agent uses the provided IDs as a starting point and the spec as adjustment guidance, returning signals that reflect both the original selection and the requested changes (e.g., broader segments from the same provider, or comparable segments from alternative providers with higher coverage).
Response - Multiple Signals Found
{
"message": "I found 3 signals matching your luxury goods criteria. The best option is 'Affluent Shoppers' with 22% coverage, already live across all requested platforms. 'High Income Households' offers broader reach (35%) but requires activation on OpenX. All signals are priced between $2-4 CPM.",
"context_id": "ctx-signals-abc123",
"signals": [
{
"signal_agent_segment_id": "acme_affluent_shoppers",
"name": "Affluent Shoppers",
"description": "Users with demonstrated luxury purchase behavior",
"signal_type": "marketplace",
"data_provider": "Acme Data",
"coverage_percentage": 22,
"deployments": [
{
"type": "platform",
"platform": "index-exchange",
"account": "agency-123-ix",
"is_live": true,
"activation_key": {
"type": "segment_id",
"segment_id": "ix_agency123_acme_aff_shop"
}
},
{
"type": "platform",
"platform": "openx",
"account": "agency-123-ox",
"is_live": true,
"activation_key": {
"type": "segment_id",
"segment_id": "ox_agency123_affluent_789"
}
}
],
"pricing_options": [
{
"pricing_option_id": "po_cpm_usd",
"model": "cpm",
"cpm": 3.50,
"currency": "USD"
}
]
}
// ... more signals
]
}
Response - Partial Success with Warnings
{
"$schema": "/schemas/signals/get-signals-response.json",
"status": "completed",
"message": "Found 2 luxury signals, but encountered some platform limitations. The 'Premium Auto Shoppers' signal has limited reach due to data restrictions, and pricing data is unavailable for one platform. Review the warnings below for optimization suggestions.",
"context_id": "ctx-signals-abc123",
"cache_scope": "public",
"wholesale_feed_version": "sig_v2026-05-18T08:00:00Z-public-rev12",
"signals": [
{
"signal_id": {
"source": "catalog",
"data_provider_domain": "experian.com",
"id": "premium_auto_shoppers"
},
"signal_agent_segment_id": "premium_auto_shoppers",
"name": "Premium Auto Shoppers",
"description": "High-value automotive purchase intenders",
"signal_type": "marketplace",
"data_provider": "Experian",
"coverage_percentage": 8,
"deployments": [
{
"type": "platform",
"platform": "the-trade-desk",
"is_live": true,
"activation_key": {
"type": "segment_id",
"segment_id": "ttd_exp_auto_premium"
}
}
],
"pricing_options": [
{
"pricing_option_id": "po_cpm_usd",
"model": "cpm",
"cpm": 4.50,
"currency": "USD"
}
]
}
],
"errors": [
{
"code": "PRICING_UNAVAILABLE",
"message": "Pricing data temporarily unavailable for The Trade Desk platform",
"field": "signals[0].pricing_options",
"suggestion": "Retry in 15-30 minutes when platform pricing feed updates",
"details": {
"affected_platform": "the-trade-desk",
"last_updated": "2025-01-15T12:00:00Z",
"retry_after": 1800
}
},
{
"code": "PRICING_UNAVAILABLE",
"message": "Pricing data temporarily unavailable for Amazon DSP",
"field": "filters.platforms",
"suggestion": "Pricing will be available during activation, or try again later",
"details": {
"affected_platform": "amazon-dsp",
"retry_after": 1800
}
}
]
}
Response - No Signals Found
{
"$schema": "/schemas/signals/get-signals-response.json",
"status": "completed",
"message": "I couldn't find any signals matching 'underwater basket weavers' in the requested platforms. This appears to be a very niche audience. Consider broadening your criteria to 'craft enthusiasts' or 'hobby communities' for better results. Alternatively, we could create a custom signal for this specific audience.",
"context_id": "ctx-signals-abc123",
"cache_scope": "public",
"wholesale_feed_version": "sig_v2026-05-18T08:00:00Z-public-rev12",
"signals": []
}
Implementation Guide
Generating Signal Messages
The message field should provide actionable insights:
def generate_signals_message(signals, request):
if not signals:
return generate_no_signals_message(request.signal_spec)
best_signal = find_best_signal(signals)
if len(signals) == 1:
signal = signals[0]
deployment_status = get_deployment_summary(signal, request.destinations)
pricing = signal.pricing_options[0] if signal.pricing_options else None
if pricing:
p = pricing.pricing
if p.model == "cpm":
price_commentary = f"Priced at ${p.cpm:.2f} CPM {p.currency}."
elif p.model == "percent_of_media":
cap = f", capped at ${p.max_cpm:.2f} CPM" if getattr(p, "max_cpm", None) else ""
price_commentary = f"Priced at {p.percent}% of media spend{cap}."
elif p.model == "flat_fee":
price_commentary = f"Flat fee of {p.amount} {p.currency} per {p.period}."
else:
price_commentary = ""
else:
price_commentary = ""
return f"I found a perfect match: '{signal.name}' from {signal.data_provider} with {signal.coverage_percentage}% coverage. {deployment_status} {price_commentary}"
else:
return f"I found {len(signals)} signals matching your {extract_key_criteria(request.signal_spec)} criteria. {describe_best_option(best_signal)} {get_pricing_range(signals)}."
def get_deployment_summary(signal, requested_deployments):
live = [d for d in signal.deployments if d.is_live]
pending = [d for d in signal.deployments if not d.is_live]
if not pending:
return "Already live on all requested deployments, ready to use immediately."
elif live:
activation_time = max((d.estimated_activation_duration_minutes or 0) for d in pending)
return f"Live on {len(live)} deployment(s). Activation on {len(pending)} more would take about {activation_time} minutes."
else:
return "Requires activation on all deployments, which typically takes 1-2 hours."