diff --git a/.changeset/creative-wording-errata-cta-carousel-filters.md b/.changeset/creative-wording-errata-cta-carousel-filters.md
new file mode 100644
index 00000000000..a685bdba834
--- /dev/null
+++ b/.changeset/creative-wording-errata-cta-carousel-filters.md
@@ -0,0 +1,10 @@
+---
+"adcontextprotocol": minor
+---
+
+Creative wording errata from the maintainer answers on adcp#8628, adcp#8629, adcp#8630 and adcp#8631. Descriptions, one schema `default` and docs only; no new fields or codes. Minor because the `asset_types` filter on `list_creative_formats` changes from any-of to all-of, which changes the results an any-of seller returned before; that makes it ineligible for a 3.2.x patch backport.
+
+- `CREATIVE_VALUE_NOT_ALLOWED` (adcp#8628): the closed set is `cta_values` on canonical formats or `allowed_values` in text-asset requirements. The code also covers a card's per-card `cta`, with `error.field` pointing at the card. Sellers carry the declared set in `error.details.accepted_values`, the canonical key in `core/error.json`; also sending `allowed_values` is harmless but not needed. The `text-asset-requirements` and `card-asset` descriptions, `sync_creatives` and the error-code snippet say the same. The Meta Reels table now says the buyer ships `assets.cta.content`, not `assets.cta.text`.
+- `image_carousel` (adcp#8629): `allowed_card_media_asset_types` declares `"default": ["image"]`, so a declaration that omits both it and the deprecated `allowed_card_asset_types` alias accepts image cards only; declare `["image", "video"]` for mixed-media carousels. `min_cards` and `max_cards` only narrow the `cards` slot: `max_cards: 35` alone still caps at the default 10, and a product taking 35 cards restates the slot with `max: 35`. The `responsive_creative` pool counts narrow their slots the same way. The format-conformance text notes that a parameter whose canonical schema states a `default` applies it when omitted.
+- `list_creative_formats` `asset_types` (adcp#8630, media-buy and creative request schemas): a format matches when it declares every requested type, whether each asset is required or optional and whether it sits at the top level or inside a `repeatable_group`. `list_creatives` keeps its any-of rule over creatives.
+- Multi-publisher products without `placements[]` (adcp#8631): `Product.format_options` now states that such a product MUST NOT carry publisher-scoped declarations, and that when no product-local declaration remains the seller MUST publish `placements[]` or split the product by publisher. The intersection is taken by declaration identity. The per-slice rule in `formats.mdx`, `canonical-formats.mdx` and `media-products.mdx` now describes how a buyer grades a product that breaks this rule.
diff --git a/docs/creative/canonical-formats.mdx b/docs/creative/canonical-formats.mdx
index 2e53da089ef..4d2843b3ff7 100644
--- a/docs/creative/canonical-formats.mdx
+++ b/docs/creative/canonical-formats.mdx
@@ -359,7 +359,7 @@ Add a new `format_kind` only when the buyer-facing asset contract or bounded ren
## Declaring a different slot contract with `params.slots`
-Canonical product declarations publish their complete asset-slot contract in `format_options[].params.slots`. Each entry declares an `asset_group_id`, an `asset_type`, and any requiredness, count, or technical constraints. A supplied list replaces the default slot set rather than adding to it; include every slot the buyer must or may supply. `slots_override` belongs only to a legacy catalog's `canonical:` projection annotation and is translated to `params.slots` when projecting that catalog. It is not a product-authoring field.
+Canonical product declarations publish their complete asset-slot contract in `format_options[].params.slots`. Each entry declares an `asset_group_id`, an `asset_type`, and any requiredness, count, or technical constraints. A supplied list replaces the default slot set rather than adding to it; include every slot the buyer must or may supply. Flat count parameters such as image_carousel `max_cards` or responsive_creative `headlines_max` only narrow a slot's `min` / `max`. To accept more than a default slot allows (35 carousel cards against the default `max: 10`), restate that slot here with the higher `max`. `slots_override` belongs only to a legacy catalog's `canonical:` projection annotation and is translated to `params.slots` when projecting that catalog. It is not a product-authoring field.
**Decision rule.** Would a buyer composing a creative manifest list **different assets** in this case vs the canonical's default? If yes, declare that different contract in `params.slots`. If no, use the canonical slots and narrow their applicable parameters. A slot type must be materializable as an asset of that type: use `card` for carousel cards and `video_brief` for a structured video brief, rather than an untyped `object`.
@@ -544,7 +544,7 @@ Meta Reels is a useful test of canonical-formats coverage: a platform-specific f
| --- | --- | --- |
| Vertical 9:16, 3-90s, 1080×1920, h264/aac, mp4 | `params` on `video_hosted` | Standard canonical parameters. |
| Headline (40ch), Primary text (125ch) | `params.headline_max_chars`, `params.primary_text_max_chars` + inherited `headline` / `primary_text` slots | Buyer ships text content via the slots; the param narrows the constraint. |
-| CTA from a fixed enum | `params.cta_values[]` + inherited `cta` slot | Buyer ships `assets.cta.text`; validator checks against the enum. |
+| CTA from a fixed enum | `params.cta_values[]` + inherited `cta` slot | Buyer ships `assets.cta.content`; validator checks against the enum. |
| Landing page URL | Inherited `landing_page_url` slot on `video_hosted` | No special handling. |
| Brand name overlay | NOT in the format. Auto-applied by Meta from the linked Page (auth context outside AdCP). | The inherited `brand_name` slot exists in the canonical; products that need explicit content (publisher-direct video, CTV) populate it. Meta doesn't consume it for Reels. |
| Logo overlay | NOT in the format. Auto-applied by Meta from the linked Page. | `BrandRef.brand_kit_override.logo` exists for formats whose seller-side renderer overlays a logo (host-read podcasts, CTV bumpers, publisher direct). Meta doesn't use it. |
@@ -589,7 +589,7 @@ Completeness governs what an option leaves undeclared. It never weakens what the
- A declared parameter is the whole constraint for its slot. A seller MUST NOT refuse, with a conformance code, a creative on the grounds of a declared parameter that the creative satisfies, and MUST NOT enforce a narrower value. An option declaring `main_image_sizes: [{ "width": 600, "height": 400 }]` accepts a 600×400 `main_image`.
- A closed set narrows a slot only when declared. With no `cta_values`, the `cta` slot takes free text within any declared `cta_max_chars`, and `CREATIVE_VALUE_NOT_ALLOWED` does not apply.
-- Any other narrowing parameter the option omits sets no limit through that parameter. Under `technical_requirements_complete: true` the seller holds no undeclared limit either, so a creative that conforms to every parameter is not refused with a conformance code. Without the assertion, the omitted constraint is undeclared as described above.
+- Any other narrowing parameter the option omits sets no limit through that parameter, unless its canonical schema states a `default`: that default applies. A carousel option that omits `allowed_card_media_asset_types` accepts image cards only (`default: ["image"]`). Under `technical_requirements_complete: true` the seller holds no undeclared limit either, so a creative that conforms to every parameter is not refused with a conformance code. Without the assertion, the omitted constraint is undeclared as described above.
The task-level rule, including how sellers code refusals outside format conformance, is in [`sync_creatives`: format conformance](/docs/creative/task-reference/sync_creatives#format-conformance).
@@ -609,9 +609,9 @@ Classification follows the buyer deliverable and who renders it, not the behavio
A publisher declares the format contract and enough production semantics for a buyer to know what it must supply: canonical kind, parameter constraints, slots, asset source, composition model, and required connections. It does **not** maintain a vendor list of every creative agent capable of producing that contract. Creative agents self-declare compatibility.
-For multi-publisher products, `Product.format_options[]` is the union of contracts the sales agent can traffic somewhere in the product. Eligibility is then computed per placement by intersecting that product set with the applicable publisher/placement declaration. One creative used across every selected placement must satisfy the intersection of all those effective sets; workflows that assign distinct creatives per placement may use the union, but must cover every placement.
+For multi-publisher products that publish `placements[]`, `Product.format_options[]` is the union of contracts the sales agent can traffic somewhere in the product. Eligibility is then computed per placement by intersecting that product set with the applicable publisher/placement declaration. One creative used across every selected placement must satisfy the intersection of all those effective sets; workflows that assign distinct creatives per placement may use the union, but must cover every placement.
-If the product has no explicit `placements[]`, treat each `publisher_properties[]` selector as an eligibility slice. Product-local options apply to every slice; publisher-scoped options apply only to slices with the matching `publisher_domain`. A single creative must satisfy the intersection across slices, while distinct creatives must cover every slice. If any slice cannot be resolved from the product and publisher catalog, fail closed and require explicit placements or a refined product.
+A multi-publisher product with no explicit `placements[]` has no public routing key for per-publisher creatives. Its `format_options[]` MUST be the common intersection across every publisher, taken by declaration identity, so it MUST NOT carry publisher-scoped options; when no product-local option remains, the seller MUST publish `placements[]` or split the product by publisher (see [matching and satisfaction](/docs/creative/formats#matching-and-satisfaction)). When grading a product that breaks this rule, treat each `publisher_properties[]` selector as an eligibility slice. Product-local options apply to every slice; publisher-scoped options apply only to slices with the matching `publisher_domain`. A single creative must satisfy the intersection across slices, while distinct creatives must cover every slice. If any slice cannot be resolved from the product and publisher catalog, fail closed and require explicit placements or a refined product.
For exact publisher-format support, a creative capability's `format` repeats the publisher declaration's `{publisher_domain, format_option_id}` identity and narrows the same canonical contract. Generic creative agents advertise a canonical parameter envelope without publisher identity. A generic capability matches only when it can satisfy every constraint on the target declaration; equal `format_kind` values alone are not proof of compatibility.
@@ -1378,7 +1378,7 @@ Pinterest is the canonical disambiguation example because a single platform sell
|---|---|---|
| **Promoted Pin** (sponsored single Pin in the home/search feed) | `native_in_feed` | Asset-bundle composition — buyer ships title + image + body + landing URL; Pinterest's renderer assembles the Pin to match feed look-and-feel. No catalog feed; the buyer-supplied bundle IS the creative. |
| **Pinterest Collection** (single hero image + 3 product thumbnails sourced from a catalog) | `sponsored_placement` | Catalog-keyed — buyer ships a `source_catalog` reference (and an optional `hero_asset`); Pinterest composes the per-item thumbnails by reading product catalog rows. `fanout_mode: multi_item_in_creative` distinguishes Collection from per-item retail-media SP. |
-| **Pinterest Idea Pin** (multi-page swipeable native unit; pages may be image or video) | `image_carousel` | Multi-card swipe shape — the carousel canonical's per-card slot is polymorphic image-or-video, which matches Idea Pin pages that mix the two within one Pin. |
+| **Pinterest Idea Pin** (multi-page swipeable native unit; pages may be image or video) | `image_carousel` | Multi-card swipe shape — the carousel canonical's per-card slot is polymorphic image-or-video, which matches Idea Pin pages that mix the two within one Pin. The declaration sets `allowed_card_media_asset_types: ["image", "video"]`, since the default is image only. |
| **Pinterest Shopping Pin** (single product Pin keyed to a catalog row) | `sponsored_placement` with `fanout_mode: single_item` | Single rendered creative pulled from one catalog item — still catalog-keyed composition, just one item per ad. |
The cleave is **asset-bundle vs catalog-row composition**, not "is it Pinterest." Same logic applies to Snap Story Ad (native_in_feed) vs Snap Collection (sponsored_placement), TikTok TopView (native_in_feed via `applies_to_channels: ["social"]`) vs TikTok Collection (sponsored_placement), and so on. Buyer agents route on the composition shape; the publisher's brand of the surface is incidental.
diff --git a/docs/creative/channels/carousels.mdx b/docs/creative/channels/carousels.mdx
index dde06f2c35f..aaee1dfb652 100644
--- a/docs/creative/channels/carousels.mdx
+++ b/docs/creative/channels/carousels.mdx
@@ -51,6 +51,27 @@ Use `image_carousel` when one creative contains an ordered set of cards. Card co
}
```
+## Card media and card count
+
+- **Card media.** `allowed_card_media_asset_types` lists the asset types a card's `media` may carry. It defaults to `["image"]`: a declaration that omits both it and the deprecated `allowed_card_asset_types` alias accepts image cards only. Declare `["image", "video"]` for mixed-media carousels.
+- **Card count.** The `cards` slot's `min` and `max` set the count (2 and 10 in the default slots). `min_cards` and `max_cards` only narrow them: the effective minimum is the larger value and the effective maximum the smaller one. `max_cards: 35` on its own still caps at 10. A product that takes 35 cards restates the `cards` slot with `max: 35`, and may also set `max_cards: 35`. A restated `slots` list replaces the whole default slot set (see [declaring a different slot contract](/docs/creative/canonical-formats#declaring-a-different-slot-contract-with-params-slots)), so it lists every slot the buyer may supply.
+
+```json
+{
+ "format_option_id": "long_carousel",
+ "format_kind": "image_carousel",
+ "params": {
+ "max_cards": 35,
+ "allowed_card_media_asset_types": ["image", "video"],
+ "slots": [
+ { "asset_group_id": "cards", "asset_type": "card", "required": true, "min": 2, "max": 35 },
+ { "asset_group_id": "primary_text", "asset_type": "text", "required": false },
+ { "asset_group_id": "landing_page_url", "asset_type": "url", "required": false }
+ ]
+ }
+}
+```
+
## Card image size
`card_aspect_ratio` fixes the shape every card shares. `card_image_sizes` (added in AdCP 3.3) fixes the pixel size of card images. It is a list of accepted logical `{ width, height }` pairs, the same shape as `main_image_sizes` on `native_in_feed`.
diff --git a/docs/creative/formats.mdx b/docs/creative/formats.mdx
index 39e8cd09834..7827a34d1ff 100644
--- a/docs/creative/formats.mdx
+++ b/docs/creative/formats.mdx
@@ -108,9 +108,9 @@ Canonical compatibility is directional. A creative capability or manifest satisf
- An exact publisher claim must match both `publisher_domain` and `format_option_id`.
- Equal `format_kind` values alone do not prove compatibility.
-Satisfaction binds the seller as well as the buyer. A seller never refuses, on format-conformance grounds, a value that a declared parameter permits, and a narrowing parameter the option omits does not narrow its slot: with no `cta_values`, the `cta` slot takes free text. Policy and review refusals are separate. See [`sync_creatives`: format conformance](/docs/creative/task-reference/sync_creatives#format-conformance).
+Satisfaction binds the seller as well as the buyer. A seller never refuses, on format-conformance grounds, a value that a declared parameter permits, and a narrowing parameter the option omits does not narrow its slot (with no `cta_values`, the `cta` slot takes free text) unless its canonical schema states a `default`, which then applies: an `image_carousel` option silent on `allowed_card_media_asset_types` accepts image cards only. Policy and review refusals are separate. See [`sync_creatives`: format conformance](/docs/creative/task-reference/sync_creatives#format-conformance).
-For a product with multiple included placements, the selected format set must cover every placement. A seller must reject or refine a package with an uncovered placement; it must not silently remove inventory. If a multi-publisher product omits `placements[]`, each `publisher_properties[]` selector is an eligibility slice: product-local options apply to every slice, publisher-scoped options only to their publisher, and an indeterminate slice fails closed until the seller supplies placements or a refined product.
+For a product with multiple included placements, the selected format set must cover every placement. A seller must reject or refine a package with an uncovered placement; it must not silently remove inventory. A multi-publisher product that omits `placements[]` has no public routing key for per-publisher creatives, so its `format_options[]` MUST be the common intersection across every publisher, and it MUST NOT carry publisher-scoped options. The intersection is taken by declaration identity, not by accepted formats: when publisher-scoped declarations would accept the same creatives everywhere, the seller publishes one product-local declaration instead. When no product-local declaration remains, the product cannot be sold as published, and the seller MUST publish `placements[]` with publisher-scoped identities or split the product by publisher. A buyer grading a product that breaks this rule treats each `publisher_properties[]` selector as an eligibility slice: product-local options apply to every slice, publisher-scoped options only to their publisher, and an indeterminate slice fails closed until the seller supplies placements or a refined product.
## Discovery flow
diff --git a/docs/creative/task-reference/list_creatives.mdx b/docs/creative/task-reference/list_creatives.mdx
index a0c4fa52081..bfc93e48462 100644
--- a/docs/creative/task-reference/list_creatives.mdx
+++ b/docs/creative/task-reference/list_creatives.mdx
@@ -91,7 +91,7 @@ The `filters` object supports these optional, composable filters:
### Asset-type matching
-`asset_types` matches a creative when at least one direct object value in the top-level `creative.assets` map has an exact `asset_type` value in the requested set. Multiple requested values use **OR logic**. Array-valued slots are not inspected, and matching does not recurse into nested asset fields such as `cards[].media`; broader traversal is deferred.
+`asset_types` matches a creative when at least one direct object value in the top-level `creative.assets` map has an exact `asset_type` value in the requested set. Multiple requested values use **OR logic**. Array-valued slots are not inspected, and matching does not recurse into nested asset fields such as `cards[].media`; broader traversal is deferred. This filter runs over creatives; the `asset_types` filter on [`list_creative_formats`](/docs/creative/task-reference/list_creative_formats) runs over formats and keeps only formats that declare every requested type.
All active filter fields compose with **AND logic**. For example, the following request returns published-post creatives that also match the requested canonical format kind; a creative satisfying only one condition is excluded:
diff --git a/docs/creative/task-reference/sync_creatives.mdx b/docs/creative/task-reference/sync_creatives.mdx
index f9fcc3a88c4..189f057292b 100644
--- a/docs/creative/task-reference/sync_creatives.mdx
+++ b/docs/creative/task-reference/sync_creatives.mdx
@@ -303,10 +303,12 @@ The conformance codes are [`CREATIVE_SIZE_MISMATCH`](/docs/building/verification
- **Declared parameters bind the seller.** A seller MUST NOT refuse, with a conformance code, a creative that satisfies a parameter the targeted option declares on the grounds of that parameter, and MUST NOT enforce a narrower value than the one declared. A declared parameter is the whole constraint for its slot. When an option declares `main_image_sizes: [{ "width": 600, "height": 400 }]`, a 600×400 `main_image` conforms, and a seller that also wants other sizes declares them.
- **An absent closed set does not narrow.** A slot is limited to a closed set of values only when the option declares one, such as `cta_values` or `allowed_values`. With no `cta_values`, the `cta` slot takes free text within any declared `cta_max_chars`, and the seller MUST NOT refuse a CTA as `CREATIVE_VALUE_NOT_ALLOWED`. That code applies only to a value outside a declared list.
-- **Other absent narrowing parameters set no limit.** An option that omits a narrowing parameter (`title_max_chars`, `body_text_max_chars`, a size list, a duration range) imposes no limit through it. An optional slot such as `icon` may be left out. Whether the seller may still hold a limit it has not declared is what `technical_requirements_complete` answers. When the effective declaration asserts `true`, the seller has no undeclared technical limit, so it MUST NOT refuse, with a conformance code, a creative that conforms to every parameter of the option. Without that assertion the omitted constraint is undeclared, as [technical completeness](/docs/creative/canonical-formats#technical-completeness-and-file-size-units) describes, and a seller that enforces a limit SHOULD declare it on the option.
+- **Other absent narrowing parameters set no limit.** An option that omits a narrowing parameter (`title_max_chars`, `body_text_max_chars`, a size list, a duration range) imposes no limit through it. A parameter whose canonical schema states a `default` is the exception: the default applies, so an `image_carousel` option that omits `allowed_card_media_asset_types` accepts image cards only. An optional slot such as `icon` may be left out. Whether the seller may still hold a limit it has not declared is what `technical_requirements_complete` answers. When the effective declaration asserts `true`, the seller has no undeclared technical limit, so it MUST NOT refuse, with a conformance code, a creative that conforms to every parameter of the option. Without that assertion the omitted constraint is undeclared, as [technical completeness](/docs/creative/canonical-formats#technical-completeness-and-file-size-units) describes, and a seller that enforces a limit SHOULD declare it on the option.
- **Refusals outside format conformance are unaffected.** Content-policy, brand-safety, and accessibility review failures use [`CREATIVE_REJECTED`](/docs/building/verification/compliance-catalog#error-code-creative-rejected), or `status: "rejected"` when review completes after the sync. A seller's advertising policy uses [`POLICY_VIOLATION`](/docs/building/verification/compliance-catalog#error-code-policy-violation). A missing destination URL uses [`CREATIVE_MISSING_CLICK_URL`](/docs/building/verification/compliance-catalog#error-code-creative-missing-click-url) when the option requires one. A corrupt or unfetchable asset, or asset metadata that the inspected bytes contradict, is a failure of the asset rather than of the option's parameters. None of these grounds narrows a declared parameter.
-When a seller refuses on a declared constraint, it SHOULD name that constraint in `error.details`: `allowed_values` with `CREATIVE_VALUE_NOT_ALLOWED`, or the accepted sizes with `CREATIVE_SIZE_MISMATCH`. The buyer can then tell a mismatch with the declaration from a seller-side rule.
+When a seller refuses on a declared constraint, it SHOULD name that constraint in `error.details`: the declared set as `accepted_values` with `CREATIVE_VALUE_NOT_ALLOWED` (the canonical closed-set key in [`core/error.json`](https://adcontextprotocol.org/schemas/v3/core/error.json); also sending `allowed_values` is harmless but not needed), or the accepted sizes with `CREATIVE_SIZE_MISMATCH`. The buyer can then tell a mismatch with the declaration from a seller-side rule.
+
+`CREATIVE_VALUE_NOT_ALLOWED` turns on the declared set, not on the container holding the value. A card's per-card `cta` checked against `cta_values` is covered as well as a text asset, and `error.field` then points at the card, for example `creatives[0].assets.cards[2].cta`.
### Creative revisions
@@ -1329,7 +1331,7 @@ If the seller needs a person to confirm a match, it returns the [`submitted` env
| [`VALIDATION_ERROR`](/docs/building/verification/compliance-catalog#error-code-validation-error) | Manifest failed format validation: an asset file is corrupt or invalid, or assets don't match the format's requirements (codec, dimensions, duration). `error.field` identifies the offending asset path | Verify asset types and specifications match the format definition |
| [`PACKAGE_NOT_FOUND`](/docs/building/verification/compliance-catalog#error-code-package-not-found) | Package ID doesn't exist in media buy | Verify `package_id`; for legacy package correlation use [`get_media_buys`](/docs/media-buy/task-reference/get_media_buys) + package `context.buyer_ref` |
| [`CREATIVE_SIZE_MISMATCH`](/docs/building/verification/compliance-catalog#error-code-creative-size-mismatch) | Creative dimensions match no size the targeted format option accepts. Never returned for a size the option declares (see [Format conformance](#format-conformance)) | Resize to a declared size; `error.details` names the accepted sizes when the seller supplies them |
-| [`CREATIVE_VALUE_NOT_ALLOWED`](/docs/building/verification/compliance-catalog#error-code-creative-value-not-allowed) | A text value is outside the closed set the targeted option declares (`cta_values`, `allowed_values`). Never returned when the option declares no such set | Pick a value from `error.details.allowed_values` and resubmit |
+| [`CREATIVE_VALUE_NOT_ALLOWED`](/docs/building/verification/compliance-catalog#error-code-creative-value-not-allowed) | A value (a text asset or a card's `cta`) is outside the closed set the targeted option declares (`cta_values`, `allowed_values`). Never returned when the option declares no such set | Pick a value from `error.details.accepted_values` and resubmit |
| [`CREATIVE_REJECTED`](/docs/building/verification/compliance-catalog#error-code-creative-rejected) | Creative failed content-policy, brand-safety, or accessibility review | Revise according to the applicable policy or validated accessibility criteria |
| [`CREATIVE_LOCALE_NOT_ACCEPTED`](/docs/building/verification/compliance-catalog#error-code-creative-locale-not-accepted) | Assigned creative has no materialized variant accepted by the package's effective format `locale_policy`, lacks locale topology, or uses an ineligible `serve_default` | Supply a matching source/target variant, choose a compatible format option, or change the default policy |
| [`INVALID_STATE`](/docs/building/verification/compliance-catalog#error-code-invalid-state) | Creative is assigned to an active, non-paused package (blocks updates and `delete_missing` deletions). A seller that declares `creative.live_updates` refuses a change it does not apply in place with [`CREATIVE_LIVE_UPDATE_UNSUPPORTED`](/docs/building/verification/compliance-catalog#error-code-creative-live-update-unsupported) instead; every other seller keeps this code and this rule. | Pause the package first, or create a new creative version |
diff --git a/docs/media-buy/product-discovery/media-products.mdx b/docs/media-buy/product-discovery/media-products.mdx
index fff9567ca65..02c86a3ac70 100644
--- a/docs/media-buy/product-discovery/media-products.mdx
+++ b/docs/media-buy/product-discovery/media-products.mdx
@@ -470,11 +470,11 @@ Placements can also carry `identifiers[]` using the same `{ type, value }` shape
Product-level `format_options` define the creative formats accepted by the product as a whole. Placement-level `format_options`, whether returned inline on the product placement or inherited from a public publisher placement declaration, only narrow that product-wide set for the specific placement. Deprecated `format_ids` may appear only as a 3.x compatibility projection of the same declarations.
-For a multi-publisher or multi-placement product, the product-level list is the union of formats the seller can traffic somewhere in the product. It is not a promise that every option works on every placement. Compute each placement's effective set independently by intersecting the product declaration with that placement's publisher declaration.
+For a product that publishes `placements[]`, whether it spans one publisher or several, the product-level list is the union of formats the seller can traffic somewhere in the product. It is not a promise that every option works on every placement. Compute each placement's effective set independently by intersecting the product declaration with that placement's publisher declaration.
If both product and placement format declarations are present, buyers compute the effective accepted formats for that placement as their intersection. For legacy `format_ids`, first project named formats to canonical declarations through `canonical`, `v1_format_ref`, or the canonical mapping registry; do not compare raw `(agent_url, id)` values after projection. A legacy 300x250 display ID and a canonical `image` declaration with `width: 300` and `height: 250` are compatible. For 3.1+ `format_options`, match publisher-declared options by `{publisher_domain, format_option_id}`, match product-local options by `format_option_id` when `publisher_domain` is omitted, and otherwise match declarations with the same `format_kind` whose placement parameters narrow the product declaration.
-For a multi-publisher product without explicit `placements[]`, each `publisher_properties[]` selector is an eligibility slice. A publisher-scoped format option applies only to slices for the matching `publisher_domain`; a product-local option applies to every slice. One creative intended for the entire product must satisfy the intersection across all slices. A set of distinct creatives may use the union only when it covers every slice. If a seller or buyer cannot determine a slice's effective formats from the product and resolved publisher catalog, it must fail closed and require the seller to add placements or return a refined product.
+A multi-publisher product without explicit `placements[]` has no public routing key for per-publisher creatives, so its `format_options` MUST be the common intersection across every publisher and MUST NOT include publisher-scoped options, even ones that would accept the same creatives everywhere: the seller publishes one product-local option instead. When no product-local option remains, the seller MUST publish `placements[]` or split the product by publisher. A buyer grading a product that breaks this rule treats each `publisher_properties[]` selector as an eligibility slice. A publisher-scoped format option applies only to slices for the matching `publisher_domain`; a product-local option applies to every slice. One creative intended for the entire product must satisfy the intersection across all slices. A set of distinct creatives may use the union only when it covers every slice. If a seller or buyer cannot determine a slice's effective formats from the product and resolved publisher catalog, it must fail closed and require the seller to add placements or return a refined product.
Product gating is stricter than equivalence matching. When the product or placement declares fixed constraints such as `width`, `height`, `duration_ms_exact`, or a duration range, the buyer's selected format or creative manifest must declare and satisfy those constraints. A broad request (`format_kind: "image"` with no dimensions, or `video_hosted` with no duration) does not satisfy a fixed-size or fixed-duration product. For ranges, satisfaction means containment: a range-based request satisfies a product only when every value it permits falls within the product's accepted range; overlap alone is insufficient. An exact duration satisfies a range when the exact value falls inside it. The reverse is allowed: a broad product declaration can accept a specific 300x250 image or 30-second video unless another product constraint excludes it. See [format matching vs product satisfaction](/docs/creative/canonical-formats#format-matching-vs-product-satisfaction).
diff --git a/docs/reference/migration/creatives.mdx b/docs/reference/migration/creatives.mdx
index 0fedf08350a..2daeeb0bbc8 100644
--- a/docs/reference/migration/creatives.mdx
+++ b/docs/reference/migration/creatives.mdx
@@ -53,7 +53,7 @@ Replace `format_types` filters with `asset_types` (what the format needs) or `fo
}
```
-`asset_types` returns any format that includes at least one asset of that type — so a video format with companion banners appears in both `["video"]` and `["image"]` results.
+`asset_types` returns formats that declare every requested type. An asset counts whether it is required or optional, and whether it sits at the top level or inside a `repeatable_group`. A video format with companion banners appears in `["video"]`, `["image"]` and `["video", "image"]` results. To find formats with any of several types, make one call per type and union the results.
---
diff --git a/docs/snippets/compliance-error-codes.mdx b/docs/snippets/compliance-error-codes.mdx
index f7836636180..c51e8122838 100644
--- a/docs/snippets/compliance-error-codes.mdx
+++ b/docs/snippets/compliance-error-codes.mdx
@@ -58,7 +58,7 @@ description: "Canonical AdCP error codes with recovery classifications, remediat
| `CREATIVE_REVISION_CONTENT_MISMATCH` | correctable | resend the exact content previously bound to this revision_id, or mint a new revision_id for changed content |
| `CREATIVE_SIZE_MISMATCH` | correctable | resize or replace the creative with a size accepted by every selected package that may serve it |
| `CREATIVE_VALIDATION_FAILED_GENERIC` | correctable | correct the buyer-visible validation problem and resubmit the creative |
-| `CREATIVE_VALUE_NOT_ALLOWED` | correctable | pick a value from error.details.allowed_values (or re-fetch the format) and resubmit |
+| `CREATIVE_VALUE_NOT_ALLOWED` | correctable | pick a value from error.details.accepted_values (or re-fetch the format) and resubmit |
| `CREDENTIAL_IN_ARGS` | terminal | do NOT auto-retry — auto-retry re-logs the credential on each attempt. Move authentication material or caller-supplied trust material out of request args (top-level, {"context"}, {"ext"}, any nested location) onto the relevant transport authentication/trust channel or account provisioning path (Authorization: Bearer, RFC 9421 signature/JWKS, mTLS, MCP/A2A authentication framing); rotate any leaked credential, then resubmit |
| `CURSOR_EXPIRED` | correctable | obtain a latest checkpoint, rebuild authoritative account snapshots, then drain changes after the checkpoint |
| `EVALUATOR_AGENT_NOT_ACCEPTED` | correctable | replace the evaluator agent_url (evaluator.feature_agent.agent_url or the evaluator agent-form agent_url) with one from the seller's published accepted_verifiers, or drop the evaluator agent pointer to fall back to seller-default ranking |
@@ -558,9 +558,9 @@ The creative failed buyer-correctable validation, but the producer cannot classi
-**Suggested action:** pick a value from error.details.allowed_values (or re-fetch the format) and resubmit
+**Suggested action:** pick a value from error.details.accepted_values (or re-fetch the format) and resubmit
-A submitted text-asset value is not in the closed set the targeted format option declares: its {"allowed_values"} list, or a canonical closed-set parameter such as native_in_feed {"cta_values"}. Sellers MUST NOT return this code for a slot whose option declares no such set; that slot takes free text. Distinct from {"CREATIVE_REJECTED"} (generic creative-review failure) by being a closed-set constraint violation that the buyer can resolve mechanically without policy interpretation — the seller has published the complete list of acceptable values on the format, and any value outside that list is rejected by definition. The seller MUST set {"error.field"} to the offending asset's path within the manifest (e.g., {"creatives[0].creative_manifest.assets[0].value"} or the field name declared by the format) and SHOULD include the format's {"allowed_values"} array in {"error.details.allowed_values"} so the buyer agent can re-prompt its LLM with constrained sampling. Recovery: correctable (select a value from {"allowed_values"} and resubmit).
+A submitted value is outside the closed set the targeted format option declares ({"cta_values"} on canonical formats, {"allowed_values"} in text-asset requirements). The code turns on the declared set, not on the container holding the value: it covers a text asset and a card's per-card {"cta"} checked against {"cta_values"} alike. Sellers MUST NOT return this code for a slot whose option declares no such set; that slot takes free text. Distinct from {"CREATIVE_REJECTED"} (generic creative-review failure) by being a closed-set constraint violation that the buyer can resolve mechanically without policy interpretation — the seller has published the complete list of acceptable values on the format, and any value outside that list is rejected by definition. The seller MUST set {"error.field"} to the offending value's path within the manifest (e.g., {"creatives[0].assets.cta.content"}, {"creatives[0].assets.cards[2].cta"} for a card's CTA, or the field name declared by the format) and SHOULD include the declared set in {"error.details.accepted_values"}, the canonical closed-set key in {"core/error.json"}, so the buyer agent can re-prompt its LLM with constrained sampling. Also sending {"allowed_values"} is harmless but not needed; SDKs normalize legacy keys on read. Recovery: correctable (select a value from {"accepted_values"} and resubmit).
diff --git a/server/src/creative-agent/task-handlers.ts b/server/src/creative-agent/task-handlers.ts
index 24d6cafb710..6e5eaac25d7 100644
--- a/server/src/creative-agent/task-handlers.ts
+++ b/server/src/creative-agent/task-handlers.ts
@@ -220,7 +220,8 @@ function matchesAssetTypes(format: Format, assetTypes: string[]): boolean {
}
}
}
- return assetTypes.some(t => formatAssetTypes.has(t));
+ // All-of, per list-creative-formats-request `asset_types` (#8630).
+ return assetTypes.every(t => formatAssetTypes.has(t));
}
function matchesNameSearch(format: Format, search: string): boolean {
@@ -948,7 +949,7 @@ const TOOLS = [
adcp_major_version: ADCP_MAJOR_VERSION_PROP,
format_ids: { type: 'array', description: 'Return only these specific format IDs', items: FORMAT_ID_SCHEMA, minItems: 1 },
type: { type: 'string', description: 'Filter by format type', enum: ['audio', 'video', 'display', 'dooh'] },
- asset_types: { type: 'array', description: 'Filter to formats that include these asset types', items: { type: 'string', enum: ['image', 'video', 'audio', 'text', 'html', 'javascript', 'url'] }, minItems: 1 },
+ asset_types: { type: 'array', description: 'Filter to formats that declare every one of these asset types, counting optional assets and assets inside repeatable groups', items: { type: 'string', enum: ['image', 'video', 'audio', 'text', 'html', 'javascript', 'url'] }, minItems: 1 },
name_search: { type: 'string', description: 'Case-insensitive partial match on name or description' },
min_width: { type: 'integer', description: 'Minimum width in pixels (inclusive)' },
max_width: { type: 'integer', description: 'Maximum width in pixels (inclusive)' },
diff --git a/server/src/training-agent/task-handlers.ts b/server/src/training-agent/task-handlers.ts
index e966eb36377..03593862853 100644
--- a/server/src/training-agent/task-handlers.ts
+++ b/server/src/training-agent/task-handlers.ts
@@ -7677,7 +7677,7 @@ function nativeInFeedValidationError(
details: {
constraint: violation.constraint,
declared: violation.expected,
- ...(violation.code === 'CREATIVE_VALUE_NOT_ALLOWED' && { allowed_values: violation.expected }),
+ ...(violation.code === 'CREATIVE_VALUE_NOT_ALLOWED' && { accepted_values: violation.expected }),
...(violation.constraint === 'main_image_sizes' && { allowed_sizes: violation.expected }),
...(declaration.formatOptionId && { format_option_id: declaration.formatOptionId }),
},
diff --git a/server/tests/unit/creative-agent.test.ts b/server/tests/unit/creative-agent.test.ts
index 3813a7799aa..ca7b3d75a0d 100644
--- a/server/tests/unit/creative-agent.test.ts
+++ b/server/tests/unit/creative-agent.test.ts
@@ -30,6 +30,23 @@ const inlineMarkupManifest = (markup: string, size?: { width: number; height: nu
},
});
const getId = (format: Record) => (format.format_id as { id: string }).id;
+// asset_types filter fixtures (#8630): the carousel's only image sits inside
+// its repeatable card group, and the banner's text asset is optional.
+const ASSET_TYPE_FILTER_FORMATS = [
+ { format_id: { agent_url: TEST_AGENT_URL, id: 'image_only' }, assets: [{ asset_type: 'image', required: true }] },
+ {
+ format_id: { agent_url: TEST_AGENT_URL, id: 'image_with_optional_text' },
+ assets: [{ asset_type: 'image', required: true }, { asset_type: 'text', required: false }],
+ },
+ {
+ format_id: { agent_url: TEST_AGENT_URL, id: 'carousel_cards' },
+ assets: [
+ { item_type: 'repeatable_group', asset_group_id: 'cards', assets: [{ asset_type: 'image', required: true }] },
+ { asset_type: 'text', required: true },
+ ],
+ },
+ { format_id: { agent_url: TEST_AGENT_URL, id: 'video_only' }, assets: [{ asset_type: 'video', required: true }] },
+];
// ── Training agent formats (shared/formats.ts) ─────────────────────
@@ -421,6 +438,14 @@ describe('handleListCreativeFormats', () => {
}
});
+ it('keeps only formats that declare every requested asset type, counting optional and repeatable-group assets', () => {
+ const ids = (assetTypes: string[]) =>
+ (handleListCreativeFormats({ asset_types: assetTypes }, ASSET_TYPE_FILTER_FORMATS).formats as Array>).map(getId);
+ expect(ids(['image', 'text'])).toEqual(['image_with_optional_text', 'carousel_cards']);
+ expect(ids(['image'])).toEqual(['image_only', 'image_with_optional_text', 'carousel_cards']);
+ expect(ids(['image', 'video'])).toEqual([]);
+ });
+
it('filters by is_responsive: true', () => {
const result = handleListCreativeFormats({ is_responsive: true }, formats);
const returned = result.formats as Array<{ format_id: { id: string } }>;
diff --git a/server/tests/unit/training-agent-native-in-feed-options.test.ts b/server/tests/unit/training-agent-native-in-feed-options.test.ts
index 1d4c01af148..67bfbb379e1 100644
--- a/server/tests/unit/training-agent-native-in-feed-options.test.ts
+++ b/server/tests/unit/training-agent-native-in-feed-options.test.ts
@@ -203,7 +203,7 @@ describe('training agent sync_creatives honors declared native_in_feed options (
field: 'creatives[0].assets.cta.content',
details: {
constraint: 'cta_values',
- allowed_values: ['Get offer', 'Visit site'],
+ accepted_values: ['Get offer', 'Visit site'],
format_option_id: OPTION_ID,
},
});
@@ -306,7 +306,7 @@ describe('training agent sync_creatives honors declared native_in_feed options (
const cta = await sync([nativeCreative('bare_cta', { cta: 'EXPLORE_MORE' })]);
expect(cta.errors?.[0]).toMatchObject({
code: 'CREATIVE_VALUE_NOT_ALLOWED',
- details: { allowed_values: ['LEARN_MORE', 'SHOP_NOW', 'SIGN_UP', 'DOWNLOAD', 'APPLY_NOW'] },
+ details: { accepted_values: ['LEARN_MORE', 'SHOP_NOW', 'SIGN_UP', 'DOWNLOAD', 'APPLY_NOW'] },
});
});
diff --git a/static/schemas/source/core/assets/card-asset.json b/static/schemas/source/core/assets/card-asset.json
index 4fb3d16ffb0..9512417b024 100644
--- a/static/schemas/source/core/assets/card-asset.json
+++ b/static/schemas/source/core/assets/card-asset.json
@@ -12,7 +12,7 @@
"description": "Discriminator identifying this as a card asset. See /schemas/creative/asset-types for the registry."
},
"media": {
- "description": "The card's primary visual asset. Either an `image` or `video` asset, matching the parent format's `allowed_card_media_asset_types` parameter.",
+ "description": "The card's primary visual asset. Either an `image` or `video` asset, matching the parent format's `allowed_card_media_asset_types` parameter (image only when the parameter is omitted).",
"oneOf": [
{ "$ref": "/schemas/core/assets/image-asset.json" },
{ "$ref": "/schemas/core/assets/video-asset.json" }
@@ -29,7 +29,7 @@
},
"cta": {
"type": "string",
- "description": "Optional per-card call-to-action label (e.g., 'SHOP_NOW', 'LEARN_MORE'). When the parent format declares `cta_values` (allowed CTA labels), the per-card `cta` MUST be one of those values. Lets a Meta or TikTok carousel show different CTAs per card."
+ "description": "Optional per-card call-to-action label (e.g., 'SHOP_NOW', 'LEARN_MORE'). When the parent format declares `cta_values` (allowed CTA labels), the per-card `cta` MUST be one of those values; a seller refuses any other value with `CREATIVE_VALUE_NOT_ALLOWED`, with `error.field` pointing at the card's `cta`. Lets a Meta or TikTok carousel show different CTAs per card."
},
"landing_page_url": {
"$ref": "/schemas/core/assets/url-asset.json",
diff --git a/static/schemas/source/core/product.json b/static/schemas/source/core/product.json
index 58d2b9e0b9d..f947818eab2 100644
--- a/static/schemas/source/core/product.json
+++ b/static/schemas/source/core/product.json
@@ -48,7 +48,7 @@
"format_options": {
"type": "array",
"minItems": 1,
- "description": "Canonical format-option path: one or more inline format declarations the product accepts. Each element narrows a canonical format with parameters, slots, platform_extensions, and optional locale_policy. New 3.2 products MUST carry format_options; a seller MAY additionally project the same declarations to deprecated format_ids for older 3.x peers. A declaration carrying locale_policy is canonical-only because legacy format_ids cannot preserve locale eligibility; no product or placement format_id may project to an effective locale-constrained option.\n\nWhen placements are published, product-level format_options are the union of formats deliverable somewhere in the product and the upper bound for every placement. A placement's effective accepted set is the intersection of every applicable layer: (1) the product format_options; (2) the product placement's inline format_options, when present; and (3) for kind publisher_ref, the named publisher's adagents.json catalog narrowing. Resolve layer 3 by locating the matching placements[] entry: use its format_options when present, resolving bare format_option_id references against that same file's top-level formats[]; otherwise use top-level formats[] applicable to the placement's property_ids/property_tags. An omitted optional layer is unconstrained, but an unresolved publisher placement or format-option reference MUST fail closed. A publisher-referenced placement without inline product format_options therefore does NOT inherit the full product union when the publisher catalog supplies narrower placement or property-scoped acceptance.\n\nMatch publisher-declared options by {publisher_domain, format_option_id}, match product-local options by format_option_id when publisher_domain is omitted, and otherwise match declarations with the same format_kind whose narrower parameters satisfy the broader declaration. A product- or placement-level declaration MUST NOT introduce a format outside the product upper bound. Locale policy follows the same intersection. If the product locale policy is absent, a placement may introduce any concrete policy as a narrowing of an unconstrained option; when both are present, every placement range must be contained by a product range under RFC 4647 Basic Filtering. Locale eligibility is checked independently for every placement where an assignment may serve.\n\nFor a product or package containing multiple included placements, a single creative intended for every placement MUST lie in the intersection of every selected placement's effective set. Distinct per-placement creatives MAY use the union, but the selected creative set MUST cover every included placement; uncovered inventory MUST be rejected or refined, never silently omitted. If a product spans multiple publishers but omits placements[], there is no public routing key for per-placement creatives: its format_options MUST therefore be the common intersection accepted across every selected publisher/property scope. A seller that needs a union of publisher-specific formats MUST publish placements[] with publisher-scoped identities and narrowing. Commercial terms such as price, floor, availability, and deal eligibility are product facts, not format parameters.",
+ "description": "Canonical format-option path: one or more inline format declarations the product accepts. Each element narrows a canonical format with parameters, slots, platform_extensions, and optional locale_policy. New 3.2 products MUST carry format_options; a seller MAY additionally project the same declarations to deprecated format_ids for older 3.x peers. A declaration carrying locale_policy is canonical-only because legacy format_ids cannot preserve locale eligibility; no product or placement format_id may project to an effective locale-constrained option.\n\nWhen placements are published, product-level format_options are the union of formats deliverable somewhere in the product and the upper bound for every placement. A placement's effective accepted set is the intersection of every applicable layer: (1) the product format_options; (2) the product placement's inline format_options, when present; and (3) for kind publisher_ref, the named publisher's adagents.json catalog narrowing. Resolve layer 3 by locating the matching placements[] entry: use its format_options when present, resolving bare format_option_id references against that same file's top-level formats[]; otherwise use top-level formats[] applicable to the placement's property_ids/property_tags. An omitted optional layer is unconstrained, but an unresolved publisher placement or format-option reference MUST fail closed. A publisher-referenced placement without inline product format_options therefore does NOT inherit the full product union when the publisher catalog supplies narrower placement or property-scoped acceptance.\n\nMatch publisher-declared options by {publisher_domain, format_option_id}, match product-local options by format_option_id when publisher_domain is omitted, and otherwise match declarations with the same format_kind whose narrower parameters satisfy the broader declaration. A product- or placement-level declaration MUST NOT introduce a format outside the product upper bound. Locale policy follows the same intersection. If the product locale policy is absent, a placement may introduce any concrete policy as a narrowing of an unconstrained option; when both are present, every placement range must be contained by a product range under RFC 4647 Basic Filtering. Locale eligibility is checked independently for every placement where an assignment may serve.\n\nFor a product or package containing multiple included placements, a single creative intended for every placement MUST lie in the intersection of every selected placement's effective set. Distinct per-placement creatives MAY use the union, but the selected creative set MUST cover every included placement; uncovered inventory MUST be rejected or refined, never silently omitted. If a product spans multiple publishers but omits placements[], there is no public routing key for per-placement creatives: its format_options MUST therefore be the common intersection accepted across every selected publisher/property scope. A multi-publisher product without placements[] MUST NOT carry publisher-scoped format_options declarations; when no product-local declaration remains, the seller MUST publish placements[] or split the product by publisher. A seller that needs a union of publisher-specific formats MUST publish placements[] with publisher-scoped identities and narrowing. Commercial terms such as price, floor, availability, and deal eligibility are product facts, not format parameters.",
"items": {
"$ref": "/schemas/core/product-format-declaration.json"
},
diff --git a/static/schemas/source/core/requirements/text-asset-requirements.json b/static/schemas/source/core/requirements/text-asset-requirements.json
index 6169ca3bcab..352f1122f70 100644
--- a/static/schemas/source/core/requirements/text-asset-requirements.json
+++ b/static/schemas/source/core/requirements/text-asset-requirements.json
@@ -43,7 +43,7 @@
},
"minItems": 1,
"uniqueItems": true,
- "description": "Closed set of permitted string values for this text slot. When present, a conformant implementation MUST reject any submitted content not in this list with `CREATIVE_VALUE_NOT_ALLOWED` (echoing the offending field path in `error.field` and the allowed list in `error.details.allowed_values`). Matching is case-sensitive; producers MUST supply exact casing. If the submitted value contains unresolved template tokens (e.g., {{product_name}}), validation against allowed_values MUST be deferred until interpolation is complete. All declared constraints (allowed_values, character_pattern, max_length, etc.) are conjunctive — submitted content must satisfy every applicable constraint simultaneously."
+ "description": "Closed set of permitted string values for this text slot. When present, a conformant implementation MUST reject any submitted content not in this list with `CREATIVE_VALUE_NOT_ALLOWED` and set `error.field` to the offending field path; it SHOULD carry the allowed list in `error.details.accepted_values`, the canonical closed-set key in `core/error.json`. Matching is case-sensitive; producers MUST supply exact casing. If the submitted value contains unresolved template tokens (e.g., {{product_name}}), validation against allowed_values MUST be deferred until interpolation is complete. All declared constraints (allowed_values, character_pattern, max_length, etc.) are conjunctive — submitted content must satisfy every applicable constraint simultaneously."
}
},
"additionalProperties": true
diff --git a/static/schemas/source/creative/list-creative-formats-request.json b/static/schemas/source/creative/list-creative-formats-request.json
index 067ccddf798..2e18960a742 100644
--- a/static/schemas/source/creative/list-creative-formats-request.json
+++ b/static/schemas/source/creative/list-creative-formats-request.json
@@ -51,7 +51,7 @@
},
"asset_types": {
"type": "array",
- "description": "Filter to formats that include these asset types. For third-party tags, search for 'html' or 'javascript'. For published-post reference formats, search for 'published_post'. E.g., ['image', 'text'] returns formats with images and text, ['javascript'] returns formats accepting JavaScript tags.",
+ "description": "Filter to formats that declare every one of these asset types. An asset counts whether it is required or optional, and whether it is declared at the top level or inside a `repeatable_group`. For third-party tags, search for 'html' or 'javascript'. For published-post reference formats, search for 'published_post'. E.g., ['image', 'text'] returns only formats that declare both an image and a text asset, ['image'] also returns a carousel whose only image sits inside its card group, and ['javascript'] returns formats accepting JavaScript tags.",
"items": {
"$ref": "/schemas/enums/asset-content-type.json"
},
diff --git a/static/schemas/source/enums/error-code.json b/static/schemas/source/enums/error-code.json
index dbee1152222..858ed1b9c61 100644
--- a/static/schemas/source/enums/error-code.json
+++ b/static/schemas/source/enums/error-code.json
@@ -154,7 +154,7 @@
"CREATIVE_MISSING_CLICK_URL": "A submitted creative that requires a destination URL does not provide one. Sellers SHOULD identify the missing buyer-visible field in error.field and MUST NOT expose downstream ad-server names or internal object identifiers in the buyer-facing message. Recovery: correctable (add the required destination URL and resubmit the creative).",
"CREATIVE_VALIDATION_FAILED_GENERIC": "The creative failed buyer-correctable validation, but the producer cannot classify the failure with a more specific standard code. Producers SHOULD prefer a specific creative code whenever one applies and MUST keep buyer-facing messages free of vendor identifiers, internal object names, internal IDs, and stack traces. Recovery: correctable (correct the buyer-visible validation problem and resubmit the creative).",
"CREATIVE_LOCALE_NOT_ACCEPTED": "A creative bound to a locale-constrained product format has no materialized variant matching locale_policy.accepted_language_ranges, lacks protocol-declared locale topology, or uses serve_default with a seller-ineligible default variant. Seller ranges use RFC 4647 Basic Filtering and are applied independently for every placement where the assignment may serve, before buyer Lookup, locale_fallbacks, or default selection. Distinct from CREATIVE_REJECTED because this is a mechanically discoverable assignment-eligibility mismatch, not subjective content review. error.field SHOULD point to the offending creative or assignment; error.details SHOULD include format_option_id when present, accepted_language_ranges, available_variant_locales, and placement identity when applicable. Recovery: correctable (supply or assign a matching materialized locale variant, narrow placement scope, choose a compatible format option, or change an ineligible serve_default).",
- "CREATIVE_VALUE_NOT_ALLOWED": "A submitted text-asset value is not in the closed set the targeted format option declares: its `allowed_values` list, or a canonical closed-set parameter such as native_in_feed `cta_values`. Sellers MUST NOT return this code for a slot whose option declares no such set; that slot takes free text. Distinct from `CREATIVE_REJECTED` (generic creative-review failure) by being a closed-set constraint violation that the buyer can resolve mechanically without policy interpretation — the seller has published the complete list of acceptable values on the format, and any value outside that list is rejected by definition. The seller MUST set `error.field` to the offending asset's path within the manifest (e.g., `creatives[0].creative_manifest.assets[0].value` or the field name declared by the format) and SHOULD include the format's `allowed_values` array in `error.details.allowed_values` so the buyer agent can re-prompt its LLM with constrained sampling. Recovery: correctable (select a value from `allowed_values` and resubmit).",
+ "CREATIVE_VALUE_NOT_ALLOWED": "A submitted value is outside the closed set the targeted format option declares (`cta_values` on canonical formats, `allowed_values` in text-asset requirements). The code turns on the declared set, not on the container holding the value: it covers a text asset and a card's per-card `cta` checked against `cta_values` alike. Sellers MUST NOT return this code for a slot whose option declares no such set; that slot takes free text. Distinct from `CREATIVE_REJECTED` (generic creative-review failure) by being a closed-set constraint violation that the buyer can resolve mechanically without policy interpretation — the seller has published the complete list of acceptable values on the format, and any value outside that list is rejected by definition. The seller MUST set `error.field` to the offending value's path within the manifest (e.g., `creatives[0].assets.cta.content`, `creatives[0].assets.cards[2].cta` for a card's CTA, or the field name declared by the format) and SHOULD include the declared set in `error.details.accepted_values`, the canonical closed-set key in `core/error.json`, so the buyer agent can re-prompt its LLM with constrained sampling. Also sending `allowed_values` is harmless but not needed; SDKs normalize legacy keys on read. Recovery: correctable (select a value from `accepted_values` and resubmit).",
"CREATIVE_REVISION_CONTENT_MISMATCH": "A sync_creatives item reused a revision_id for different canonical revision content under the same creative_id. Revision identity is scoped to the parent creative and immutable after first acceptance. Sellers MUST evaluate the buyer input before transcoding or normalization, MUST leave the prior creative state unchanged, and SHOULD return details conforming to error-details/creative-revision-content-mismatch.json. Distinct from IDEMPOTENCY_CONFLICT: idempotency_key protects one request replay window, while revision identity protects creative content across requests and retention. Recovery: correctable (resend the exact content previously bound to this revision_id, or mint a new revision_id for the changed content).",
"CREATIVE_MERGE_REJECTED": "One or more sync_creatives merge assignment operations failed seller verification. The seller evaluates every merge in the request, MUST leave every assignment unchanged (assignment operations are atomic), SHOULD set error.field to the first rejected assignment_operations entry, and SHOULD return details conforming to error-details/creative-merge-rejected.json listing each rejected merge with a reason: content_mismatch (the seller judges the copy is a different creative), format_mismatch (the buyer's creative cannot take over the copy's serving object), not_on_package (merged_creative_id is not a seller-assigned creative assigned to the package in this account, reported uniformly for unknown IDs), or not_supported (the seller cannot merge this pairing on this package). A seller that does not declare creative.supports_assignment_merge returns UNSUPPORTED_FEATURE instead. Recovery: correctable (drop or fix the rejected merges and resubmit; use replace to traffic creative_id as a new serving object).",
"CREATIVE_LIVE_UPDATE_UNSUPPORTED": "A sync_creatives item changed a live creative in a way the seller does not apply in place under the same creative_id. A creative is live when its status is approved and it has at least one assignment on a package that is not canceled, whose flight has not ended, and whose media buy is not in a terminal state; a paused package still counts. Returned only by sellers that declare creative.live_updates (others keep returning INVALID_STATE). The seller MUST fail only that item, MUST leave the creative, its review state, and its serving unchanged, SHOULD set error.field to the item, and SHOULD return details conforming to error-details/creative-live-update-unsupported.json naming the refused change kinds and the live packages. A dry_run returns the same refusal. Recovery: correctable (sync the change as a new creative_id, then swap it in on each live package with an assignment_operations replace).",
@@ -337,7 +337,7 @@
},
"CREATIVE_VALUE_NOT_ALLOWED": {
"recovery": "correctable",
- "suggestion": "pick a value from error.details.allowed_values (or re-fetch the format) and resubmit"
+ "suggestion": "pick a value from error.details.accepted_values (or re-fetch the format) and resubmit"
},
"UNSUPPORTED_FEATURE": {
"recovery": "correctable",
diff --git a/static/schemas/source/formats/canonical/image_carousel.json b/static/schemas/source/formats/canonical/image_carousel.json
index a50946fd20e..63921173e6a 100644
--- a/static/schemas/source/formats/canonical/image_carousel.json
+++ b/static/schemas/source/formats/canonical/image_carousel.json
@@ -2,7 +2,7 @@
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "/schemas/formats/canonical/image_carousel.json",
"title": "Canonical Format: Image Carousel",
- "description": "Multi-card swipeable carousel. The buyer ships a `cards` slot whose value is an **array** of [card-asset](/schemas/core/assets/card-asset.json) objects (a single key with an array value — NOT one key per card, NOT dotted/bracketed paths). Each card-asset carries: `asset_type: \"card\"`, `media` (an image or video asset), optional `headline` (text), optional `landing_page_url` (url asset). Per-card structure is the same across all cards; mixed orientations not allowed within a single carousel. Tracking model: per-card impression and engagement pixels + carousel-level engagement (swipe, view-time). Allowed asset types for a card's `media` field: `image` and `video` (Meta-style mixed-media); platforms can narrow to image-only or video-only via `allowed_card_media_asset_types`.\n\nThe manifest's `assets.cards` value is an array of card-asset objects. Example: `\"cards\": [{\"asset_type\": \"card\", \"media\": {\"asset_type\": \"image\", \"url\": \"...\"}, \"headline\": \"Buy now\", \"landing_page_url\": {\"asset_type\": \"url\", \"url_type\": \"clickthrough\", \"url\": \"...\"}}, ...]`. Each card-asset validates against the card schema; per-card platform extensions attach via the card's `platform_extensions` field, never via inline non-canonical keys.",
+ "description": "Multi-card swipeable carousel. The buyer ships a `cards` slot whose value is an **array** of [card-asset](/schemas/core/assets/card-asset.json) objects (a single key with an array value — NOT one key per card, NOT dotted/bracketed paths). Each card-asset carries: `asset_type: \"card\"`, `media` (an image or video asset), optional `headline` (text), optional `landing_page_url` (url asset). Per-card structure is the same across all cards; mixed orientations not allowed within a single carousel. Tracking model: per-card impression and engagement pixels + carousel-level engagement (swipe, view-time). A card's `media` field can carry `image` or `video` (Meta-style mixed media); `allowed_card_media_asset_types` narrows that set and defaults to `['image']`, so a declaration that omits it accepts image cards only.\n\nThe manifest's `assets.cards` value is an array of card-asset objects. Example: `\"cards\": [{\"asset_type\": \"card\", \"media\": {\"asset_type\": \"image\", \"url\": \"...\"}, \"headline\": \"Buy now\", \"landing_page_url\": {\"asset_type\": \"url\", \"url_type\": \"clickthrough\", \"url\": \"...\"}}, ...]`. Each card-asset validates against the card schema; per-card platform extensions attach via the card's `platform_extensions` field, never via inline non-canonical keys.",
"allOf": [{ "$ref": "/schemas/formats/canonical/_base.json" }],
"x-adcp-validation": {
"verifier_constraints": {
@@ -47,11 +47,11 @@
"min_cards": {
"type": "integer",
"minimum": 2,
- "description": "Minimum card count (typical: 2 or 3)."
+ "description": "Minimum card count (typical: 2 or 3). Narrows the `cards` slot's `min` and cannot lower it: the effective minimum is the larger of the two."
},
"max_cards": {
"type": "integer",
- "description": "Maximum card count (typical: 6, 10, or 35 depending on platform)."
+ "description": "Maximum card count (typical: 6, 10, or 35 depending on platform). Narrows the `cards` slot's `max` and cannot raise it, so `max_cards: 35` alone still caps at the default slot's 10. A product that takes 35 cards restates the `cards` slot in `slots` with `max: 35`, and may also set `max_cards: 35`."
},
"allowed_card_media_asset_types": {
"type": "array",
@@ -59,7 +59,8 @@
"type": "string",
"enum": ["image", "video"]
},
- "description": "Asset types each card's `media` field may carry. Default: ['image']. Polymorphic carousels (Meta) allow ['image', 'video']. Renamed from `allowed_card_asset_types` to disambiguate that this constrains the card's media payload, not the card-asset itself (which is always asset_type: \"card\")."
+ "default": ["image"],
+ "description": "Asset types each card's `media` field may carry. A declaration that omits both it and the deprecated `allowed_card_asset_types` alias accepts image cards only; declare `['image', 'video']` for mixed-media carousels. Renamed from `allowed_card_asset_types` to disambiguate that this constrains the card's media payload, not the card-asset itself (which is always asset_type: \"card\")."
},
"allowed_card_asset_types": {
"type": "array",
diff --git a/static/schemas/source/formats/canonical/responsive_creative.json b/static/schemas/source/formats/canonical/responsive_creative.json
index 01ebffd29b3..f47dda191ff 100644
--- a/static/schemas/source/formats/canonical/responsive_creative.json
+++ b/static/schemas/source/formats/canonical/responsive_creative.json
@@ -2,7 +2,7 @@
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "/schemas/formats/canonical/responsive_creative.json",
"title": "Canonical Format: Responsive Creative",
- "description": "Buyer supplies a pool of typed assets (multiple headlines, descriptions, images, videos, logos); the surface algorithmically composes combinations per placement. **Composition is algorithmic** — surface picks combinations and reports per-asset performance breakdowns. Covers Google Responsive Display Ads (RDA), Responsive Search Ads (RSA), Performance Max (PMax), Demand Gen, and Meta Advantage+ creative. Industry term: \"Responsive\" (Google) / \"Advantage+ creative\" (Meta) / \"Dynamic Creative\" (older Meta term). Distinct from `sponsored_placement` (catalog-driven, deterministic) and `agent_placement` (AI-surface composition). The structured `slots` field below enumerates expected canonical asset_group_id slots; per-slot count/length narrowing lives in flat parameters (`headlines_min`, `headline_max_chars`, etc.).",
+ "description": "Buyer supplies a pool of typed assets (multiple headlines, descriptions, images, videos, logos); the surface algorithmically composes combinations per placement. **Composition is algorithmic** — surface picks combinations and reports per-asset performance breakdowns. Covers Google Responsive Display Ads (RDA), Responsive Search Ads (RSA), Performance Max (PMax), Demand Gen, and Meta Advantage+ creative. Industry term: \"Responsive\" (Google) / \"Advantage+ creative\" (Meta) / \"Dynamic Creative\" (older Meta term). Distinct from `sponsored_placement` (catalog-driven, deterministic) and `agent_placement` (AI-surface composition). The structured `slots` field below enumerates expected canonical asset_group_id slots; per-slot count/length narrowing lives in flat parameters (`headlines_min`, `headline_max_chars`, etc.). Count parameters only narrow the matching slot's `min` / `max`: `headlines_max: 20` alone still caps at the default slot's 15, so a product that takes more restates that slot in `slots`.",
"allOf": [{ "$ref": "/schemas/formats/canonical/_base.json" }],
"properties": {
"experimental": {
diff --git a/static/schemas/source/media-buy/list-creative-formats-request.json b/static/schemas/source/media-buy/list-creative-formats-request.json
index 9aba8d909dd..76d95a709ab 100644
--- a/static/schemas/source/media-buy/list-creative-formats-request.json
+++ b/static/schemas/source/media-buy/list-creative-formats-request.json
@@ -24,7 +24,7 @@
},
"asset_types": {
"type": "array",
- "description": "Filter to formats that include these asset types. For third-party tags, search for 'html' or 'javascript'. For published-post reference formats, search for 'published_post'. E.g., ['image', 'text'] returns formats with images and text, ['javascript'] returns formats accepting JavaScript tags.",
+ "description": "Filter to formats that declare every one of these asset types. An asset counts whether it is required or optional, and whether it is declared at the top level or inside a `repeatable_group`. For third-party tags, search for 'html' or 'javascript'. For published-post reference formats, search for 'published_post'. E.g., ['image', 'text'] returns only formats that declare both an image and a text asset, ['image'] also returns a carousel whose only image sits inside its card group, and ['javascript'] returns formats accepting JavaScript tags.",
"items": {
"$ref": "/schemas/enums/asset-content-type.json"
},