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

# Ad-server signals

> Build signals from targeting in a connected ad server

`/api/v2/storefront/esa/{esaId}/signals`

The ad-server signal endpoints let a storefront operator build and
manage the signals a seller builds from targeting in an ad server source. The `/esa/`
path segment and `esaId` field are stable API identifiers; the operator-facing
object is an **ad server source**.

Keep these two layers distinct:

| Term                    | Meaning                                                                                                                                    |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| **Ad-server targeting** | Source-native audience segments, custom targeting keys, and custom targeting values already present in the connected ad server             |
| **Signal**              | A named targeting definition the seller creates from one or more source-native targeting primitives; carried as a `signal` on the API wire |

Browsing ad-server targeting does not create or import a signal. It
shows the raw material available for authoring.

Start every authoring workflow by reading source capabilities. Capabilities
describe which mapping kinds, API candidate types, value types, composition
models, and parent-filtered types the source supports. Do not assume every ad
server source supports GAM fields. When a selected targeting candidate includes
`defaultSignal`, use that complete draft as the starting point instead of
reconstructing its adapter mapping.

## Endpoints

| Method | Endpoint                                                      | Description                                                                     |
| ------ | ------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| `GET`  | `/api/v2/storefront/esa/{esaId}/signals`                      | List signals created on the source                                              |
| `GET`  | `/api/v2/storefront/esa/{esaId}/signals/{signalId}`           | Read one signal                                                                 |
| `POST` | `/api/v2/storefront/esa/{esaId}/signals`                      | Create a signal. Add `?dry_run=true` to validate and preview without persisting |
| `GET`  | `/api/v2/storefront/esa/{esaId}/signals/adapter-capabilities` | Describe source support for signal authoring                                    |
| `GET`  | `/api/v2/storefront/esa/{esaId}/signals/candidates`           | Browse source-native ad-server targeting; `candidates` is the API response name |

## Bulk signal creation with Murph

When you need to create many signals, ask Murph to create them as a
batch instead of one by one. Murph still validates and previews the proposed
signals before any durable write, and the confirmation gate still fires
before creation. The batch is approved once, then Murph creates the set
together.

* **Paste a list or upload rows.** Murph maps each row to the source's supported signal shape after reading source capabilities.
* **One approval for the batch.** You review the full set and approve once rather than approving every signal individually.
* **Continues on error.** If one signal fails, the rest continue. Murph reports a per-signal result so you can retry only the failures.

Use this for requests such as "create signals from these 49 GAM
values," "set up my first audience signals," or "build signals from
these key-values."

## Source capabilities

```bash theme={null}
curl https://api.apostra.com/api/v2/storefront/esa/123/signals/adapter-capabilities \
  -H "Authorization: Bearer $SCOPE3_API_KEY"
```

```json theme={null}
{
  "data": {
    "adapter": "google_ad_manager",
    "supportsSignalMappingAuthoring": true,
    "mappingKinds": [
      {
        "mappingKind": "audience_segment",
        "label": "Audience segment",
        "candidateType": "audience_segment",
        "supportsSearch": true
      },
      {
        "mappingKind": "gam_targeting_groups",
        "label": "GAM targeting groups",
        "candidateType": null,
        "supportsSearch": false
      }
    ],
    "supportedCandidateTypes": [
      "audience_segment",
      "custom_targeting_key",
      "custom_targeting_value"
    ],
    "candidateTypes": [
      {
        "candidateType": "custom_targeting_key",
        "label": "Custom targeting key",
        "browseOnly": true,
        "childCandidateTypes": ["custom_targeting_value"],
        "supportsSearch": true
      },
      {
        "candidateType": "custom_targeting_value",
        "label": "Custom targeting value",
        "mappingKind": "custom_key_value",
        "directlyMappable": true,
        "parentCandidateType": "custom_targeting_key",
        "supportsParentFilter": true,
        "supportsSearch": true
      }
    ],
    "targetingSemantics": {
      "supportsComposed": true,
      "compositionModels": ["groups"],
      "supportedModes": ["include", "exclude"],
      "mappingKinds": [
        {
          "mappingKind": "gam_targeting_groups",
          "supportsComposed": true,
          "compositionModels": ["groups"],
          "exclusiveWithOtherSignals": true,
          "notes": "Grouped GAM targeting must be used alone for buyer targeting that consumes this field."
        }
      ]
    },
    "valueTypes": ["binary", "categorical", "numeric"]
  }
}
```

### Capability fields

| Field                            | Notes                                                                                                                                              |
| -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `adapter`                        | Adapter implementation behind the ad server source, for example `google_ad_manager`                                                                |
| `supportsSignalMappingAuthoring` | `false` means the source can list signals but cannot create drafts from this API                                                                   |
| `mappingKinds`                   | Signal mapping kinds the adapter can create, such as `audience_segment`, `custom_key_value`, or `gam_targeting_groups`                             |
| `candidateTypes`                 | API types for browseable ad-server targeting. Types listed under `childCandidateTypes` can require `parent_id` when `supportsParentFilter` is true |
| `targetingSemantics`             | Composition behavior, include/exclude modes, buyer targeting fields, and warnings such as exclusivity                                              |
| `valueTypes`                     | Allowed `valueType` values for signal drafts                                                                                                       |

## Browse ad-server targeting

Ad-server targeting records are source-native values that can be used to build
a signal draft. The endpoint returns them in a `candidates` array;
that is the wire name, not a separate operator-facing object. For hierarchical
API types, first browse the parent type and then pass its `externalId` as
`parent_id` when browsing the nested type.

```bash theme={null}
curl "https://api.apostra.com/api/v2/storefront/esa/123/signals/candidates?candidate_type=custom_targeting_key&q=DAYOFWEEK&limit=10" \
  -H "Authorization: Bearer $SCOPE3_API_KEY"
```

```bash theme={null}
curl "https://api.apostra.com/api/v2/storefront/esa/123/signals/candidates?candidate_type=custom_targeting_value&parent_id=17467172&q=FRIDAY&limit=10" \
  -H "Authorization: Bearer $SCOPE3_API_KEY"
```

```json theme={null}
{
  "data": {
    "candidates": [
      {
        "candidateType": "custom_targeting_value",
        "externalId": "451422266753",
        "name": "FRIDAY",
        "parentId": "17467172",
        "path": ["DAY OF WEEK", "FRIDAY"],
        "mappingKind": "custom_key_value",
        "defaultSignal": {
          "signalId": "dayofweek_friday",
          "name": "DAY OF WEEK: FRIDAY",
          "valueType": "binary",
          "adapterConfig": {
            "type": "passthrough",
            "kind": "custom_key_value",
            "keyId": "17467172",
            "valueId": "451422266753"
          }
        }
      }
    ],
    "count": 1,
    "nextCursor": null
  }
}
```

`defaultSignal` is the adapter-authored complete draft for that targeting
candidate. Use it directly for validation, changing only `signalId` or `name`
when the seller needs different buyer-facing wording. Validate again after any
change.

### Ad-server targeting query parameters

| Parameter        | Required | Notes                                                                                        |
| ---------------- | -------- | -------------------------------------------------------------------------------------------- |
| `candidate_type` | Yes      | API type from `candidateTypes[].candidateType`                                               |
| `q` or `query`   | No       | Adapter search string                                                                        |
| `parent_id`      | No       | Parent candidate `externalId`, required by candidate types with `supportsParentFilter: true` |
| `limit`          | No       | Page size                                                                                    |
| `cursor`         | No       | Cursor returned as `nextCursor`                                                              |

## Signal draft shape

Creation and validation use the same request body:

| Field             | Type           | Required | Notes                                                                                     |
| ----------------- | -------------- | -------- | ----------------------------------------------------------------------------------------- |
| `signalId`        | string         | Yes      | Stable caller-supplied signal identifier                                                  |
| `name`            | string         | Yes      | Human-readable signal name                                                                |
| `description`     | string or null | No       | Operator-facing description                                                               |
| `valueType`       | string         | Yes      | `binary`, `categorical`, or `numeric`; must appear in the adapter capability `valueTypes` |
| `tags`            | string\[]      | No       | Optional labels                                                                           |
| `adapterConfig`   | object         | Yes      | Adapter mapping configuration                                                             |
| `pricing_options` | object\[]      | No       | Incremental vendor pricing. Omit it for a free signal.                                    |

### Signal pricing

A signal can carry one or more AdCP vendor pricing options. Each option needs a
stable `pricing_option_id` and one of these models:

* `cpm` — numeric `cpm` and string `currency`
* `percent_of_media` — numeric `percent`, optional numeric `max_cpm`, and string `currency`
* `flat_fee` — numeric `amount`, string `currency`, and `period` set to `monthly`, `quarterly`, `annual`, or `campaign`
* `per_unit` — string `unit`, numeric `unit_price`, and string `currency`
* `custom` — string `description`, required non-empty object `metadata`, and optional string `currency`. The metadata must contain at least one field and explain the pricing basis well enough for operator review and downstream charge reconstruction; include a concise `summary_for_operator` when possible.

The signal editor currently offers CPM and percent-of-media. API clients can
use all five models. Pricing is persisted by the managed ad-server source and
returned with the signal; omitting `pricing_options` means the signal has no
incremental fee.

Writes use snake-case `pricing_options`. Normalized source responses use
camel-case `pricingOptions`, including camel-case option fields such as
`pricingOptionId`, `maxCpm`, and `unitPrice`:

```bash theme={null}
curl -X POST https://api.apostra.com/api/v2/storefront/esa/123/signals \
  -H "Authorization: Bearer $SCOPE3_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "signalId": "auto_intenders",
    "name": "Auto intenders",
    "valueType": "binary",
    "adapterConfig": {
      "type": "passthrough",
      "kind": "audience_segment",
      "segmentId": "segment-123"
    },
    "pricing_options": [{
      "pricing_option_id": "auto_intenders_cpm",
      "model": "cpm",
      "cpm": 2.5,
      "currency": "USD"
    }]
  }'
```

```json theme={null}
{
  "data": {
    "signalId": "auto_intenders",
    "name": "Auto intenders",
    "valueType": "binary",
    "pricingOptions": [
      {
        "pricingOptionId": "auto_intenders_cpm",
        "model": "cpm",
        "cpm": 2.5,
        "currency": "USD"
      }
    ]
  }
}
```

## How signals relate to products

Products describe the sellable inventory package. Signals describe
targetable attributes that can be exposed to buyers or used in targeting
workflows. For GAM key-values, do not treat "not in the product selector
fields" as "not supported." Key-value signal targeting is supported when the
source capabilities advertise the relevant signal mapping kind.

Use `custom_key_value` for a direct key/value mapping and
`gam_targeting_groups` for composite GAM key-value logic. Then validate the
signal and use it according to the adapter's `targetingSemantics`. If
`targetingSemantics.mappingKinds[].exclusiveWithOtherSignals` is true for
`gam_targeting_groups`, use that grouped signal by itself for the buyer
targeting field it consumes.

Some large networks use GAM key-values to identify site lists or other
seller-defined groups inside their network. Those keys and values are
browseable as ad-server targeting and can be used to create signals.
Publisher coverage still comes from
`adagents.json`. Buyer property-list references are forwarded and resolved
today; the seller's agent decides whether and how to map the requested subset to
products or ad-server controls. See
[Custom targeting and properties](/v2/storefront/esa/custom-targeting-properties).

### Audience segment

```json theme={null}
{
  "signalId": "auto_intenders",
  "name": "Auto intenders",
  "valueType": "binary",
  "adapterConfig": {
    "type": "passthrough",
    "kind": "audience_segment",
    "segmentId": "987654321"
  }
}
```

### Direct GAM key-value signal

Use a direct `custom_key_value` signal when the adapter exposes a specific
custom targeting value as directly mappable. Browse values with `parent_id` so
the value remains tied to the correct key.

```json theme={null}
{
  "signalId": "dayofweek_friday",
  "name": "DAY OF WEEK: FRIDAY",
  "valueType": "binary",
  "adapterConfig": {
    "type": "passthrough",
    "kind": "custom_key_value",
    "keyId": "17467172",
    "valueId": "451422266753"
  }
}
```

### Grouped GAM targeting

Use `gam_targeting_groups` for composite GAM logic. Groups are OR'd together;
criteria inside a group are AND'd together; multiple values inside one criterion
are OR'd by GAM for that key. A criterion with `exclude: true` excludes matching
values.

Grouped GAM targeting can be exclusive depending on the adapter semantics. If
`targetingSemantics.mappingKinds[].exclusiveWithOtherSignals` is true, do not
combine the grouped signal with another signal targeting the same buyer field.

```json theme={null}
{
  "signalId": "weekday_or_sports",
  "name": "Weekday or sports package",
  "valueType": "binary",
  "adapterConfig": {
    "type": "passthrough",
    "kind": "gam_targeting_groups",
    "groups": [
      {
        "criteria": [
          {
            "keyId": "17467172",
            "values": ["451422266753", "451420638792"]
          },
          {
            "keyId": "98765",
            "values": ["12345"],
            "exclude": true
          }
        ]
      },
      {
        "criteria": [
          {
            "keyId": "24680",
            "values": ["13579"]
          }
        ]
      }
    ]
  }
}
```

## Validate a signal draft with `dry_run=true`

Use `POST /api/v2/storefront/esa/{esaId}/signals?dry_run=true` to validate and
preview a signal draft without persisting it. The request body is the same as
`POST /signals`, but the response is a validation result rather than a saved
signal object. Read the `valid` flag, not the HTTP status — a dry run returns
`200` even when the draft is invalid, and the `issues[]` array explains why. A
malformed request body still returns `422`.

```bash theme={null}
curl -X POST "https://api.apostra.com/api/v2/storefront/esa/123/signals?dry_run=true" \
  -H "Authorization: Bearer $SCOPE3_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "signalId": "weekday_or_sports",
    "name": "Weekday or sports package",
    "valueType": "binary",
    "adapterConfig": {
      "type": "passthrough",
      "kind": "gam_targeting_groups",
      "groups": [
        {
          "criteria": [
            {
              "keyId": "17467172",
              "values": ["451422266753"]
            }
          ]
        }
      ]
    }
  }'
```

```json theme={null}
{
  "data": {
    "valid": true,
    "issues": []
  }
}
```

A dry run does not persist the signal. When the draft is valid the response also
includes a `preview` with the rendered wire signal and source projections. Use a dry
run before create when a client is assembling `adapterConfig` manually, or before
update to preview a change.

## Create a signal

```bash theme={null}
curl -X POST https://api.apostra.com/api/v2/storefront/esa/123/signals \
  -H "Authorization: Bearer $SCOPE3_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "signalId": "dayofweek_friday",
    "name": "DAY OF WEEK: FRIDAY",
    "valueType": "binary",
    "adapterConfig": {
      "type": "passthrough",
      "kind": "custom_key_value",
      "keyId": "17467172",
      "valueId": "451422266753"
    }
  }'
```

```json theme={null}
{
  "data": {
    "signalId": "dayofweek_friday",
    "name": "DAY OF WEEK: FRIDAY",
    "valueType": "binary",
    "adapterConfig": {
      "type": "passthrough",
      "kind": "custom_key_value",
      "keyId": "17467172",
      "valueId": "451422266753"
    }
  }
}
```

## List and get signals

```bash theme={null}
curl https://api.apostra.com/api/v2/storefront/esa/123/signals \
  -H "Authorization: Bearer $SCOPE3_API_KEY"
```

```bash theme={null}
curl https://api.apostra.com/api/v2/storefront/esa/123/signals/dayofweek_friday \
  -H "Authorization: Bearer $SCOPE3_API_KEY"
```

Signal response objects include at least `signalId` and `name`; sources can
return additional fields such as `description`, `status`, `valueType`,
`adapterConfig`, `coverageForecast`, `createdAt`, and `updatedAt`.

## Errors

* `400 VALIDATION_ERROR` — invalid ad server source id, malformed draft, missing required fields, or a source validation issue.
* `401 UNAUTHORIZED` — missing or invalid bearer token.
* `404 NOT_FOUND` — ad server source or signal does not exist for the caller.
* `502` or `503` — the ad server source or its upstream service could not complete the request.

See [Errors](/v2/reference/errors) for the full error contract.

## Related

<CardGroup cols={2}>
  <Card title="Storefront signal tasks" href="/v2/storefront/signals/tasks" icon="list-check">
    Create and manage storefront signals
  </Card>

  <Card title="Custom targeting and properties" href="/v2/storefront/esa/custom-targeting-properties" icon="sliders">
    How GAM key-values relate to site lists and publisher coverage
  </Card>

  <Card title="Signal overview" href="/v2/object-guides/signal" icon="wave-pulse">
    Signal model and lifecycle
  </Card>

  <Card title="Storefront API reference" href="/v2/storefront-api-reference" icon="code">
    OpenAPI reference for storefront routes
  </Card>
</CardGroup>
