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 apply to the advertiser’s campaigns and are 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. A targeting_overlay.property_list reference is sent on media-buy packages only when the seller declares that it applies the list, and what counts depends on the AdCP release the seller speaks:
    • AdCP 3.2: the seller declares media_buy.execution.targeting.property_list. property_list_filtering there means discovery filtering only, so it does not make Apostra send the reference.
    • AdCP 3.0 and 3.1: the seller declares media_buy.features.property_list_filtering: true, which those releases define as honoring the list in get_products and in targeting_overlay, its adcp.supported_versions includes 3.0 or 3.1, and the product sets property_targeting_allowed: true. A product that does not allow property targeting is all or nothing, so the list cannot narrow it.
    Sellers without the relevant declaration do not receive the reference.
  • Apostra-managed sales agents on Google Ad Manager do not take list references. For their products, Apostra sends both kinds of list as the agent’s property signal instead (see route 2 below).
  • 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. When a seller declares property_list_exclude support, Apostra sends the exclusion list with the package and the seller removes matching properties within the product. Executing a campaign rejects a matching product only when its seller cannot enforce the exclusion list. It also rejects a product with no listed properties when that seller cannot enforce the list, because Apostra cannot prove where the product will run. 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.

Every list must be enforceable

Apostra only offers and buys a product when something will actually enforce every property list that applies to it: the advertiser’s lists and, when the buy is launched, the campaign’s own lists (see Campaign lists). For each product and each list, one of these must hold:
  1. The seller applies the list. The seller declares support for the list in its targeting capabilities (property_list for include lists, property_list_exclude for exclusion lists), or the product itself declares property_list_exclude in its overlay_support. An AdCP 3.0 or 3.1 seller declares include-list support with property_list_filtering instead, as described above; those releases have no exclusion list. Apostra sends the list reference with the buy. For a product sold through an Apostra storefront, the declaration that counts is the sales agent’s behind the storefront, as described above; it is the same declaration that decides which references the buy carries. The reference covers every list that applies: exclusion lists are combined, and the advertiser’s include list is combined with the campaign’s own.
  2. Apostra sends the list as the seller’s property signals. This applies to products sold through an Apostra-managed sales agent whose ad server is Google Ad Manager and whose product offers the agent’s property signals. These are system-managed signals, one per ad-server key that identifies a site (for example property_domain); buyers are never offered them directly. Apostra reads the sites and apps the seller is authorized to sell and finds the ones on the list. Each one must have a value on one of those keys: a site’s value is its domain unless the seller has mapped it to a different value, and an app has a value only through the seller’s own mapping. Apostra then sends those values in the package’s signal_targeting_groups, alongside any signal targeting the buy already has. An exclusion list goes in one none group (the line item does not run on those sites), with an entry for each key it needs. An include list goes in one any group on a single key (the line item runs only on those sites), so the listed sites must share a key. The route does not apply if any listed site or app has no value, if a value is longer than 40 characters (Google Ad Manager’s limit; values are never shortened), if the listed sites of an include list share no key, if the product does not offer the property signals, or if one list needs more than 100 values. The values are computed from the sites and apps the seller is authorized to sell when the buy launches. If the list changes, or the seller authorizes a listed site afterwards, the values the buy was sent stop matching what Apostra computes now. Apostra checks this when you change a list and on its schedule, and updates the running buy with the new values instead of pausing it. It pauses the buy if the seller refuses the update, or does not confirm one that tightens the list (see When a live buy is paused).
  3. Apostra can prove the product satisfies the list. Apostra knows every site and app the product can run on, and none of them is on an exclusion list (or, for include lists, all of them are on every one). Each entry in the product’s publisher_properties must either name a specific site or app with its own identifiers, or name a publisher with a selector that Apostra can resolve (see below).
Products that name a publisher rather than its sites. A publisher_properties entry can name a publisher with a selection_type instead of listing properties. Apostra resolves it to the sites and apps that publisher’s adagents.json authorizes the product’s seller to sell: For a seller on the Apostra-managed sales agent, the authorized properties come from that sales agent’s own authorized list. For any other seller, they come from the AAO registry’s record of each publisher’s adagents.json, matched on the seller’s agent URL. For a product sold through an Apostra storefront, the seller is the source the storefront forwards the product to. Execution always looks the properties up afresh. If the lookup finds no authorized properties for the publisher, a by_id selector names a property the publisher does not authorize, or a selected property has no identifier to check, the product cannot be proven and counts as coverage_unknown. If the lookup itself fails (for example, the registry is unreachable), the buy fails with a retryable SERVICE_UNAVAILABLE, the same as when the lists cannot be read. Only products from a seller agent or the Apostra-managed sales agent are resolved this way; a product sold through an ad-server adapter connection or a linked storefront cannot be resolved yet. If none holds, the product is removed from discovery results and execution refuses it before anything reaches the seller. Discovery checks the advertiser’s lists; a campaign’s own lists are checked when the buy is launched. A buy with no property lists, from the advertiser or the campaign, is not affected. A seller can make its products eligible by declaring support for the list fields, by listing every site and app a product can run on, with identifiers, in its publisher_properties, by naming publishers whose adagents.json authorizes it for the properties it selects, or, on an Apostra-managed sales agent running on Google Ad Manager, by mapping each site and app to its ad-server value with the replace_property_mappings operation. The reasons are the same in discovery and execution: A further value, seller_cannot_apply, is reserved for an enforcement route that is not available yet and is not returned today. Treat any reason you do not recognise as “this list cannot be enforced on this product”. Discovery. Removals are reported separately from exclusion-list removals. Canonical get_products returns the total as ext.interchange.unenforceable_product_count, and each affected seller row carries unenforceable_product_count plus up to 25 unenforceable_products, each with its product ID, name, and the lists that cannot be enforced (the list, property_list or property_list_exclude, and its reason). unenforceable_products_truncated is true when more products were removed than are listed. The product discovery API reports the same as unenforceableProductCount and unenforceableProductsByStorefront. The v3 buyer search for wholesale_product reports the total as unenforceableProductCount on the result, and each entry of sellerOutcomes carries unenforceableProductCount, up to 25 unenforceableProducts (each with productId, name, and its lists) and unenforceableProductsTruncated. These fields are omitted when nothing was removed for this reason. When these removals empty a completed result, the response guidance says so instead of calling the result a lack of matching inventory. If Apostra cannot finish the check for a product (for example a lookup fails or times out), the product is withheld rather than offered unchecked, and it is reported separately as unverified: ext.interchange.unverified_product_count and each seller row’s unverified_product_count plus up to 25 unverified_products in get_products, and unverifiedProductCount and unverifiedProductsByStorefront in the product discovery API. The v3 buyer search reports unverifiedProductCount on the result and, on each sellerOutcomes entry, unverifiedProductCount, up to 25 unverifiedProducts (productId and name) and unverifiedProductsTruncated. The guidance says the products could not be verified. Retry the request. Execution. A buy that includes such a product fails with a VALIDATION_ERROR whose message names each product, the list, and the reason, and whose details carry reason: "PROPERTY_LIST_NOT_ENFORCEABLE" and one entry per product and list. Remove those products from the buy, or choose products whose seller applies property lists. If Apostra cannot read the property lists at that moment, cannot identify the seller behind a storefront product or read its declaration, or cannot look up the sites and apps a publisher authorizes the seller for, the buy fails with a retryable SERVICE_UNAVAILABLE instead of launching without the check; retry the execution. Live buys. The same rule is applied again to every active buy a list change touches. A live buy keeps running only while some route enforces every list that applies to it; otherwise it is paused. See When a live buy is paused.

Concept

Each list stores a set of typed identifiers that are normalized, deduped, and checked against the AAO registry, the cross-publisher Ad Context registry that identifies properties. Every submitted identifier is kept on the list; whether it is applied depends on its type and on AAO: These counts appear in every create/update response as a resolutionSummary.
Domains are always applied, even when AAO does not know them. AAO’s coverage of websites is incomplete; this is a known gap in AAO. A domain you add is kept and applied whether or not AAO has seen it, so an exclusion list keeps excluding a site even before AAO recognizes it. Apps, CTV apps and other identifier types are applied only once AAO recognizes them.
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 is checked against the AAO registry independently.
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. A subdomain is stored, and reads back on subsequent GET/list responses, as domain; an apple_tv_bundle reads back as ios_bundle. Other types, including the generic bundle_id, read back as submitted.

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.

Property scores

A V3 buyer agent can save a quality score for each property and set a list that keeps or blocks properties by score, for example “keep only sites with a Spektre ASPI of 80 to 90” or “never buy below 20”. The scores are yours: you supply them, Apostra stores them against your account and applies them when a list is resolved. Save scores with a list. Each identifier on save_property_list may carry features, a map from a feature id you choose to a score:
  • Feature ids are yours. Name them after the provider (spektre_aspi). An id that starts with registry: is reserved by AdCP and refused. An identifier can carry up to 20 features, and one save up to 100,000 scores in total.
  • Each score is an AdCP property feature value: value (a number, a string or a boolean) and optionally unit, confidence, measured_at, expires_at and methodology_version. measured_at and expires_at are ISO 8601 date-times. A value is limited to 2,048 bytes. AdCP’s free-form details and ext objects are stored with the value but are neither applied nor returned by get.
  • Scores belong to your account, not the list. A property has one score per feature, whichever list names it. A later save replaces the value for that feature and property, and Apostra keeps the earlier value as history. Saving a value that has not changed adds no history, so you can save the whole score list again every day.
  • Scores are saved before the list. If the list is then refused, the scores stay saved, and sending the same call again is safe: the scores are replaced in place and the list is created once.
  • Saving scores does not need the list to be applied anywhere: save a list with appliesToAllCampaigns: false to keep scores without changing any campaign. Archiving a list does not delete scores.
  • get(kind: "property_list") returns your scores beside each identifier it lists, as saved. A value past its expires_at is still shown, so check that field; it no longer counts when a list is resolved. A check (check: true) never saves scores and refuses features.
Filter by score. filters.feature_requirements works as AdCP defines it. Each requirement names a feature_id and any of min_value, max_value and allowed_values; a property must meet every requirement. if_not_covered decides what happens to a property with no usable score for that feature: exclude (the default) removes it and include keeps it. A score past its expires_at counts as no score.
  • A requirement is judged once you have saved a score for its feature. Until then it is not applied, so a floor saved before your scores cannot empty a list. The save response tells you when a requirement reads a feature with no saved score.
  • A list with filters and no identifiers resolves to every property you have scored that meets the requirements. The example above is the whole list. A brand floor is an exclude list with "max_value": 19.9. Because such a list covers only properties you have scored, it never blocks a property you have not scored; to block one, list it by identifier.
  • A list with identifiers and filters resolves to the identifiers that meet the requirements. An identifier with no live score for the requirement’s feature is removed unless if_not_covered is include, so an exclude list that must also block its unscored identifiers needs if_not_covered: "include".
  • Edit the filters later. Save the list again with its propertyListId and filters. The new filters replace the old ones (filters: null removes them), and a list with no identifiers must keep at least one feature_requirements entry. channels_any cannot change after creation, because it chose which of the advertiser’s list slots took the list.
  • A score or filter change that changes what a list resolves to is a list change. Saving the filters, or saving a changed score for a feature a list’s requirements read, pushes the list to the active media buys that use it, the same way editing its identifiers does, and the save response’s cascadeSummary counts the buys across all the lists that were pushed. A score that only changes metadata such as measured_at is not a change. A score save pushes every affected list inside the request. After 60 seconds it starts no further push and cancels the ones that have not reached a seller; a push already sent to a seller cannot be recalled, so it is allowed a few seconds to settle and is reported as pending. The save response’s cascadeSummary names the lists whose push finished in pushedListIds and the lists cancelled or not started in leftToRecheckListIds. A list left to the re-check is not a failure: the scheduled re-check of live buys compares each live buy with the lists as they now resolve and acts on any that are out of date, on its next run, and the counts cover only the pushes that finished. A score reaching its expires_at changes what a list resolves to at the time a seller next fetches it, without a push.
  • What a seller sees. The hosted list a seller fetches carries identifiers only, never scores. It reflects your scores as of the fetch; a seller that has cached the list sees a score change after its cache expires, at most 24 hours later. A scored list does not report total_count.
  • Exclusion checks (discovery and launch) read the same scores, so a floor list keeps scored properties below it out of a buy.

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.

Package lists

A single package of a media buy can carry its own lists, for example one include list per quality band. Set property_list or property_list_exclude in the package’s targetingOverlay on save_media_buy (the v2 batch create takes the same fields on a package’s targeting overlay). A package’s own list combines with the lists already in effect for the buy instead of replacing them:
  • Exclusions add up. The package’s exclude list is added to the advertiser’s and the campaign’s exclude lists.
  • Inclusions narrow. The package’s include list is intersected with the advertiser’s and the campaign’s include lists, so a property must be on all of them.
Apostra hosts one combined list for that package and sends the seller that reference. Packages without a list of their own carry the lists in effect, as before. Every list that applies to a package must be enforced, or the package is not bought; that holds for the combined lists too. A package’s own list that Apostra hosts must be an active list of the same advertiser, used in its own direction: an include list in property_list and an exclusion list in property_list_exclude. Use the agent_url and list_id of a list you saved on that advertiser (a combined include: or exclude: id of such lists is also accepted, in the matching field). A hosted list that is another advertiser’s, archived, or used in the other direction is refused with a validation error whose reason is PACKAGE_PROPERTY_LIST_REFERENCE_REFUSED and whose packages name each refused package, field, listId and cause (unrecognized_list_id, not_owned_by_advertiser or wrong_purpose). The check runs when you save the package, when the buy launches, and on every update that dispatches packages, a budget-only update included, because the seller is sent the stored list too. A status-only change such as a pause or a cancel is not blocked. A list hosted somewhere else is handled by where you send it:
  • save_media_buy (V3) refuses it with the same error (cause not_hosted_by_us): save the list on the advertiser and use its id.
  • The v2 batch create and update send it to the seller unchanged, as before, when no advertiser or campaign list applies to the same field, and the seller enforces it. When an advertiser or campaign list does apply to that field, the two cannot be combined, so the list that applies is sent in its place, as it always was. The response is unchanged; Apostra logs the package and list id that were not applied.
A change to the advertiser’s or campaign’s lists is pushed to the campaign’s active media buys combined with each package’s own lists. Before the push, the lists stored on each package are checked again. If one is no longer a list the advertiser owns (another advertiser’s, archived, or used in the wrong direction), the change is not sent for that buy and the push counts it in the failed updates (cascadeSummary.failedCount), so a package’s list is never silently dropped or sent to a seller. A stored list hosted elsewhere is replaced by the pushed list for the same field, as before.

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}[] that are applied: every domain and subdomain, plus other identifiers AAO recognizes.
  • unresolvedIdentifiers — submitted identifiers AAO does not recognize, other than domains. They are kept on the list but not applied.
  • registeredIdentifiers — always empty; kept for compatibility.
  • 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.
Every submitted identifier is stored, including unresolved ones, so unresolvedIdentifiers also appears on later GETs. Re-submit the identifier set on a PUT to have AAO check it again.

Update a list

A PUT replaces the full identifier set. Apostra sends update_media_buy to each seller that declares the list field, even when the reference itself is unchanged. That message tells the seller to refresh the list contents rather than waiting for its cache to expire. Each of those buys is also checked again against the new contents — 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

A media buy carries a reference to a list, not its contents. An include list travels as targeting_overlay.property_list and an exclusion list as targeting_overlay.property_list_exclude, each only to a sales agent that declares support for it (see How lists are enforced). The seller fetches the contents from the resolution endpoint. Every list change checks the active media buys it touches with the rule in Every list must be enforceable, against all the lists that apply to each buy after the change. What happens next depends on whether the reference changed:
  • The reference changes when you create a list for the advertiser, archive, delete or restore one, or apply or clear a campaign’s list (V3 save_campaign, or POST /campaigns/:campaignId/property-lists). This covers include and exclude lists alike, whether the list is one of the advertiser’s lists or a campaign’s own list, and each campaign’s buys receive the combined reference in effect for that campaign (see Take property-list changes on live buys). Each checked buy whose sales agent declares support for the list gets the new reference through ADCP update_media_buy. A buy whose sales agent does not declare support gets nothing sent; that is not a failure, it only means another route has to enforce the list.
  • The reference stays the same when you replace a list’s contents with PUT, but Apostra still sends update_media_buy to each seller that declares the field. This refreshes the seller’s hosted-list contents rather than waiting for its cache lifetime (see Limits). Buys that rely on Apostra proving their products clear are also checked against the new contents.
The outcome is reported in a cascadeSummary (on list create and update, and when V3 save_property_list archives or restores a list) or a cascade object (on campaign apply or clear). The REST DELETE returns 204 No Content without a summary, so use save_property_list with isArchived: true when you need to know whether archiving paused a buy:
A failed check or update does not roll back the list change; the database is the source of truth. A buy that cannot keep its lists enforced is paused — see When a live buy is paused. A PUT that sends neither domains nor identifiers (a rename) checks nothing.
Each update waits for the seller to confirm it. Fan-out is bounded: 2 campaigns at a time, and 5 buys at a time within each campaign. One advertiser-wide list save has a 210-second cascade deadline, leaving time for the 240-second request limit: a buy that has not started or confirmed by then is returned as pending, and no new seller update starts after the deadline.

When a live buy is paused

A live buy is paused, rather than left delivering on a list you changed or on no list, in two cases:
  • No route enforces one of its lists (reason: "list_not_enforceable"). Its sales agent does not apply the list, and Apostra cannot prove that every site and app its products can run on satisfies the list. Nothing is sent to the buy. The notification names each product, the list, and why. To restore the buy, remove the product, or resume it once its seller applies the list. A buy enforced through the seller’s property signals is paused the same way only when its values no longer match its lists and it cannot be updated in place (reason: "key_values_out_of_date", which happens when one product resolves to different values on different lines). Resuming it does not help: replace it with a new buy, which launches with current values.
  • Its seller does not take the change (reason: "list_update_not_applied"). A change is not treated as applied until the seller confirms it. A short-lived failure (a timeout, a network error, a seller that answers “try again”) is retried a few times within about 15 seconds. A buy whose update still fails, because the seller rejects it, does not confirm it, or the update cannot be sent, is paused.
A buy enforced through the seller’s property signals whose values no longer match its lists (a list edit, a score upload that changes which sites a list resolves to, or a site the seller authorized since launch) is not paused. Apostra sends the new values to the seller as an update to the buy’s existing packages and records them once the seller confirms. A buy is paused if the seller refuses or fails the update. What happens when the seller has not confirmed it depends on the change. If the change tightens the list for the buy (a newly excluded site, or a site dropped from an include list), the buy is paused, because the old values would keep delivering where the list now forbids: that includes a seller that queues the update for its own approval and a buy Apostra did not reach before the request’s deadline. If the change only loosens the list, the buy keeps running and is reported as pending, and the next re-check sends the update again. Apostra re-checks the live buys for up to 50 campaigns every 15 minutes, with the same rule. A full pass usually takes about 15 minutes for every 50 campaigns with an active buy (for example, about 30 minutes for 100 campaigns). Retries and repairs share each re-check with the pass, but always leave room for at least 25 campaigns of it, so a full pass normally takes at most about 15 minutes for every 25 campaigns (about an hour for 100). Those are places in a re-check, not a guaranteed rate: campaigns are checked one at a time, a campaign whose list change is already under way can wait up to 2 minutes, and a re-check stops starting new checks after 10 minutes, so a slow re-check covers fewer campaigns and the pass takes longer. If a campaign’s check cannot run, it is tried again at the start of the next two re-checks; after that it waits for the next full pass. That catches changes nothing announces, such as a seller dropping its list support or a product’s declared sites changing, and buys that were already running without their lists before this check existed. Such a buy is paused with reason: "list_not_enforceable" exactly as above, and nothing is sent to it. A paused buy stays paused until you resume it (V3 save_media_buy with isPaused: false). Resuming does not re-send the list: once the seller can apply it, save the list (or the campaign’s list) again so the change is checked and sent again. A buy that still cannot keep its lists enforced is paused again. If the seller queues the change for its own approval (some storefronts approve every change by hand), the buy is not paused. It is reported in pendingSellerApproval until the seller decides. The same pending outcome is returned if the shared cascade deadline passes before seller synchronization finishes or its campaign starts; retry or follow up with the seller before relying on the new list. A campaign whose check was already under way at the deadline gets about 10 more seconds to finish, and if it does, its buys are reported with their real outcome, including any pause. A buy reported pending is not changed afterwards: a seller update already sent runs to completion, but whatever the seller then decides, the buy is not paused automatically, and an update that had not been sent is not sent later. Save the list again to retry; a buy whose seller still does not take the change is then paused. Only a pause that was already under way at the deadline can still finish, and you receive the media_buy.list_action_required notification for it. A pending result looks like this:
If Apostra cannot run the check, for example because it cannot read the lists or which lists a seller applies, nothing is sent to that campaign’s buys and none of them is paused. V3 save_campaign returns an error, and the advertiser-wide cascadeSummary counts the campaign in failedCampaignCount. Save the change again to retry. For each paused buy you get a media_buy.list_action_required notification naming the buy, and the cascadeSummary reports the outcome per buy:
totalMediaBuys counts the active buys found. updatedCount counts those that took the change: their seller confirmed the refreshed list reference, or another route already enforces their lists (enforcedByAnotherRouteCount). failedCount counts those that did not: their update call failed, or no route enforces their lists, like mb_123 above, which was paused without being sent an update. A buy still pending is in neither and is listed in pendingSellerApproval. sellerUpdatedCount and sellerFailedCount count only update calls: those the seller confirmed and those that failed. Every checked buy not listed in actionRequired or pendingSellerApproval took the change. sellerUpdatedCount, sellerFailedCount, enforcedByAnotherRouteCount, pausedCount, pauseFailedCount, actionRequired, pendingCount, and pendingSellerApproval appear whenever at least one active buy was checked. An outcome of pause_failed is the urgent case: the buy needed pausing and could not be paused, so it may still be delivering without its lists enforced. Pause it yourself, or contact support if the pause is refused (for example while another change to the buy is awaiting seller approval). The same per-buy outcomes appear on the cascade object returned when a campaign’s property list is applied or cleared.

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. An advertiser has one all-campaigns list per purpose, and a campaign adds at most one of each, so put everything a brand needs excluded in one list.
  • filters.channels_any does not scope where a list applies. It is AdCP’s channel filter, stored with the list. An all-campaigns list applies to every campaign in every channel; use a campaign’s own list for a narrower rule.
  • 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 list changes during high-traffic windows. Each one checks every active media buy on the advertiser, and a change of reference is sent to each of their sellers; schedule large changes 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.