> ## Documentation Index
> Fetch the complete documentation index at: https://agenticadvertisingorg-feature-feedback.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# get_signals

> get_signals is the AdCP task for discovering audience and contextual signals. Search by natural language or signal reference, filter by platform and CPM, and get real-time deployment status with activation keys.

**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`](https://adcontextprotocol.org/schemas/v3/signals/get-signals-request.json)
**Response Schema**: [`https://adcontextprotocol.org/schemas/v3/signals/get-signals-response.json`](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`, `signal_refs`, or deprecated `signal_ids` is required, agent performs inference/RAG. `"wholesale"`: raw wholesale signals feed enumeration — `signal_spec`, `signal_refs`, 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[]`](#incomplete-array). Agents receiving requests from pre-v3.1 clients without `discovery_mode` MUST default to `"brief"`. Probe support via [`get_adcp_capabilities`](/docs/protocol/get_adcp_capabilities) (`signals.discovery_modes`).                                                                                                                                                                                                                                                                                                                                      |
| `signal_spec`               | string                                                                          | Conditional               | Natural language description of the desired signals. Required when `discovery_mode` is `"brief"` and both `signal_refs` and legacy `signal_ids` are absent. MUST NOT be provided when `discovery_mode` is `"wholesale"`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `signal_refs`               | SignalRef\[]                                                                    | Conditional               | Specific signals to look up by reference. Required when `discovery_mode` is `"brief"` and `signal_spec` is absent. MUST NOT be provided when `discovery_mode` is `"wholesale"`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `signal_ids`                | SignalID\[]                                                                     | Conditional               | **Deprecated.** Use `signal_refs` instead. Legacy specific-signal lookup for older clients. MUST NOT be provided when `discovery_mode` is `"wholesale"`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `account`                   | [AccountRef](/docs/building/integration/accounts-and-agents#account-references) | 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).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `fields`                    | string\[]                                                                       | No                        | Specific signal fields to include in the response, aligned with `get_products.fields`. Required identity and activation fields are always included when required by the response schema. Use for progressive disclosure of rich definition metadata such as `taxonomy`, `data_sources`, `methodology`, `segmentation_criteria`, `criteria_url`, `onboarder`, `modeling`, `countries`, `consent_basis`, `restricted_attributes`, `policy_categories`, `art9_basis`, and `data_subject_rights`. Agents SHOULD honor requested fields for exact lookup, refinement, small custom-signal result sets, and private/source-native signals when available. `fields` is a projection request, not an entitlement grant; agents MAY redact requested definition fields unless the caller is authorized for the underlying lineage, methodology, and rights-routing metadata. When `consent_basis` or `art9_basis` is projected for another provider's signal, the value remains provider-declared signal-definition posture; sellers and federating agents MUST NOT substitute their own processing basis. Broad discovery and wholesale pages MAY still return compact pointers to provider-published definitions or disclosure URLs. |
| `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](#wholesale-feed-versioning) and [Cache layering](#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).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `push_notification_config`  | PushNotificationConfig                                                          | No                        | Optional webhook channel for async terminal completion/failure notifications on `brief` semantic discovery. `submitted` responses with a `task_id` remain pollable through `get_task_status` (legacy `tasks/get`) whether or not this field is present. If the request includes this field and the agent returns `submitted`, the agent MUST deliver at least the terminal completion/failure notification to the configured webhook; intermediate progress notifications are MAY. If the agent cannot honor the webhook channel, it MUST reject the request with a structured error instead of silently accepting. Ignored for `wholesale`; agents MUST NOT route wholesale reads through the Submitted arm because this field is present.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |

<Note>
  `discovery_mode: "wholesale"` is an AdCP 3.1+ discovery shape. When the
  caller cannot assume a 3.1+ signals surface or wholesale support, clients
  should first call [`get_adcp_capabilities`](/docs/protocol/get_adcp_capabilities)
  and confirm that `signals.discovery_modes` includes `"wholesale"` before
  issuing a wholesale request. If the field is absent or lists only `"brief"`,
  treat the agent as brief-only and use `signal_spec`, `signal_refs`, or
  deprecated `signal_ids`.
</Note>

### Async discovery

`discovery_mode: "brief"` MAY return `submitted` when semantic discovery depends on slow provider queries or review that cannot finish before the initial response. Polling `get_task_status` (legacy `tasks/get`) with `task_id` is always valid. If `push_notification_config` is present and the agent returns `submitted`, the agent also sends at least the terminal completion/failure notification to that webhook; intermediate progress notifications are optional. `discovery_mode: "wholesale"` remains a synchronous feed read and reports partial completion via `incomplete[]`, not `submitted`.

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

<Note>
  `catalog_types`, the deprecated `catalog_signals` capability flag, and the deprecated `signal_id.source: "catalog"` value are legacy wire terms. In new prose, read them as provider-published signal definitions in adagents.json `signals[]`. Existing 3.x agents may continue to accept or emit them for compatibility, but new callers SHOULD use `signal_ref` and MUST NOT require `signals.features.catalog_signals` before using the Signals protocol.
</Note>

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

```json theme={null}
{
  "signals": [
    {
      "signal_ref": {
        "scope": "data_provider",
        "data_provider_domain": "string",
        "signal_id": "string"
      },
      "signal_agent_segment_id": "string",
      "name": "string",
      "description": "string",
      "signal_type": "string",
      "data_provider": "string",
      "coverage_percentage": "number (optional, deprecated)",
      "coverage_forecast": {
        "method": "estimate",
        "forecast_range_unit": "availability",
        "scope": {
          "kind": "inventory",
          "label": "network price-priority inventory"
        },
        "bucket_semantics": "exclusive",
        "bucket_completeness": "partial",
        "points": [
          {
            "label": "not present",
            "dimensions": [
              {
                "kind": "signal",
                "signal_ref": {
                  "scope": "data_provider",
                  "data_provider_domain": "weather-data.example",
                  "signal_id": "weather"
                },
                "signal_value": null,
                "presence": "absent"
              }
            ],
            "metrics": {
              "impressions": { "mid": 280000 },
              "coverage_rate": { "mid": 0.28 }
            }
          },
          {
            "label": "hot",
            "dimensions": [
              {
                "kind": "signal",
                "signal_ref": {
                  "scope": "data_provider",
                  "data_provider_domain": "weather-data.example",
                  "signal_id": "weather"
                },
                "signal_value": "hot",
                "presence": "present"
              }
            ],
            "metrics": {
              "impressions": { "mid": 180000 },
              "coverage_rate": { "mid": 0.18 }
            }
          }
        ]
      },
      "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",
          "...": "..."
        }
      ]
    }
  ]
}
```

<Note>
  `get_signals` is a discovery and availability surface, not a requirement to
  inline every field from the authoritative signal definition. For broad search
  results and wholesale feed pages, signal agents SHOULD keep each listing
  compact and use stable references plus cacheable disclosure pointers for large
  definition resources. Buyers can dereference provider-published definitions
  from `signal_ref`: fetch the provider's `/.well-known/adagents.json` (following
  `authoritative_location` when present) and select the matching `signals[].id`.
  Cache the fetched `adagents.json` document by resolved authoritative URL plus
  `catalog_etag`, HTTP `ETag` / `Last-Modified`, or a bounded TTL, then resolve
  `signal_ref.signal_id` within that cached document. Use `taxonomy.etag` only for
  taxonomy documents that have their own freshness validator. These reuse existing
  provider-file and taxonomy validators; `get_signals` does not define a separate
  signal-definition validator, and clients must not key signal-definition caches by
  validator alone.
</Note>

### Definition field inclusion

Buyers that need richer review context in the same call can set `fields`. This
uses the same response-projection pattern as `get_products.fields`, rather than
introducing a separate lookup task.

Required identity and activation fields are always included when required by the
response schema. Additional values request optional listing fields or rich
definition metadata inline, including `taxonomy`, `data_sources`, `methodology`,
`segmentation_criteria`, `criteria_url`, `refresh_cadence`, `lookback_window`,
`onboarder`, `modeling`, `audience_expansion`, `device_expansion`, `countries`,
`consent_basis`, `restricted_attributes`, `policy_categories`, `art9_basis`, and
`data_subject_rights`.

When `consent_basis` or `art9_basis` is projected for another provider's signal,
the value remains provider-declared signal-definition posture. Sellers and
federating agents MUST NOT substitute their own processing basis for the
provider-declared basis.

Agents SHOULD honor requested fields for exact lookup, refinement, and small
custom-signal result sets when the fields are available. For broad discovery and
wholesale pages, agents MAY still return compact listings with pointers to
provider-published definitions, taxonomy documents, criteria pages, and
disclosure URLs when inlining the requested fields would make the page too
large. This keeps static signals cacheable through `adagents.json` while letting
custom or brief-specific signals return deeper inline context when useful.

### Field Descriptions

* **signals**: Array of matching signals
  * **signal\_ref**: Canonical signal reference. Use `scope: "data_provider"` for signals resolved through adagents.json `signals[]`, `scope: "signal_source"` for source-native signals that are not published in adagents.json `signals[]`, and `scope: "product"` only in product-contextual responses. New responses SHOULD include this field.
  * **signal\_id**: Deprecated legacy SignalId object. New clients should read `signal_ref`; during the migration window, older responses may include only `signal_id`.
  * **signal\_agent\_segment\_id**: Opaque signal handle issued by this signal source. Use it verbatim for `activate_signal`; do not treat it as a globally portable signal ID. For package-level `signal_targeting_groups`, `signal_ref` is the buy-time identity and this handle is echoed only when the selected product option exposes it as a separate execution handle.
  * **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 source directly owns
    * `custom` — source-native segment built on demand from models, composites, or buyer inputs (not attributable to a standing upstream provider)
  * **data\_provider**: Human-readable source name when applicable. For `scope: "data_provider"` signals this is the data provider; for `scope: "signal_source"` signals it may identify the signal source or proprietary origin.
  * **coverage\_percentage**: Optional deprecated legacy scalar percentage of audience coverage. Use only as a fallback for clients that do not consume `coverage_forecast`. When **coverage\_forecast** is present, `coverage_forecast` is authoritative for signal-level discovery and this scalar is fallback-only. If `coverage_forecast` includes an absent bucket over the same denominator, `coverage_percentage` should align with `100 * (1 - absent coverage_rate.mid)`.
  * **coverage\_forecast**: Optional forecast-shaped availability guidance for the signal. `scope` declares the denominator, `bucket_semantics` declares whether returned value buckets are `exclusive` or `overlapping`, and `bucket_completeness` declares whether the returned buckets are a full denominator partition or a partial histogram. Each point can use a `kind: "signal"` dimension with canonical `signal_ref`, `presence: "present"` for any present value, `presence: "present"` plus `signal_value` for a specific value bucket, or `presence: "absent"` plus `signal_value: null` for the not-present bucket. `metrics.coverage_rate` is a 0.0-1.0 fraction of the declared scope.
  * **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 when it has an incremental price. Pass the selected `pricing_option_id` in `report_usage` or package-level `signal_targeting_groups` for billing verification. Omitted when pricing is unavailable to the caller, bundled into the destination product, or has no incremental cost.
    * **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. For direct signal activation, pass its `pricing_option_id` in `report_usage` for billing verification; for seller-offered signals selected on a media product, pass it in package-level `targeting_overlay.signal_targeting_groups.groups[].signals[].pricing_option_id`. If a signal offers multiple models (e.g., CPM and flat fee), choose based on your expected delivery volume and campaign structure.

### Response Metadata

| 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](#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](#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](#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:**

```json theme={null}
{
  "type": "segment_id",
  "segment_id": "ttd_segment_12345"
}
```

**Key-Value format:**

```json theme={null}
{
  "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:

```json theme={null}
{
  "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:

```json theme={null}
{
  "$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_ref": {
        "scope": "data_provider",
        "data_provider_domain": "pinnacle-data.example",
        "signal_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": "Pinnacle Data",
      "coverage_forecast": {
        "method": "estimate",
        "forecast_range_unit": "availability",
        "scope": {
          "kind": "inventory",
          "label": "eligible destination inventory"
        },
        "bucket_semantics": "exclusive",
        "bucket_completeness": "partial",
        "points": [
          {
            "label": "present",
            "dimensions": [
              {
                "kind": "signal",
                "signal_ref": {
                  "scope": "data_provider",
                  "data_provider_domain": "pinnacle-data.example",
                  "signal_id": "luxury_auto_intenders"
                },
                "presence": "present"
              }
            ],
            "metrics": {
              "coverage_rate": { "mid": 0.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` for direct signal usage or in package-level `signal_targeting_groups` when the signal is selected on a media buy:

```json theme={null}
{
  "$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_ref": {
        "scope": "data_provider",
        "data_provider_domain": "acmedata.com",
        "signal_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"
        }
      ]
    }
  ]
}
```

### MCP Request - Buyer Querying Multiple DSP Platforms

A buyer checking availability across multiple DSP platforms:

```json theme={null}
{
  "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
    }
  }
}
```

### MCP Response - Buyer With Multi-Platform Access

A buyer with credentials for both The Trade Desk and Amazon DSP receives keys for both platforms:

```json theme={null}
{
  "$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_ref": {
        "scope": "data_provider",
        "data_provider_domain": "experian.com",
        "signal_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

```javascript theme={null}
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

```javascript theme={null}
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:

```json theme={null}
{
  "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_ref": {
                  "scope": "data_provider",
                  "data_provider_domain": "experian.com",
                  "signal_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://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

### All Platforms Discovery

Discover all available deployments across platforms:

```json theme={null}
{
  "$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**:

```json theme={null}
{
  "$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_ref": {
      "scope": "data_provider",
      "data_provider_domain": "peer39.com",
      "signal_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): Opaque signal handle issued by this signal source. Use it verbatim for `activate_signal`; do not treat it as a globally portable signal ID. For media-buy signal groups, `signal_ref` is the buy-time identity and this handle is echoed only when the selected product option exposes it as a separate execution handle.
  * **name** (string): Human-readable signal name
  * **description** (string): Detailed signal description
  * **signal\_type** (string): `marketplace` (resold third-party), `owned` (signal source first-party data), or `custom` (source-native segment built on demand)
  * **data\_provider** (string, optional): Human-readable source/provider name when applicable
  * **coverage\_percentage** (number, optional, deprecated): Legacy scalar estimated reach percentage. Use only as a fallback when `coverage_forecast` is absent or unsupported by the client.
  * **coverage\_forecast** (object, optional): Forecast-shaped coverage breakdown with an explicit denominator and availability points. Use `presence: "present"` with omitted `signal_value` for an aggregate "any present value" bucket; add `signal_value` only when the row is for a specific value. Use `bucket_semantics: "exclusive"` when returned buckets do not overlap, or `"overlapping"` when multi-value signals can make returned rates sum above 1.0. Use `bucket_completeness: "complete"` only when the returned buckets cover the declared denominator; otherwise use `"partial"` and buyers must treat omitted share as undisclosed, other, or unsupported buckets.

Use `coverage_forecast` as the authoritative field for signal-first discovery: "how much inventory has this signal or its values?" Use product or proposal forecasts with `kind: "signal"` dimensions as the authoritative surface for product-specific planning: "how does this signal restrict this product's baseline availability?" Bucket-specific filtering is client-side for now; request filters such as `min_coverage_percentage` still use the legacy scalar.

* **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 when it has an incremental price. Select one and pass its `pricing_option_id` in `report_usage` or package-level `signal_targeting_groups`.
  * **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

1. **Authentication-Based Keys**: Activation keys are only returned when the authenticated caller matches one of the deployment targets
2. **Permission Security**: The signal agent determines key inclusion based on caller identity, not request flags
3. **Deployment Status**: Check `is_live` to determine if activation is needed
4. **Multiple Deployments**: Query multiple deployment targets to check availability across platforms
5. **Activation Required**: If `is_live` is false, use the `activate_signal` task
6. **The message field** provides a quick summary of the most relevant findings

## Seller-offered signals on media products

A sales agent that owns or is authorized to apply targeting signals exposes buy-time product eligibility through `get_products`. It can also expose a broader cross-product signal feed through `get_signals` when buyers need discovery or activation before package selection:

* `get_products` declares whether a product has package-level signal targeting. `products[].included_signals` describes non-selectable signals already bundled into or planned into a product. Inline `products[].signal_targeting_options` can carry a product-specific menu, price, activation handle, default, grouping hint, or brief/refine-selected subset; wholesale products can omit inline options and use `get_signals` as the selectable signal feed.
* `get_signals` optionally returns cross-product signal metadata, including `signal_ref`, `signal_agent_segment_id`, value metadata, and any default or account-scoped `pricing_options`.
* `create_media_buy` selects the signal for a package in `packages[].targeting_overlay.signal_targeting_groups`, carrying the selected signal `pricing_option_id`, `signal_ref`, and any separate seller execution handle when required.

This gives buyers a product-first buy-time eligibility path without forcing products to duplicate a large wholesale signal feed: use the selected product's inline `signal_targeting_options` when present and `signal_targeting_rules` to determine what may be applied on that package. When a product omits inline options but allows signal targeting, use `get_signals` for candidate discovery and activation metadata, then use `get_products.filters.signal_targeting` or a product-specific `get_products` query to confirm the candidate set is selectable and jointly composable for the intended product before calling `create_media_buy`. When both surfaces include pricing, the product-scoped `signal_targeting_options[].pricing_options` price is authoritative for that product. For product-local signals exposed on both surfaces, `signal_ref.signal_id` in the product option MUST match the seller's `get_signals.signals[].signal_ref.signal_id` for the same signal. `included_signals` is descriptive only and does not make a signal selectable.

For media-buy product targeting, product options and package groups use the same `signal_ref` identity: `scope: "product"` for product-local signal options, `scope: "data_provider"` with `data_provider_domain` for signals defined in published adagents.json `signals[]`, or `scope: "signal_source"` for source-native custom signals that are not published in adagents.json `signals[]`. When a provider-published signal is selected, buyers can verify the seller's authorization by checking the provider's `adagents.json` `authorized_agents` rules for the signal id or tags. The older Signals Protocol `signal_id.source` shape is deprecated and retained only for backwards compatibility.

## 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_refs` / deprecated `signal_ids`. When the caller cannot assume a 3.1+ signals surface or wholesale support, first check `get_adcp_capabilities.signals.discovery_modes` for `"wholesale"`. Wholesale enumeration is symmetric with [`get_products` `buying_mode: "wholesale"`](/docs/media-buy/task-reference/get_products): synchronous, paginated, firm-priced, with partial completion declared via `incomplete[]`.

### Request

```json theme={null}
{
  "$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

```json theme={null}
{
  "$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_ref": { "scope": "data_provider", "data_provider_domain": "acme-data.com", "signal_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 standalone signal 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` or use product-scoped pricing from `get_products` before composing). Unauthenticated callers MAY receive signal metadata without pricing. Signals bundled into a media product or carrying no incremental cost MAY omit `pricing_options[]`.

### Capability declaration

Signals agents declare wholesale support in [`get_adcp_capabilities`](/docs/protocol/get_adcp_capabilities):

```json theme={null}
{
  "signals": {
    "discovery_modes": ["brief", "wholesale"]
  }
}
```

Agents not declaring `"wholesale"` MAY return `INVALID_REQUEST` for wholesale calls. Because wholesale discovery is AdCP 3.1+, the capability declaration is the canonical support signal when a caller cannot assume a 3.1+ signals surface or wholesale support; 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:**

```json theme={null}
{
  "$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):**

```json theme={null}
{
  "$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)

```json test=false theme={null}
{
  "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_ref": { "scope": "data_provider", "data_provider_domain": "acme-data.com", "signal_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](#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_refs` determines the operation:

| Fields provided                                                         | Behavior                                                                                                           |
| ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `signal_spec` only                                                      | Discovery — find signals matching the description                                                                  |
| `signal_refs` only                                                      | Exact lookup — return specific signals by reference                                                                |
| `signal_refs` + `signal_spec`                                           | Refinement — start from known signals, adjust per the spec                                                         |
| `discovery_mode: "wholesale"` (neither `signal_spec` nor `signal_refs`) | Wholesale — enumerate the agent's full priced signals feed. See [Wholesale signals feed](#wholesale-signals-feed). |

To refine previous results, pass back the `signal_ref` values from signals you want to keep, and provide an updated `signal_spec` describing what to change:

```json theme={null}
{
  "$schema": "/schemas/signals/get-signals-request.json",
  "signal_spec": "Same audience but with broader coverage, ideally above 20%",
  "signal_refs": [
    {
      "scope": "data_provider",
      "data_provider_domain": "experian.com",
      "signal_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

```json theme={null}
{
  "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_ref": {
        "scope": "data_provider",
        "data_provider_domain": "acme-data.com",
        "signal_id": "affluent_shoppers"
      },
      "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

```json theme={null}
{
  "$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_ref": {
        "scope": "data_provider",
        "data_provider_domain": "experian.com",
        "signal_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

```json theme={null}
{
  "$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:

```python theme={null}
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 = ""
        coverage_commentary = f" with {signal.coverage_percentage}% coverage" if getattr(signal, "coverage_percentage", None) is not None else ""
        return f"I found a perfect match: '{signal.name}' from {signal.data_provider}{coverage_commentary}. {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."
```
