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

# Property lists and audiences

> Include and exclude lists of websites, apps and CTV apps, and first-party audiences, for an advertiser

A **property list** is a named list of typed identifiers (websites, mobile apps, CTV apps) owned
by one advertiser. Its `purpose` is `include` (only buy on these) or `exclude` (never buy on
these).

## How lists are enforced

Enforcement happens when products are selected: at discovery and when a campaign goes live.

* **Exclude lists** are enforced by Semicola. Products whose publisher website domain (or a
  subdomain of it) is on a list leave discovery, and a campaign that still has such a product
  staged won't go live; the error names each product and the list. App and CTV properties without
  a website domain aren't matched. A list with `filters.channels_any` applies only to products on
  those channels.
* **Include lists** go to sellers that declare property-list support in their capabilities, as
  `targeting_overlay.property_list` on every package of a media buy (and as `property_list` on
  discovery once the seller is known to support it). Sellers that don't declare support never
  receive it. Creating an include list, replacing its identifiers, or attaching it to a campaign
  re-sends it to the advertiser's live media buys (`cascadeSummary`); a failed buy is logged and
  doesn't undo the change.

The seller resolves the reference at `GET /lists/{listId}` on the API origin, with the bearer
token carried in the reference. Storefronts hosted on Semicola declare property-list support: they
offer only products that cover a listed property (answering `property_list_applied: true`), and
refuse a package whose product covers none of the list. The answer is the ADCP `GetPropertyListResponse` (paginated with
`max_results` and `cursor`; cache it for 24 hours).

## Identifiers and resolution

Pass `domains` (shorthand for `type: "domain"`), typed `identifiers`, or both: 1 to 100,000 per
request, canonicalized and deduplicated. Website domains always resolve. App and CTV identifiers
resolve when they appear on products this account has been offered; the rest come back in
`unresolvedIdentifiers` and won't target. Only resolved identifiers are stored, so the
unresolved list appears on the create or update response and not on later reads. Always check
`resolutionSummary`.

`PUT` replaces the whole identifier set (there is no incremental add or remove). `DELETE`
archives the list.

## Upload a large list

For lists in the thousands, upload a file instead of batching JSON calls (separate create calls make
separate lists):

```bash theme={null}
curl -X POST \
  -H "Authorization: Bearer $TOKEN" \
  -F file=@exclusions.xlsx \
  -F name="Q1 - Global exclusions" \
  -F purpose=exclude \
  https://api.semicola.com/api/v2/buyer/advertisers/{advertiserId}/property-lists/upload
```

The file is xlsx, xls or csv, up to 10 MB. Semicola reads the first column of the first sheet and
ignores other columns. A first row whose first cell is `domain`, `identifier`, `url`, `host` or `app`
is treated as a header and skipped. Blank rows are dropped, duplicates are removed (the first one
is kept), and at most 100,000 identifiers are accepted. Each value is resolved as a website domain.
The `201` response is the created list plus an `upload` block (`filename`, `sizeBytes`, `totalRows`,
`skippedHeader`, `parsedIdentifiers`); compare `parsedIdentifiers` with your file and check
`resolutionSummary`.

## Check before you commit

`POST /api/v2/buyer/property-lists/check` sorts candidates into `ok`, `modify` (canonicalized,
for example `www.` removed), `remove` (duplicates) and `assess` (manual review). Every app and
CTV identifier lands in `assess`. Semicola isn't connected to the AgenticAdvertising.org property
registry yet, so domains can't be registry-confirmed or registry-blocked: clean domains land in
`assess` too. Checks that include a domain return a `reportId`, readable for 7 days.

## Audiences

`POST /api/v2/buyer/advertisers/{advertiserId}/audiences/sync` adds members (an `externalId` plus
an email, a phone with its `+` country code, their SHA-256 hashes, or universal ids), removes
members by `externalId`, or deletes an audience. Raw email and phone are normalized and hashed
before anything is stored. The call returns `202` with a `taskId` for `GET /tasks/{taskId}`.
`uploadedCount` is the stored member count.

A campaign's `audienceConfig` (`targetAudienceIds`, `suppressAudienceIds`) adds audiences; with
`deleteMissing: true` it replaces the set. They reach sellers that declare audience targeting as
`audience_include` and `audience_exclude`.

### How audiences reach sellers

* **At launch.** When a buy goes to a seller that declares audience targeting, Semicola first sends
  that seller the campaign's audiences with AdCP `sync_audiences`: every current member, as the
  `externalId` and hashed identifiers only. This shares the buy's 5-second budget with
  [event-source registration](/buy/event-sources#how-a-source-reaches-each-seller) and never holds
  the buy back. An audience the seller already holds isn't sent again; one that failed is retried on
  the next buy.
* **Later changes.** Each `sync_audiences` call afterwards sends the same change (members added,
  members removed with their hashed identifiers, or the audience deleted) to every seller already
  holding the audience.
* **Audiences off.** With **Audiences** off for a seller in
  [Connections](/buy/connections#distribution), nothing is sent to it, at launch or later, and its
  buys carry no `audience_include` or `audience_exclude`.

Matching happens at the sellers, so `GET /api/v2/buyer/advertisers/{advertiserId}/audiences` and
`list_audiences` report what they answered:

| Sellers' answers                                                                        | `status`                       | `matchedCount`                              |
| --------------------------------------------------------------------------------------- | ------------------------------ | ------------------------------------------- |
| At least one seller reports the audience ready                                          | `READY`                        | The largest match count a ready seller gave |
| None ready, at least one still processing                                               | `PROCESSING`                   | Empty                                       |
| None ready or processing, at least one too small                                        | `TOO_SMALL`                    | Empty                                       |
| Not held by any seller (not sent yet, or every seller failed or doesn't take audiences) | `READY` once the sync finishes | Empty                                       |

A seller's answer is read when Semicola sends it the audience or a change, so a seller that finishes
matching later shows up after the next change.

Semicola's hosted storefronts take audiences but have no identity graph to match against: they count
members and answer `processing`, never a match count, so an audience sent only to hosted storefronts
stays `PROCESSING`.

To share an audience with a chosen agent before any campaign buys there, use
[Syndication](/buy/syndication).

### The sync callback

Pass `pushNotificationConfig` on an audience sync to have Semicola `POST` the outcome to your URL
when the sync finishes (Semicola's own sync, not a seller's matching):

| Field            | Notes                                                                                                                                                                                                                                      |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `url`            | Checked when you call; a URL Semicola won't call is refused with `400`.                                                                                                                                                                    |
| `operation_id`   | Echoed back. Defaults to the sync's `taskId`.                                                                                                                                                                                              |
| `token`          | Echoed back, so you can match the call to your request.                                                                                                                                                                                    |
| `authentication` | `{ "schemes": ["Bearer"], "credentials": "…" }` sends `Authorization: Bearer …`. `["HMAC-SHA256"]` signs the call with `x-adcp-timestamp` and `x-adcp-signature: sha256=<hex>`, the HMAC of `<timestamp>.<body>` keyed with `credentials`. |

The body is the AdCP task payload: `idempotency_key`, `operation_id`, `task_id`,
`task_type: "sync_audiences"`, `status` (`completed` or `failed`), `timestamp`, `message`, your
`token`, and on success `result.audiences[]` (`audienceId`, `action`, `uploadedCount`). Credentials
are used for that one call and never stored. It's one attempt with a 10-second timeout, and it
doesn't hold up the sync's answer. The same outcome is also raised as an `audience.synced` or
`audience.sync_failed` [notification](/guides/notifications).
