Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/image-carousel-cta-values.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"adcontextprotocol": minor
---

`image_carousel` gains an optional `cta_values` parameter (adcp#8960), the same type as `cta_values` on `native_in_feed` and `image`. It governs each card's `cta`: when declared, every card's `cta` must be one of the listed values, and a seller refuses any other value with `CREATIVE_VALUE_NOT_ALLOWED`, with `error.field` pointing at that card's `cta`. A card may still omit `cta`. Without `cta_values`, a card's `cta` takes free text and a seller does not refuse it as `CREATIVE_VALUE_NOT_ALLOWED`. Additive and optional: existing declarations are unchanged. A seller that carried `cta_values` on a carousel option as an undeclared extra parameter can keep the same value; it is now the declared field.
14 changes: 14 additions & 0 deletions docs/creative/channels/carousels.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Use `image_carousel` when one creative contains an ordered set of cards. Card co
"max_cards": 10,
"card_aspect_ratio": "1:1",
"card_image_sizes": [{ "width": 1080, "height": 1080 }],
"cta_values": ["SHOP_NOW", "LEARN_MORE"],
"slots": [
{ "asset_group_id": "cards", "asset_type": "card", "required": true, "min": 3, "max": 10 },
{ "asset_group_id": "landing_page_url", "asset_type": "url", "required": false }
Expand All @@ -31,18 +32,21 @@ Use `image_carousel` when one creative contains an ordered set of cards. Card co
{
"asset_type": "card",
"headline": "Trail Pro",
"cta": "SHOP_NOW",
"media": { "asset_type": "image", "url": "https://cdn.acmeoutdoor.example/trail-pro.jpg" },
"landing_page_url": { "asset_type": "url", "url": "https://acmeoutdoor.example/trail-pro" }
},
{
"asset_type": "card",
"headline": "Summit Pack",
"cta": "SHOP_NOW",
"media": { "asset_type": "image", "url": "https://cdn.acmeoutdoor.example/summit-pack.jpg" },
"landing_page_url": { "asset_type": "url", "url": "https://acmeoutdoor.example/summit-pack" }
},
{
"asset_type": "card",
"headline": "Alpine Shell",
"cta": "LEARN_MORE",
"media": { "asset_type": "image", "url": "https://cdn.acmeoutdoor.example/alpine-shell.jpg" },
"landing_page_url": { "asset_type": "url", "url": "https://acmeoutdoor.example/alpine-shell" }
}
Expand All @@ -60,6 +64,16 @@ Use `image_carousel` when one creative contains an ordered set of cards. Card co
- **Images only.** `card_image_sizes` does not constrain video card media. Video cards follow `card_video_max_file_size_kb` and `card_video_max_duration_ms`.
- **Agreement with `card_aspect_ratio`.** When both are declared, every listed pair MUST have that ratio, within 1% (so 1200×627 matches `1.91:1`). A declaration where they disagree is invalid; neither field wins. When only `card_image_sizes` is declared, the listed sizes imply the accepted card ratios. When only `card_aspect_ratio` is declared, buyers match the ratio and choose the pixel size.

## Per-card CTA

Each card can carry its own `cta` label. `cta_values` (added in AdCP 3.3) makes that label a closed set, the same parameter `native_in_feed` and `image` use for their `cta` slot. On `image_carousel` it governs the `cta` field of every card in the `cards` slot.

- **Closed set when declared.** When the option declares `cta_values`, every card's `cta` MUST be one of the listed values. A seller refuses any other value with `CREATIVE_VALUE_NOT_ALLOWED` and sets `error.field` to that card's `cta`, for example `creatives[0].assets.cards[2].cta` on `sync_creatives`.
- **Optional per card.** `cta_values` does not make `cta` required. A card that omits it is not refused on this parameter.
- **Free text when absent.** Without `cta_values`, a card's `cta` takes free text, and a seller does not refuse it as `CREATIVE_VALUE_NOT_ALLOWED`.

In the example above, the option declares `["SHOP_NOW", "LEARN_MORE"]`, and each card's `cta` is one of those values. A card with `"cta": "BUY_TICKETS"` would be refused.

For catalog-composed collections where the seller reads product rows and renders tiles, use `sponsored_placement` rather than a buyer-authored carousel bundle.

See [Catalogs](/docs/creative/catalogs) and [Canonical formats](/docs/creative/canonical-formats).
4 changes: 4 additions & 0 deletions docs/reference/whats-new-in-3-3.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,10 @@ The `pixel_tracker` `event` enum adds `start`, `first_quartile`, `midpoint`, `th

`image_carousel` gains an optional `card_image_sizes` list of accepted `{ width, height }` pairs for card images, the same shape as `native_in_feed`'s `main_image_sizes`. Card images match a listed pair exactly, video cards are not constrained, and a declaration whose sizes disagree with `card_aspect_ratio` is invalid. See [Carousel ads](/docs/creative/channels/carousels#card-image-size) and [#8911](https://github.com/adcontextprotocol/adcp/issues/8911).

## Carousel per-card CTA values

`image_carousel` gains an optional `cta_values` list, the same type as `cta_values` on `native_in_feed` and `image`. When it is declared, every card's `cta` must be one of the listed values, and a seller refuses any other value with `CREATIVE_VALUE_NOT_ALLOWED`. Without it, a card's `cta` stays free text. See [Carousel ads](/docs/creative/channels/carousels#per-card-cta) and [#8960](https://github.com/adcontextprotocol/adcp/issues/8960).

## Platform object IDs have one name

`sync_event_sources` and `sync_audiences` results now use optional `platform_id` for the object's ID in the seller's ad platform, matching creatives and catalogs. A seller reporting this ID in 3.3 and later 3.x responses MUST emit both `platform_id` and the deprecated `seller_id` alias with identical values, including discovery responses. Buyers MUST prefer `platform_id` and fall back to `seller_id` only when the new field is absent. Continue using `event_source_id` and `audience_id` in AdCP requests. The aliases keep working throughout 3.x; removal is no earlier than 4.0 under the six-month notice policy. Seller identity in `adagents.json` and publisher identifiers is unchanged. See [Deprecations](/docs/reference/migration/3-2-to-3-3#deprecations).
Expand Down
5 changes: 5 additions & 0 deletions static/examples/products/canonical/meta_carousel.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@
"card_video_max_duration_ms": 240000,
"primary_text_max_chars": 125,
"card_headline_max_chars": 40,
"cta_values": [
"SHOP_NOW",
"LEARN_MORE",
"SIGN_UP"
],
"ssl_required": true,
"composition_model": "deterministic",
"slots": [
Expand Down
6 changes: 6 additions & 0 deletions static/schemas/source/formats/canonical/image_carousel.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,12 @@
"minimum": 1,
"description": "Per-card description character limit. Governs the `description` field on each card-asset in the `cards` slot. Distinct from `card_headline_max_chars`: description is longer body copy (typically 100-500 chars); headline is the short label (typically 25-40 chars)."
},
"cta_values": {
"x-added-in": "3.3.0",
"type": "array",
"items": { "type": "string" },
"description": "Permitted values for each card's `cta` (e.g., ['SHOP_NOW', 'LEARN_MORE']). When set, every card's `cta` MUST be one of these values, and a seller refuses any other value with `CREATIVE_VALUE_NOT_ALLOWED`, with `error.field` pointing at that card's `cta`. A card may still omit `cta`. When absent, a card's `cta` takes free text, and a seller does not refuse it as `CREATIVE_VALUE_NOT_ALLOWED`."
},
"ssl_required": {
"type": "boolean"
}
Expand Down
15 changes: 11 additions & 4 deletions tests/canonical-format-conventions.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@
*
* 5. Slot-param consistency: when a format declaration's params include `cta_values` or
* `*_max_chars` for a content type, the canonical's default slots OR the product's
* override slots SHOULD declare a matching slot. Soft-warn (does not fail) until the
* canonicals are updated to add default slots for cta and primary_text.
* override slots SHOULD declare a matching slot. `cta_values` matches the `cta` slot,
* except on image_carousel, where it governs each card's `cta` in the `cards` slot.
* Soft-warn (does not fail) until the canonicals are updated to add default slots for
* cta and primary_text.
*
* Run: node tests/canonical-format-conventions.test.cjs
*/
Expand Down Expand Up @@ -72,6 +74,10 @@ const CANONICAL_DEFAULT_SLOTS = (() => {
return out;
})();

// The slot a canonical's `cta_values` governs, where it is not the `cta` slot.
// image_carousel has no `cta` slot; its `cta_values` governs each card's `cta`.
const CTA_VALUES_SLOT = { image_carousel: 'cards' };

let errors = 0;
let warnings = 0;

Expand Down Expand Up @@ -143,8 +149,9 @@ function checkDeclaration(decl, ctx, file) {
? new Set(overrideSlots.map(s => s.asset_group_id))
: inheritedDefaults;

if (params.cta_values && !effectiveSlotIds.has('cta')) {
warn(file, `${ctx} declares cta_values but effective slots[] has no 'cta' slot (canonical=${decl.format_kind})`);
const ctaSlotId = CTA_VALUES_SLOT[decl.format_kind] || 'cta';
if (params.cta_values && !effectiveSlotIds.has(ctaSlotId)) {
warn(file, `${ctx} declares cta_values but effective slots[] has no '${ctaSlotId}' slot (canonical=${decl.format_kind})`);
}
if (params.primary_text_max_chars && !effectiveSlotIds.has('primary_text')) {
warn(file, `${ctx} declares primary_text_max_chars but effective slots[] has no 'primary_text' slot (canonical=${decl.format_kind})`);
Expand Down
17 changes: 17 additions & 0 deletions tests/canonical-negative-fixtures.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,23 @@ const NEGATIVE_CASES = {
doc: { width: 300, height: 250, pixel_ratios: [] },
},
],
'/schemas/formats/canonical/image_carousel.json': [
{
label: 'per-card cta_values accepts a list of strings (positive control)',
expected: true,
doc: { cta_values: ['SHOP_NOW', 'LEARN_MORE'] },
},
{
label: 'per-card cta_values rejects a bare string',
expected: false,
doc: { cta_values: 'SHOP_NOW' },
},
{
label: 'per-card cta_values rejects a non-string value',
expected: false,
doc: { cta_values: ['SHOP_NOW', 1] },
},
],
'/schemas/core/assets/image-asset.json': [
{
label: 'image asset accepts positive pixel_ratio metadata',
Expand Down
1 change: 1 addition & 0 deletions tests/x-added-in-3-3-sweep.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ const PROPERTIES_3_3 = [
['protocol/get-adcp-capabilities-response.json', ['properties', 'request_signing', 'properties', 'operation_sources']],
['formats/canonical/video_hosted.json', ['properties', 'loudness']],
['formats/canonical/image_carousel.json', ['properties', 'card_image_sizes']],
['formats/canonical/image_carousel.json', ['properties', 'cta_values']],
['core/installment-deadlines.json', ['properties', 'change_cutoff']],
['core/reporting-status-issue.json', ['properties', 'reporting_revision_id']],
['governance/check-governance-request.json', ['properties', 'budget_period_id']],
Expand Down
Loading