Skip to main content

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 can travel to supporting sales agents during discovery or as buy-time targeting. Apostra also filters conflicting inventory at selection time. Property lists are a buyer-curated concept. They are owned by your advertiser and validated against the AAO property registry. Supporting sales agents receive list references that they resolve on demand. Apostra enforces exclusions on the buyer’s side as a backstop.

Include lists

Restrict targeting to a hand-picked allow-list (e.g., a curated PMP of premium publishers).

Exclude lists

Block specific domains across all of an advertiser’s campaigns (e.g., a brand-safety blocklist).

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 as filters.property_list during discovery when a sales agent declares either media_buy.features.property_list_filtering: true or media_buy.execution.targeting.property_list. The feature declaration authorizes discovery filtering only. A targeting_overlay.property_list reference is sent on media-buy packages only when the seller declares the execution-targeting dimension. Sellers without the relevant declaration do not receive the reference.
  • Exclude lists are not sent in get_products: AdCP discovery filters do not define property_list_exclude. They remain enforced on the buyer side during discovery and are sent as targeting_overlay.property_list_exclude on media-buy packages only when a sales agent declares media_buy.execution.targeting.property_list_exclude support. When an advertiser links multiple exclusion catalogs, the seller receives one reference that resolves to their combined, deduplicated identifiers. Apostra also enforces the list on the buyer’s side: products whose stored property identifiers match the advertiser’s exclusion list are filtered out of product discovery results, and execution rejects conflicting selections. Discovery reports the number removed in excludedProductCount, per-storefront counts in excludedProductCountsByStorefront, and bounded product details in excludedProductsByStorefront; each detail contains the product ID, name, and matching domains plus typed identifiers. Canonical get_products reports the same total as ext.interchange.excluded_product_count and each seller row carries excluded_product_count plus bounded excluded_products. The domains are the intersection of that seller product and the buyer’s own list, not the buyer’s complete list. When an exclusion empties a completed result, the response guidance says how many products from how many sellers were removed because they included excluded properties instead of calling the result a lack of matching inventory. A legacy candidate without seller identity is included in the total but not assigned to a seller row, so the guidance reports the removed-product count without inventing a seller count. 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. Matching uses the typed identifiers the current property-membership model stores: domain, subdomain, ios_bundle, android_package, apple_tv_bundle, bundle_id, apple_app_store_id, google_play_id, roku_store_id, roku_channel_id, fire_tv_asin, fire_tv_app_id, amazon_app_store_id, samsung_app_id, lg_channel_id, and vizio_app_id. Identifier values are exact after normalisation: domains, iOS bundles, and Apple TV bundles are lowercased; Android packages, generic bundles, store IDs, and channel IDs keep their case. Products can declare every AdCP identifier type, but DOOH venue and podcast list entries are not stored by this property-list model yet, so they do not match an exclusion list at selection time.
  • Products sold through an Apostra storefront take the declaration of the sales agent behind the storefront, not the storefront’s own. The storefront passes both references to that sales agent unchanged. Apostra reads the agent’s declaration from the AdCP capabilities it last refreshed for it, so a seller declares property_list or property_list_exclude in its own sales agent’s capabilities. Managed sales agents and platform adapters publish no such declaration, so they don’t receive references.
  • A list reference you set on a package yourself (property_list, property_list_exclude, collection_list or collection_list_exclude in a package’s targetingOverlay) is sent only to a seller that declares that exact field under media_buy.execution.targeting. A collection-list reference also needs the product to allow collection targeting (collection_targeting_allowed: true); AdCP treats a product without it as a bundle. If the seller does not declare the field, executing the media buy, or updating a media buy the seller already holds, fails with a validation error that names the package, the field and the seller, and nothing is sent. A seller that does not declare a field is not obliged to apply the list, so sending it anyway could let the buy run without it. Remove the reference from the package, or choose products whose seller declares the field. If Apostra cannot read the seller’s declaration, the request fails with a retryable error instead.

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 — Apostra’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: 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. 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.
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.
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.

Use with a V3 MCP agent

Buyer agents use one V3 write call for a property list’s lifecycle: save_property_list. Create a list with its advertiser, name, purpose, and identifiers; update its name or identifier set with its returned ID; or archive it with isArchived: true. Archiving is currently one-way. To validate identifiers without saving a list, use the same call with check: true, then pass its returned report ID to get(kind: "property_list", include: ["report"], reportId: "..."); this does not require a saved-list ID. get returns one bounded identifier page at a time. Use its identifierPage.nextOffset with identifierOffset to continue, and set identifierCategory to unresolved or registered when you need a resolution bucket. Each page item is an AdCP identifier (type, value). A value longer than 512 bytes is cut and flagged with value_truncated: true; a cut value no longer matches a property, so do not copy it into another request. search returns compact list summaries rather than identifier arrays. Use search(kind: "property_list", filter: { advertiserId: "123" }) to find an advertiser’s lists and get for a single list. The advertiser ID is required because property lists are advertiser-scoped. Every list belongs to the advertiser it was created for. By default a new list becomes the advertiser’s all-campaigns list for its purpose: it replaces the previous one (the response names it in replacedPropertyListIds; the replaced list stays saved but applies to no campaign), applies to every campaign, and is pushed to active media buys. Create it with appliesToAllCampaigns: false to save it for the advertiser without applying it to any campaign; the advertiser’s lists and active media buys are left unchanged. The choice is fixed at creation, and get and search return it as appliesToAllCampaigns. If the advertiser’s brand has no list slot for the list’s channels, a default create cannot apply the list anywhere: it is saved for campaign use only and returned with appliesToAllCampaigns: false.

Campaign lists

Every campaign inherits the advertiser’s lists. A campaign can also add one include list and one exclude list of its own, stored on the campaign:
  • campaignPropertyListId sets the campaign’s own include list.
  • campaignPropertyListExcludeId sets the campaign’s own exclude list.
Both fields work the same way on save_campaign and on the v2 API’s POST /campaigns/{campaignId}/property-lists. propertyListId is deprecated on both; use campaignPropertyListId. Until 2026-12-31 a non-null propertyListId keeps its old behaviour: it applies the named include list, which must already be configured for the advertiser, to the campaign’s active media buys, combined with the campaign’s own include list, without storing it as the campaign’s own list, and the response carries a deprecationNotice naming the replacement. propertyListId: null is refused with a validation error, on save_campaign and on the v2 endpoint alike. It used to remove property-list targeting from the campaign’s live buys, which would leave them running without the advertiser’s lists. To clear the campaign’s own list, send campaignPropertyListId: null; the advertiser’s lists keep applying. After 2026-12-31 propertyListId is refused. Each must be a list of the right purpose that belongs to the campaign’s advertiser. A list saved with appliesToAllCampaigns: false is the usual choice, because it applies only where a campaign uses it. Set either field to null to clear the campaign’s own list; that never removes an advertiser list. Both fields need an existing campaign (campaignId). The lists combine the way you would expect:
  • Exclusions add up. A property is excluded if the advertiser’s exclude lists or the campaign’s exclude list contain it. A campaign cannot remove an advertiser exclusion.
  • Inclusions narrow. When both the advertiser and the campaign have an include list, a property must be on both.
A media-buy package carries one reference per field, so when more than one list applies Apostra hosts one combined list and sends that reference. Sellers resolve it through the same GET /lists/:listId endpoint as any other list. Saving a campaign’s lists pushes the lists in effect to the campaign’s active media buys, and every buy created afterwards carries them from its first dispatch. Archiving a list clears it from any campaign that uses it. Read the campaign with get(kind: "campaign", include: ["propertyLists"]) (or GET /api/v2/buyer/campaigns/:campaignId/property-lists). The response has: The same combination applies when a campaign is executed: products whose properties are on the advertiser’s or the campaign’s exclude lists are rejected. Product discovery filters by the advertiser’s lists only; a campaign’s own lists take effect when its media buys are executed.

Endpoints

All buyer endpoints below are mounted under https://api.apostra.com/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. All propertyList-returning responses use the { propertyList } wrapper. List endpoints return { propertyLists, total }.

How to use

Create an include list

Domains-only shorthand:
Mixed web + mobile + CTV:
Response:
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.
unresolvedIdentifiers and registeredIdentifiers are transient output of the resolution call, not persisted state. They appear on create/update responses, then drop on subsequent GETs and on name-only PUTs. Only identifiers (the resolved set) is persisted on the list. Re-submit the identifier set on a PUT to re-surface them.

Update a list

A PUT replaces the full identifier set. Active media buys that reference this list are then notified — see Cascade behavior. You can pass domains, identifiers, or both:
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:
The response groups entries into action buckets: 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 a list with domains or identifiers, every active media buy that uses the list is notified via ADCP update_media_buy so the sales agent can refresh its cached property set. This covers include and exclude lists alike, whether the list is one of the advertiser’s lists or a campaign’s own list. Each campaign’s media buys receive the combined reference in effect for that campaign (see Property-list briefs for sellers). Each reference goes only to a sales agent that declares its targeting dimension (property_list for include, property_list_exclude for exclude), the same rule as a new buy. A buy whose sales agent doesn’t declare it is updated without that reference, and the skipped change is logged. The update response surfaces this with a cascadeSummary:
The cascade is best-effort. 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. A PUT that sends neither domains nor identifiers (a rename) does not cascade.
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.apostra.com/lists/:listId with an HMAC bearer token that Apostra 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):
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 lists during high-traffic windows. Cascading notifies every active media buy that uses the list; schedule large updates for off-peak times when possible.

Limits

Property-list briefs for sellers

What a seller’s agent receives and resolves when a buyer campaign carries a property list.

Publisher properties and coverage

How seller coverage appears in get_products.