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 apurpose 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_listduring discovery when a sales agent declares eithermedia_buy.features.property_list_filtering: trueormedia_buy.execution.targeting.property_list. Atargeting_overlay.property_listreference 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_filteringthere 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 inget_productsand intargeting_overlay, itsadcp.supported_versionsincludes3.0or3.1, and the product setsproperty_targeting_allowed: true. A product that does not allow property targeting is all or nothing, so the list cannot narrow it.
- AdCP 3.2: the seller declares
- 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 defineproperty_list_exclude. They remain enforced on the buyer side during discovery and are sent astargeting_overlay.property_list_excludeon media-buy packages only when a sales agent declaresmedia_buy.execution.targeting.property_list_excludesupport. 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 inexcludedProductCount, per-storefront counts inexcludedProductCountsByStorefront, and bounded product details inexcludedProductsByStorefront; each detail contains the product ID, name, and matching domains plus typed identifiers. Canonicalget_productsreports the same total asext.interchange.excluded_product_countand each seller row carriesexcluded_product_countplus boundedexcluded_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 declaresproperty_list_excludesupport, 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, andvizio_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_listorproperty_list_excludein 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_listorcollection_list_excludein a package’stargetingOverlay) is sent only to a seller that declares that exact field undermedia_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:- The seller applies the list. The seller declares support for the list
in its targeting capabilities (
property_listfor include lists,property_list_excludefor exclusion lists), or the product itself declaresproperty_list_excludein itsoverlay_support. An AdCP 3.0 or 3.1 seller declares include-list support withproperty_list_filteringinstead, 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. - 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’ssignal_targeting_groups, alongside any signal targeting the buy already has. An exclusion list goes in onenonegroup (the line item does not run on those sites), with an entry for each key it needs. An include list goes in oneanygroup 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). - 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_propertiesmust either name a specific site or app with its own identifiers, or name a publisher with a selector that Apostra can resolve (see below).
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.
Identifier types
Property lists accept AdCP-aligned typed identifiers. Pass a typed array viaidentifiers: [{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 onsave_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 withregistry: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 optionallyunit,confidence,measured_at,expires_atandmethodology_version.measured_atandexpires_atare ISO 8601 date-times. A value is limited to 2,048 bytes. AdCP’s free-formdetailsandextobjects are stored with the value but are neither applied nor returned byget. - 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: falseto 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 itsexpires_atis still shown, so check that field; it no longer counts when a list is resolved. A check (check: true) never saves scores and refusesfeatures.
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_coveredisinclude, so an exclude list that must also block its unscored identifiers needsif_not_covered: "include". - Edit the filters later. Save the list again with its
propertyListIdandfilters. The new filters replace the old ones (filters: nullremoves them), and a list with no identifiers must keep at least onefeature_requirementsentry.channels_anycannot 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
cascadeSummarycounts the buys across all the lists that were pushed. A score that only changes metadata such asmeasured_atis 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’scascadeSummarynames the lists whose push finished inpushedListIdsand the lists cancelled or not started inleftToRecheckListIds. 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 itsexpires_atchanges 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:campaignPropertyListIdsets the campaign’s own include list.campaignPropertyListExcludeIdsets the campaign’s own exclude list.
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.
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. Setproperty_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.
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 (causenot_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.
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 underhttps://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: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 totype: "domain". App identifiers are not in these.resolutionSummary— counts andresolutionRate(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
APUT 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:
resolutionSummary and cascadeSummary whenever
identifiers change. A name-only PUT returns neither.
Validate before you commit
UsePOST /api/v2/buyer/property-lists/check to lint a candidate identifier set
against curation rules without creating anything:
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 astargeting_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, orPOST /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 ADCPupdate_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 sendsupdate_media_buyto 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.
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:
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.
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:
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 aPropertyListReference 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):
cache_valid_until (24 h).
Best practices
- Always inspect
resolutionSummary. Any non-zerounresolvedCountindicates 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_bundleandandroid_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_anydoes 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.
PUTreplaces 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/checkon 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
Related
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.