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

# Property Lists

> Curate include and exclude inventory lists across web, mobile, and CTV for your advertisers and propagate them across active media buys

## Overview

A **property list** is a named, advertiser-scoped collection of typed
identifiers (websites, mobile apps, CTV apps) used to shape where a brand's
media runs. Lists carry a `purpose` of either `include` (only buy on these
properties) or `exclude` (never buy on these properties). Once created, lists
are linked to the brand's targeting profile and enforced when you buy:
include lists travel to sales agents as ADCP targeting, and exclude lists
filter conflicting inventory out at selection time.

Property lists are a **buyer-curated** concept. They are owned by your
advertiser and validated against the AAO property registry. Include lists are
pushed to sales agents as a `PropertyListReference` that the agent resolves on
demand; exclude lists are enforced by Scope3 on the buyer's side.

<CardGroup cols={2}>
  <Card title="Include lists" icon="check">
    Restrict targeting to a hand-picked allow-list (e.g., a curated PMP of
    premium publishers).
  </Card>

  <Card title="Exclude lists" icon="ban">
    Block specific domains across all of an advertiser's campaigns (e.g., a
    brand-safety blocklist).
  </Card>
</CardGroup>

## How lists are enforced

Enforcement happens at **selection time**: when products are discovered and
when a campaign is executed. Delivery-grain enforcement at the seller
(dropping individual impressions inside an already-running buy) is not yet
available.

* **Include lists** are sent to sales agents that declare `property_list`
  targeting support, as a `targeting_overlay.property_list` reference on
  every package of the media buy. Sellers that do not declare support never
  receive the reference.
* **Exclude lists** are enforced by Scope3 on the buyer's side. Products
  whose publisher domains match the advertiser's exclusion list are filtered
  out of product discovery results, and executing a campaign fails with a
  clear error naming the conflicting products when a selected product's
  publisher is on the list. Remove those products from the buy, or remove the
  exclusion, to proceed. Exclusion matching is by publisher website domain;
  app and CTV properties without a website domain are not matched at
  selection time.

## Concept

Each list stores a set of typed identifiers that are normalized, deduped, and
resolved through two systems:

1. **AAO registry** — the cross-publisher Ad Context registry used to confirm a
   property is a real, identified entry.
2. **Local property catalog** — Scope3's mapping of identifiers to targetable
   `Property` records used by sales agents.

Submitted identifiers land in one of three buckets, surfaced in every
create/update response as a `resolutionSummary`:

| Bucket            | Meaning                                | Targets?                                               |
| ----------------- | -------------------------------------- | ------------------------------------------------------ |
| `resolvedCount`   | Mapped to a local `Property` record    | Yes                                                    |
| `registeredCount` | Known to AAO but no local property yet | Not yet — will become targetable as catalog catches up |
| `unresolvedCount` | Not found anywhere                     | No — silently skipped                                  |

A single list can hold up to **100,000 identifiers** per request. The service
chunks large inputs server-side against the AAO registry (which itself caps at
10,000 domains per call) and returns a single resource with the full
resolution summary.

## Identifier types

Property lists accept AdCP-aligned typed identifiers. Pass a typed array via
`identifiers: [{type, value}]`, or use the `domains: string[]` shorthand
when every entry is a website domain.

| Type                           | Resolves via                          | Example value         |
| ------------------------------ | ------------------------------------- | --------------------- |
| `domain`                       | `Domain.domain` (`SITE`)              | `example-times.com`   |
| `subdomain`                    | `Domain.domain` (`SITE`)              | `news.example.com`    |
| `ios_bundle`                   | `App.bundle` (Apple App Store)        | `com.facebook.katana` |
| `android_package`              | `App.bundle` (Google Play)            | `com.facebook.katana` |
| `apple_tv_bundle`              | `App.bundle` (Apple App Store)        | `com.netflix.Netflix` |
| `bundle_id` (generic fallback) | `App.bundle` (any store)              | `com.example.app`     |
| `apple_app_store_id`           | `Domain.domain` (`APPLE_APP_STORE`)   | `284882215`           |
| `google_play_id`               | `Domain.domain` (`GOOGLE_PLAY_STORE`) | `com.example.app`     |
| `roku_store_id`                | `Domain.domain` (`ROKU`)              | `12`                  |
| `fire_tv_asin`                 | `Domain.domain` (`AMAZON`)            | `B00X4WHP5E`          |
| `samsung_app_id`               | `Domain.domain` (`SAMSUNG`)           | `G19173000091`        |

A single mobile app can appear in a property list under multiple identifier
types (e.g. `ios_bundle` AND `apple_app_store_id`); each resolves independently
against the AAO registry / local catalog.

<Note>
  `domains: ["example-times.com"]` is exactly equivalent to
  `identifiers: [{ "type": "domain", "value": "example-times.com" }]`. You can pass
  both fields in the same request — they're concatenated and deduplicated.
</Note>

<Note>
  **Read-side normalization.** `apple_tv_bundle` and `ios_bundle` share the
  same backing column (`App.bundle` with `appStore=APPLE_APP_STORE`), so an
  `apple_tv_bundle` write reads back as `ios_bundle` on subsequent `GET`/`list`
  responses. The generic `bundle_id` fallback likewise normalizes to the
  resolved app row's store-typed form (`ios_bundle` or `android_package`).
  Submit the type that best matches the AdCP property registry; expect the
  response to carry the canonical store-typed form.
</Note>

## Endpoints

All buyer endpoints below are mounted under `https://api.interchange.io/api/v2/buyer`.
The `GET /lists/:listId` resolution endpoint is mounted at the **app root**
(no `/api/v2/buyer` or `/api/v2/storefront` prefix) per ADCP convention.

| Method   | Path                                                             | Purpose                                                                                  |
| -------- | ---------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| `GET`    | `/api/v2/buyer/advertisers/:advertiserId/property-lists`         | List lists for an advertiser                                                             |
| `POST`   | `/api/v2/buyer/advertisers/:advertiserId/property-lists`         | Create a list                                                                            |
| `GET`    | `/api/v2/buyer/advertisers/:advertiserId/property-lists/:listId` | Get one list                                                                             |
| `PUT`    | `/api/v2/buyer/advertisers/:advertiserId/property-lists/:listId` | Replace a list's domains and/or name                                                     |
| `DELETE` | `/api/v2/buyer/advertisers/:advertiserId/property-lists/:listId` | Archive a list                                                                           |
| `POST`   | `/api/v2/buyer/property-lists/check`                             | Validate a candidate domain set without creating a list                                  |
| `GET`    | `/lists/:listId`                                                 | High-cardinality public read used by sales agents (HMAC token auth, mounted at app root) |

All `propertyList`-returning responses use the `{ propertyList }` wrapper.
List endpoints return `{ propertyLists, total }`.

## How to use

### Create an include list

Domains-only shorthand:

```bash theme={null}
curl -X POST 'https://api.interchange.io/api/v2/buyer/advertisers/12345/property-lists' \
  -H 'Authorization: Bearer scope3_<your_api_key>' \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Q1 UK Premium",
    "purpose": "include",
    "domains": ["example-times.com", "example-press.co.uk", "example-news.com"],
    "filters": {
      "channels_any": ["display", "olv"],
      "countries_all": ["GB"]
    }
  }'
```

Mixed web + mobile + CTV:

```bash theme={null}
curl -X POST 'https://api.interchange.io/api/v2/buyer/advertisers/12345/property-lists' \
  -H 'Authorization: Bearer scope3_<your_api_key>' \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Cross-screen premium",
    "purpose": "include",
    "identifiers": [
      { "type": "domain", "value": "example-times.com" },
      { "type": "ios_bundle", "value": "com.exampletimes.ExampleTimes" },
      { "type": "android_package", "value": "com.exampletimes.android" },
      { "type": "apple_app_store_id", "value": "284862083" },
      { "type": "roku_store_id", "value": "12" }
    ]
  }'
```

Response:

```json theme={null}
{
  "propertyList": {
    "listId": "42",
    "name": "Cross-screen premium",
    "purpose": "include",
    "identifiers": [
      { "type": "domain", "value": "example-times.com" },
      { "type": "ios_bundle", "value": "com.exampletimes.ExampleTimes" },
      { "type": "android_package", "value": "com.exampletimes.android" }
    ],
    "unresolvedIdentifiers": [
      { "type": "apple_app_store_id", "value": "284862083" },
      { "type": "roku_store_id", "value": "12" }
    ],
    "registeredIdentifiers": [],
    "domains": ["example-times.com"],
    "unresolvedDomains": [],
    "registeredDomains": [],
    "propertyCount": 14,
    "resolutionSummary": {
      "totalRequested": 5,
      "resolvedCount": 3,
      "registeredCount": 0,
      "unresolvedCount": 2,
      "resolutionRate": 0.6
    },
    "createdAt": "2026-04-25T10:30:00.000Z",
    "updatedAt": "2026-04-25T10:30:00.000Z"
  }
}
```

**Response fields**

* `identifiers` — typed `{type, value}[]` actually resolved to local Property
  rows. **Persisted** as catalog membership.
* `unresolvedIdentifiers` — submitted identifiers with no matching local
  Property record. **Transient** — see persistence note below.
* `registeredIdentifiers` — identifiers found in the AAO registry but not yet
  locally targetable. Today only `domain` entries can land here. **Transient**.
* `domains` / `unresolvedDomains` / `registeredDomains` — convenience views of
  the above filtered to `type: "domain"`. App identifiers are **not** in these.
* `resolutionSummary` — counts and `resolutionRate` (0..1) over the
  deduplicated, normalized input.

<Note>
  `unresolvedIdentifiers` and `registeredIdentifiers` are **transient output of
  the resolution call**, not persisted state. They appear on create/update
  responses, then drop on subsequent `GET`s and on name-only `PUT`s. Only
  `identifiers` (the resolved set) is persisted on the list. Re-submit the
  identifier set on a `PUT` to re-surface them.
</Note>

### Update a list

A `PUT` replaces the full identifier set. Active media buys that reference
this list are then notified — see [Cascade behavior](#cascade-behavior).
You can pass `domains`, `identifiers`, or both:

```bash theme={null}
curl -X PUT 'https://api.interchange.io/api/v2/buyer/advertisers/12345/property-lists/42' \
  -H 'Authorization: Bearer scope3_<your_api_key>' \
  -H 'Content-Type: application/json' \
  -d '{
    "identifiers": [
      { "type": "domain", "value": "example-times.com" },
      { "type": "ios_bundle", "value": "com.exampletimes.ExampleTimes" }
    ]
  }'
```

The update response includes `resolutionSummary` and `cascadeSummary` whenever
identifiers change. A name-only `PUT` returns neither.

### Validate before you commit

Use `POST /api/v2/buyer/property-lists/check` to lint a candidate identifier set
against curation rules without creating anything:

```bash theme={null}
curl -X POST 'https://api.interchange.io/api/v2/buyer/property-lists/check' \
  -H 'Authorization: Bearer scope3_<your_api_key>' \
  -H 'Content-Type: application/json' \
  -d '{
    "identifiers": [
      { "type": "domain", "value": "example-times.com" },
      { "type": "domain", "value": "duplicate.com" },
      { "type": "domain", "value": "duplicate.com" },
      { "type": "ios_bundle", "value": "com.facebook.katana" }
    ]
  }'
```

The response groups entries into action buckets:

| Bucket   | Meaning                                                                                                               |
| -------- | --------------------------------------------------------------------------------------------------------------------- |
| `ok`     | Clean — safe to include                                                                                               |
| `modify` | Canonicalized (e.g., `WWW.example.com` → `example.com`)                                                               |
| `remove` | Drop — duplicate or blocked                                                                                           |
| `assess` | Manual review recommended. **All non-domain identifiers (mobile/CTV) land here** — AAO does not currently check them. |

Each bucket entry carries an `identifier: { type, value }` field that mirrors
the input — use it to disambiguate types.

`reportId` (plus `reportIds[]` when domain input is chunked) is returned only
when at least one `domain` was submitted. Bundles-only and store-IDs-only
requests return no report IDs since no AAO call was made.

## Cascade behavior

When you `PUT` an **include** list, every active media buy whose advertiser
references the list is notified via ADCP `update_media_buy` so the sales agent
can refresh its cached property set. The update response surfaces this with a
`cascadeSummary`:

```json theme={null}
{
  "cascadeSummary": {
    "totalMediaBuys": 4,
    "updatedCount": 3,
    "failedCount": 1
  }
}
```

<Warning>
  The cascade is **best-effort** and applies to include lists only — the ADCP
  `targeting_overlay.property_list` reference carries include semantics. Cascade
  failures do **not** roll back the list update; the database is the source of
  truth and per-media-buy errors are logged for retry.
</Warning>

Per-media-buy fan-out is bounded (concurrency 5) so a large advertiser cannot
thunder a single sales agent.

## Resolution endpoint

Sales agents resolve a `PropertyListReference` by calling
`GET https://api.interchange.io/lists/:listId` with an HMAC bearer token
that Scope3 mints when it embeds the reference in a request. This endpoint is
mounted at the **app root** (no `/api/v2/buyer` or `/api/v2/storefront` prefix) per ADCP convention. The
endpoint returns ADCP `GetPropertyListResponse` shape (not the standard
`{ data, error, meta }` envelope):

```json theme={null}
{
  "list": { "list_id": "42", "name": "Cross-screen premium" },
  "identifiers": [
    { "type": "domain", "value": "example-times.com" },
    { "type": "ios_bundle", "value": "com.exampletimes.ExampleTimes" },
    { "type": "android_package", "value": "com.exampletimes.android" }
  ],
  "resolved_at": "2026-04-25T10:30:00.000Z",
  "cache_valid_until": "2026-04-26T10:30:00.000Z"
}
```

Agents are expected to cache the response until `cache_valid_until` (24 h).

## Best practices

* **Always inspect `resolutionSummary`.** Any non-zero `unresolvedCount`
  indicates identifiers that will not target — surface them to the user before
  going live.
* **Submit multiple identifier types for the same app.** A mobile app is more
  reliably resolved when you include both its `ios_bundle` and
  `android_package` (and, where available, `apple_app_store_id` /
  `google_play_id`). Each is resolved independently against the AAO registry.
* **Prefer one large list over many small ones.** A 100k-identifier list is
  cheaper than 100 lists of 1k identifiers because each list creates its own
  `SmartPropertyList` link.
* **Use `filters.channels_any` to scope a list.** Without it, the list applies
  across all of the brand's targeting profiles. With it, you can keep
  display/OLV separate from CTV.
* **Treat updates as full replacements.** `PUT` replaces the entire identifier
  set; there is no incremental add/remove. Re-send the union you want stored.
* **Validate before bulk import.** Run `POST /api/v2/buyer/property-lists/check`
  on the raw input first, especially when assembling a list from spreadsheets
  or third-party feeds.
* **Avoid touching include lists during high-traffic windows.** Cascading
  notifies every active media buy on the advertiser; schedule large updates
  for off-peak times when possible.

## Limits

| Limit                                             | Value                  |
| ------------------------------------------------- | ---------------------- |
| Identifiers per request (create / update / check) | 100,000                |
| Domains per AAO registry call (auto-chunked)      | 10,000                 |
| Cascade concurrency per update                    | 5 media buys at a time |
| List name length                                  | 1–255 characters       |
| Cache TTL on resolved list                        | 24 hours               |

## Related

<CardGroup cols={2}>
  <Card title="Property-list briefs for sellers" href="/v2/storefront/property-list-briefs" icon="list-check">
    What a seller's agent receives and resolves when a buyer campaign carries a
    property list.
  </Card>

  <Card title="Publisher properties and coverage" href="/v2/storefront/inventory-sources/publisher-properties-coverage" icon="globe">
    How seller coverage appears in `get_products`.
  </Card>
</CardGroup>
