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

# Finding and buying a storefront's signals

> Every storefront on Apostra answers AdCP get_signals with the audiences and other signals its inventory carries. What it returns, how an account narrows it, and how to target a signal on a media buy.

A storefront on Apostra lists the signals its inventory can target, such as
audience segments from a data provider, through AdCP `get_signals`. You can
then target one of those signals on a package in `create_media_buy`. Most of
this page is for anyone who calls a storefront's AdCP endpoint directly, with
their own agent or SDK. If you buy through Apostra, see
[Searching a seller's signals in Apostra](https://docs.apostra.com/v3/storefront-signals.md#searching-a-sellers-signals-in-apostra).

## What a storefront returns

A storefront returns the signals its inventory sources report to Apostra. It
keeps them in a catalog that Apostra refreshes in the background, the same way
it keeps the catalog of products. Each signal carries:

| Field | What it is |
| - | - |
| `signal_ref` | The reference you send back to target the signal; see "The signal reference" below. |
| `signal_agent_segment_id` | The storefront's own id for the signal. |
| `name`, `description` | As the source reported them. |
| `signal_type` | `marketplace`, `custom` or `owned`, as the source reported it. A source that reports none is listed as `owned`. |
| `data_provider` | The provider the source named, such as `Polk Audiences by S&P Global Mobility`. It is the source's own label, not a checked identity. A `marketplace` signal without one is not listed. |
| `pricing_options` | The data price, as the source set it. A signal the source did not price has no `pricing_options`; the storefront never adds a price. |
| `coverage_percentage` | When the source reported it. A source's coverage forecast is not served. |
| `value_type`, `categories`, `range` | What values the signal takes, when the source reported them. |
| `deployments` | One entry: the storefront's own endpoint. A storefront signal is used by targeting it on the storefront's products. |

The `get_signals` answer names none of the storefront's inventory sources,
their accounts or their own ids for a signal. When the storefront's signal
catalog holds a signal, a product from `get_products` names it in its signal
options and included signals with the same `signal_ref` and
`signal_agent_segment_id` that `get_signals` returns. A product can still carry
an inventory source's own reference: when that source's signals could not be
refreshed or were for one account only, or for a signal `get_signals` does not
list. You can target such a reference on a buy as it is.

When some of a seller's signals could not be read from its inventory sources,
or a new inventory source's signals have not been read yet, the answer carries
`incomplete` with scope `signals`: the list may be missing some, so ask again
later. An inventory source that does not offer signals at all leaves the
answer complete.

## Two ways to ask

`get_signals` has two modes.

* **`discovery_mode: "wholesale"`** returns every signal you can use, sorted by
  name, in pages of up to 100 (50 by default). The answer carries
  `wholesale_feed_version` and `pricing_version`. Send them back as
  `if_wholesale_feed_version` and `if_pricing_version` and the storefront
  answers `unchanged: true` when nothing changed.
* **`discovery_mode: "brief"`** (the default) looks signals up. Send
  `signal_refs` to read particular signals, `signal_spec` to search by words,
  or both. A `signal_spec` match is a word match against each signal's name,
  description, provider and categories, ranked by how many of your words it
  contains. A `signal_spec` can be up to 500 characters, and a request can look
  up at most 100 `signal_refs` and `signal_ids` together; a larger request is
  refused with `INVALID_REQUEST`.

Both modes take `filters` (`catalog_types`, `data_providers`, `max_cpm`,
`max_percent`, `min_coverage_percentage`), `countries` and `destinations`.
A signal that names no countries is not limited by country. `destinations`
matches when it names the storefront's own endpoint. Each of `destinations`,
`countries`, `data_providers` and `catalog_types` takes at most 250 values; a
longer list is refused with `INVALID_REQUEST`.

Page through with `pagination.cursor`. A cursor belongs to the answer it came
from: if the storefront's signals change while you page, the next page is
refused with `INVALID_REQUEST` and you start again without a cursor.

`get_signals` needs AdCP 3.1 or later. A request pinned to 3.0 is refused with
`VERSION_UNSUPPORTED`, because AdCP 3.0 requires a price and coverage figure on
every signal, which a source need not report. On 3.1 and later,
`get_adcp_capabilities` lists `signals` with
`signals.discovery_modes: ["brief", "wholesale"]`.

## What an account changes

`account` is optional.

* **Without an account** you get every signal the storefront's live inventory
  sources report, as a public answer (`cache_scope: "public"`). A source whose
  own signal list was for one account only is left out.
* **With an account** you get only the signals from the inventory sources that
  account can buy through (`cache_scope: "account"`). The feed and pricing
  versions belong to that account's view. The account is checked
  the way product discovery checks it: an `account_id` must be one you hold,
  and a brand and operator are accepted only as another name for an account you
  already synced. See
  [Discovering products on a storefront needs an account](https://docs.apostra.com/v3/storefront-discovery-needs-an-account.md).

## The signal reference

Most signals carry a `signal_ref` that names the storefront as the source:

```json theme={null}
{
  "scope": "signal_source",
  "signal_source_url": "https://api.interchange.io/seller/acme-media/mcp",
  "signal_id": "sfsig_Kq3x9V0bQm5o1rY2n8LwTzA4eHcDfGiJ"
}
```

The `signal_id` stays the same for as long as the source keeps reporting the
signal and Apostra keeps the key it is derived with. If Apostra changes that key,
every storefront's `signal_id`s change at once: a buy that sends an old
reference is refused with `invalid_signal_targeting_groups`, and you call
`get_signals` again for the current references. The `signal_id` reveals
nothing about the source. A signal the source names by its
data provider's published reference (`scope: "data_provider"`, with
`data_provider_domain`) keeps that reference, so the same provider signal reads
the same on every seller that carries it. A storefront reference names the
storefront, not the provider, so you cannot check the seller's right to resell
a provider's signal through that provider's `adagents.json`.

## Targeting a signal on a media buy

Send the `signal_ref` in a package's
`targeting_overlay.signal_targeting_groups` on `create_media_buy` or
`update_media_buy`:

```json theme={null}
{
  "product_id": "cf_ctv_premium",
  "targeting_overlay": {
    "signal_targeting_groups": {
      "operator": "all",
      "groups": [
        {
          "operator": "any",
          "signals": [
            {
              "signal_ref": {
                "scope": "signal_source",
                "signal_source_url": "https://api.interchange.io/seller/acme-media/mcp",
                "signal_id": "sfsig_Kq3x9V0bQm5o1rY2n8LwTzA4eHcDfGiJ"
              },
              "value_type": "binary",
              "value": true
            }
          ]
        }
      ]
    }
  }
}
```

The storefront sends the signal to the inventory source that sells the
package's product, as that source's own reference. A signal must come from the
same inventory source as the product. If it does not, or the storefront does
not know the reference, the request is refused at once with `INVALID_REQUEST`
and the reason `invalid_signal_targeting_groups`, before any seller approval,
and nothing is sent to the source. Send the `signal_source_url` exactly as the
storefront returned it: the same reference under another address for the
storefront is refused. Call `get_signals` again for current references and
send the package again.

When the signal has `pricing_options`, AdCP has you send the
`pricing_option_id` of the price you choose in the signal's entry. On a product
a storefront puts together for your brief, priced signals are not yet buyable;
see below.

The `create_media_buy` answer echoes each package's
`signal_targeting_groups` as you sent them, with the storefront's references.
On `update_media_buy`, a package's `targeting_overlay` changes only the
dimensions you send, as AdCP defines: leave `signal_targeting_groups` out to
keep the package's signals, or send `null` to clear them.

## Signals on a storefront's own products

A product a storefront puts together for your brief lists its signals in AdCP
fields, with the same `signal_ref` that `get_signals` returns.

* **`included_signals`** are signals built into the product. They describe what
  the product is, its price already covers them, and you do not select them. A
  product with exact age targeting lists its age first, with its data provider
  in `ext.apostra.data_provider`. Each other included signal carries its
  provider in `data_provider`, as `get_signals` does.
* **`signal_targeting_allowed: true`** means you can add signals to a package
  of the product through `targeting_overlay.signal_targeting_groups`. A
  product says so when the inventory it is sold from runs on Apostra's embedded
  sales agent and the product is not guaranteed. A fixed-price (guaranteed)
  package takes no added signals for now: its delivery was forecast without
  them, and a narrow audience could keep it from delivering. A buy or update
  that adds signals to a package whose pricing option is fixed-price is
  refused at once with `invalid_signal_targeting_groups`. On a product with
  both a fixed and an auction price, choose the auction one to add signals.
* **`signal_targeting_options`** lists up to 20 signals from that inventory
  that you can add, widest reach first: highest `coverage_percentage`, then by
  name. The list does not depend on your brief. `get_signals` is the full
  menu: a package can target any signal it serves for that inventory, subject
  to the checks below. Each option carries its `signal_ref`, the
  `signal_agent_segment_id` `get_signals` serves, `name`, `description`,
  `data_provider` and value type. Today the options are the publisher's own
  data that costs nothing, so they carry no `pricing_options`. A third party's
  data is not offered while its data cost is unknown, and neither is an age
  signal.
* An option you can only use to exclude an audience says
  `allowed_targeting_modes: ["exclude"]`.
* Each option's `selection_group` names the ad-server targeting it uses
  (`audience_segment`, `custom_key_value` or `gam_custom_key`), and
  `signal_targeting_rules` lists those groups. An `any` group can't mix them.
  An option that combines several targeting criteria has a `selection_group`
  of its own: it can't be an alternative to any other signal.

If the storefront could not read all of an inventory source's signals when it
answered, or had not read a new source's signals yet, a product from that
inventory lists none of them, rather than a
partial list: its `included_signals` list at most its age, and it has no
`signal_targeting_options`. It still says `signal_targeting_allowed: true`,
and `get_signals` is the menu: its answer carries `incomplete` with scope
`signals` whenever the catalog read was partial.

### Priced signals are not yet buyable here

On a product a storefront puts together, a signal with a price is not yet
buyable: the seller's ad server does not yet bill a selected signal's price on
its line item. A buy that selects one in an `any` group is refused at once
with `INVALID_REQUEST` and the reason `invalid_signal_targeting_groups`, before
any seller approval. A priced signal in a `none` group only excludes an
audience and is not charged, so it is accepted, unless the signal can only
exclude. Choose a signal from the product's `signal_targeting_options` and
send the package again.

### Groups the seller's ad server can apply

Signals in an `any` group are alternatives, and the seller's ad server can make
alternatives only of audience segments, or of values of one custom-targeting
key. A buy is refused at once with `invalid_signal_targeting_groups`, before
any seller approval, when an `any` group:

* mixes audience segments with custom-targeting key signals
  (`custom_key_value` or `gam_custom_key`);
* holds values of more than one custom-targeting key;
* holds a signal that combines several targeting criteria together with other
  signals; or
* holds a signal that can only exclude.

Values of one key, such as two key=value signals on the same key, can be
alternatives in one `any` group. A group with a single signal takes any signal
except one that can only exclude.

A `none` group excludes each of its signals separately, so it takes any mix.
A `signal_ref` that names more than one of the seller's signals is refused the
same way: send the signal's `signal_agent_segment_id` with it.

## Searching a seller's signals in Apostra

A buyer account in Apostra reads one seller's signals with `search`, the same
read a buyer's own agent makes through the Apostra MCP server or HTTP API:

```json theme={null}
{
  "kind": "signal",
  "query": "auto intenders",
  "filter": {
    "advertiserId": "1234",
    "sellerAccountId": 5678,
    "dataProvider": ["Polk Audiences by S&P Global Mobility"]
  }
}
```

* **`filter.sellerAccountId`** names the seller by its Account id, the
  `sellerAccountId` on a seller or product result. One search reads one seller.
* **`filter.advertiserId`** names the advertiser whose account with that seller
  is used. Apostra asks the seller's storefront `get_signals` with that account,
  so you see only the signals from the inventory sources the account can buy
  through (see [What an account changes](https://docs.apostra.com/v3/storefront-signals.md#what-an-account-changes)). When your
  account has only one active advertiser, it is filled in for you.
* **`query`** searches by words, as `signal_spec` does in brief mode. Without a
  `query` you get the seller's whole list, sorted by name.
* **`filter.dataProvider`** keeps only signals from these data providers. It
  matches the provider's name exactly, ignoring case, so copy the name from an
  earlier result. To search by part of a provider's name, such as "Polk", put it
  in `query`.
* **`limit`** sets the page size, up to 100 (50 by default). For the next page,
  send the returned `nextCursor` as `cursor`, with the same query and filters.

Each result is the signal as the storefront returns it, without `deployments`.
The search is refused, never answered with an empty list, when:

* your advertiser has no account with the seller yet, or the account needs
  attention: open Media Partners to see where it stands;
* the seller is a platform such as Meta or Snap, which does not offer a signal
  catalog through Apostra. That does not mean it has no signals.

To target a signal, put its `signal_ref` unchanged in
`targetingOverlay.signal_targeting_groups` on that seller's product in
`save_media_buy`, with its `value_type` (a binary signal takes `value: true`).
A signal can be targeted only on products of the inventory source that reports
it; any other pairing is refused with `invalid_signal_targeting_groups`, as
described in [Targeting a signal on a media buy](https://docs.apostra.com/v3/storefront-signals.md#targeting-a-signal-on-a-media-buy).

## Related

* [Discovering products on a storefront needs an account](https://docs.apostra.com/v3/storefront-discovery-needs-an-account.md)
* [Discovery targeting](https://docs.apostra.com/v3/discovery-targeting.md)
* [Errors](https://docs.apostra.com/v3/errors.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.