Skip to content
Merged
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
25 changes: 25 additions & 0 deletions .changeset/canonical-formats-3.1-followups.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
"adcontextprotocol": minor
---

**Canonical formats 3.1 follow-ups — fixture, vocab, Pinterest disambiguation.**

Closes three of the GA-blocking follow-ups identified in PR #3307 expert review, plus a latent slot-enum bug surfaced by the new fixture:

- **Latent slot `asset_type` enum gap fixed** in `_base.json`. The canonical-formats slot enum was missing `pixel_tracker`, `vast_tracker`, and `daast_tracker` — meaning any product carrying explicit tracker slots (including the `native_in_feed` default slots) failed validation. Added all three to the enum and to the size-mutex if/then "no size semantics" branch. Discovered by the new native_in_feed fixture; would have hit any 3.1 adopter shipping explicit tracker slots.


- **`native_in_feed` reference Product fixture** at `static/examples/products/canonical/taboola_content_recommendation.json`. Realistic Taboola US Content Recommendation product covering all 12 native_in_feed default slots — title, body_text, main_image (1200×627 / 1080×1080), cta with closed enum, advertiser_name, sponsored_label, landing_page_url, display_url, rating, plus impression / viewability / click `pixel_tracker`. CPC pricing, hourly+daily reporting, v1_format_ref points at `native_content`. Brings the canonical fixture suite to 13 (one per canonical, plus generative Veo on video_hosted).

- **Pinterest disambiguation worked example** in `docs/creative/canonical-formats.mdx`. Spells out which Pinterest product routes to which canonical: Promoted Pin → `native_in_feed`, Pinterest Collection → `sponsored_placement` (catalog-keyed), Idea Pin → `image_carousel`, Shopping Pin → `sponsored_placement` (fanout_mode: single_item). The cleave is asset-bundle vs catalog-row composition; same logic applies to Snap Story / Snap Collection, TikTok TopView / TikTok Collection, etc. Closes the routing ambiguity flagged by Pia + Nastassia at GA review.

- **10 new IAB OpenRTB Native 1.2 vocab entries** in `asset-group-vocabulary.json`.
- Five Data Asset additions: `likes` (type 4), `downloads` (type 5), `saleprice` (type 7), `address` (type 9), `secondary_body_text` aliased to `desc2` (type 10).
- Five core-native vocab additions surfaced by product-expert review — the `native_in_feed` canonical's default slots referenced these but the vocab didn't have entries, leaving the flagship fixture authoring against non-canonical IDs: `title` (Title Asset type 1; `headline` is the alias for the singular case, distinct from `headlines` pool used by responsive_creative), `main_image` (Image Asset type 3 main, with `image_main`/`hero_image` aliases), `icon` (Image Asset type 1), `advertiser_name` (the IAB `sponsoredBy` field), `sponsored_label` (renderer disclosure string).
- `phone_number` description annotated with IAB type 8; `body_text` annotated with IAB type 2. `price` description updated to call out the price ↔ saleprice discount-rendering convention.

**Migration doc** updated: 14 reference Product fixtures, dropped the "native_in_feed fixture follows in a subsequent PR" placeholder.

Remaining 3.1 follow-ups tracked separately:
- **SDK codegen (TypeScript + Python)** — multi-week build, the gating dependency for adopter consumption. Schemas shippable today; typed-tagged-union ergonomics arrive with codegen.
- **`native_in_feed` conformance storyboard** — multi-phase YAML to extend `static/compliance/source/protocols/creative/index.yaml` with native sync_creatives + preview coverage.
2 changes: 1 addition & 1 deletion docs/creative/canonical-formats-migration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ Most of AdCP doesn't change. v2 builds on the existing primitives:

**Dual emission during the migration window**: Products MAY carry `format_ids`, `format_options`, or BOTH; at least one is required (the schema enforces this via an `anyOf`, not `oneOf`). The recommended seller pattern is to author once and let the SDK project to both wire shapes via the [v1↔v2 canonical mapping registry](https://adcontextprotocol.org/schemas/v3/registries/v1-canonical-mapping.json), so every buyer reads what it knows. When both shapes are present on a product, the two MUST refer to the same underlying format declaration — the `format_options[i]` must narrow the canonical that `format_ids[i]` resolves to via the registry. SDKs that derive both shapes from one source guarantee this invariant; SDKs that hand-author both MUST treat divergence as a build error and refuse to emit. Buyers prefer `format_options` when both are present; treat `format_ids` as fallback for v1-only buyers. Sellers whose v1 named formats have no clean v2 projection ship `format_ids` only for those products until they add an explicit `canonical` declaration on the v1 format (see "v1 → v2 canonical mapping" below) — the SDK MUST NOT emit `format_options` for non-projectable formats.

For 12 fully-validated reference Product fixtures spanning the 12 canonical formats (native_in_feed fixture follows in a subsequent PR) — Meta Reels (`video_hosted` vertical), IAB MREC (`image` 300×250), NYTimes HTML5 (`html5`), GAM 3P display tag (`display_tag`), Meta Carousel (`image_carousel`), YouTube VAST pre-roll (`video_vast`), podcast 30s host-read (`audio_hosted`), Triton DAAST audio (`audio_daast`), Amazon Sponsored Products (`sponsored_placement`), Google PMax (`responsive_creative`), ChatGPT brand mention (`agent_placement`), Veo 15s generative video (`video_hosted` with `synthesis_nondeterministic` + `provenance_required`) — plus 1 `get_products` response fixture exercising bundled extensions, see `static/examples/products/canonical/` and `static/examples/get_products_responses/canonical/`. The Veo fixture exercises `synthesis_nondeterministic: true` and `provenance_required: true`. Each fixture passes `npm run test:canonical-fixtures`.
For 14 fully-validated reference Product fixtures spanning all 12 canonical formats — Meta Reels (`video_hosted` vertical), IAB MREC (`image` 300×250), NYTimes HTML5 (`html5`), GAM 3P display tag (`display_tag`), Meta Carousel (`image_carousel`), YouTube VAST pre-roll (`video_vast`), podcast 30s host-read (`audio_hosted`), Triton DAAST audio (`audio_daast`), Amazon Sponsored Products (`sponsored_placement`), Taboola Content Recommendation (`native_in_feed`), Google PMax (`responsive_creative`), ChatGPT brand mention (`agent_placement`), Veo 15s generative video (`video_hosted` with `synthesis_nondeterministic` + `provenance_required`) — plus 1 `get_products` response fixture exercising bundled extensions, see `static/examples/products/canonical/` and `static/examples/get_products_responses/canonical/`. The Veo fixture exercises `synthesis_nondeterministic: true` and `provenance_required: true`. Each fixture passes `npm run test:canonical-fixtures`.

## v1 → v2 canonical mapping

Expand Down
15 changes: 15 additions & 0 deletions docs/creative/canonical-formats.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -861,6 +861,21 @@ A retail-media product that accepts a catalog reference plus a brief, and render

`item_production_model: seller_pre_rendered_from_brief` says: for each catalog item, the seller renders ONE creative using the brief plus the catalog item's structured fields (title, image, price). `fanout_mode: per_item` says each item gets its own ad in delivery. Together they capture the multi-output generative pattern (1 brief × N items → N ads) under the existing `sponsored_placement` canonical.

## Worked example — Pinterest: which canonical?

Pinterest is the canonical disambiguation example because a single platform sells inventory under two structurally different shapes. Buyer agents reading these products must route to the matching canonical or the manifest won't render.

| Pinterest product | Canonical | Why |
|---|---|---|
| **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 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.

A buyer agent reading a `format_kind: native_in_feed` product knows to assemble the title/image/body/CTA bundle from its own creative pool. Reading `format_kind: sponsored_placement`, it knows to attach a catalog feed and let the seller compose per-item. The discriminator carries the decision; no per-platform branching needed.

## Validation flow — `validate_input`

Buyers can dry-run a manifest against canonicals and/or specific products without committing to a render. The buyer's manifest below is a v2 manifest (`format_kind: "video_hosted"`); the slot key is the canonical's `asset_group_id` (`video_main`); the asset value carries its `asset_type` discriminator. The buyer asks `validate_input` to check both the canonical contract AND the seller's specific product narrowing in a single round-trip:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
{
"$schema": "/schemas/core/product.json",
"product_id": "taboola_content_recommendation_us",
"name": "Taboola — US Content Recommendation",
"description": "In-feed native content-recommendation placement on Taboola's publisher network (CNN, USA Today, Bloomberg, MSN, and ~2 000 other premium properties). Buyer ships an IAB-shaped native asset bundle (title, thumbnail, body, display_url, optional rating); Taboola's renderer composes the unit to match each publisher's feed look-and-feel. Distinct from `sponsored_placement` (catalog-keyed retail-media — Taboola has no per-item catalog feed) and from `responsive_creative` (algorithmic combinator — Taboola uses a single asset bundle, not an asset pool). Renderer-fired pixel trackers (impression, viewability, click) ship as `pixel_tracker` assets.",
"publisher_properties": [
{
"publisher_domain": "taboola.com",
"selection_type": "all"
}
],
"channels": [
"display"
],
"delivery_type": "non_guaranteed",
"pricing_options": [
{
"pricing_option_id": "cpc_open_auction",
"pricing_model": "cpc",
"currency": "USD",
"floor_price": 0.1
}
],
"reporting_capabilities": {
"available_reporting_frequencies": [
"hourly",
"daily"
],
"expected_delay_minutes": 30,
"timezone": "America/New_York",
"supports_webhooks": true,
"available_metrics": [
"impressions",
"clicks",
"spend",
"ctr",
"viewability",
"conversions"
],
"date_range_support": "date_range"
},
"format_options": [
{
"format_kind": "native_in_feed",
"display_name": "Taboola Sponsored Content — Premium",
"applies_to_channels": ["display"],
"params": {
"title_max_chars": 80,
"body_text_max_chars": 140,
"cta_max_chars": 25,
"cta_values": ["LEARN_MORE", "READ_MORE", "SHOP_NOW", "SIGN_UP", "DOWNLOAD"],
"main_image_sizes": [
{ "width": 1200, "height": 627 },
{ "width": 1080, "height": 1080 }
],
"max_image_file_size_kb": 500,
"image_formats": ["jpg", "jpeg", "png", "webp"],
"ssl_required": true,
"asset_source": "buyer_uploaded",
"buyer_asset_acceptance": "accepted",
"composition_model": "deterministic",
"slots": [
{ "asset_group_id": "title", "asset_type": "text", "required": true, "max_chars": 80 },
{ "asset_group_id": "body_text", "asset_type": "text", "required": false, "max_chars": 140 },
{ "asset_group_id": "main_image", "asset_type": "image", "required": true },
{ "asset_group_id": "cta", "asset_type": "text", "required": false, "max_chars": 25 },
{ "asset_group_id": "advertiser_name", "asset_type": "text", "required": true },
{ "asset_group_id": "sponsored_label", "asset_type": "text", "required": false },
{ "asset_group_id": "landing_page_url", "asset_type": "url", "required": true },
{ "asset_group_id": "display_url", "asset_type": "text", "required": false },
{ "asset_group_id": "rating", "asset_type": "text", "required": false },
{ "asset_group_id": "impression_tracker", "asset_type": "pixel_tracker", "required": false },
{ "asset_group_id": "viewability_tracker", "asset_type": "pixel_tracker", "required": false },
{ "asset_group_id": "click_tracker", "asset_type": "pixel_tracker", "required": false }
]
},
"v1_format_ref": [
{ "agent_url": "https://creative.adcontextprotocol.org", "id": "native_content" }
]
}
]
}
65 changes: 61 additions & 4 deletions static/schemas/source/core/asset-group-vocabulary.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,37 @@
"body_text": {
"description": "Longer free-text body content.",
"asset_type": "text",
"typical_use": "Body copy on Reddit, Amazon DSP, LinkedIn formats — distinct from short headlines."
"typical_use": "Body copy on Reddit, Amazon DSP, LinkedIn formats — distinct from short headlines. IAB OpenRTB Native 1.2 Data Asset type 2."
},
"title": {
"description": "Single headline string rendered verbatim on the creative (no pool / no per-render selection).",
"asset_type": "text",
"typical_use": "Native in-feed units, content-recommendation widgets, single-creative display ads where exactly one headline is rendered. IAB OpenRTB Native 1.2 Title Asset (type 1). Distinct from `headlines` (pool of variants for algorithmic surfaces that pick combinations) — `title` is one fixed string the renderer always uses.",
"aliases": ["headline"]
},
"main_image": {
"description": "Primary image asset rendered as the visual focal point of the creative.",
"asset_type": "image",
"typical_use": "Native in-feed units, content-recommendation widgets (the large thumbnail above the headline), single-image display creative. IAB OpenRTB Native 1.2 Image Asset type 3 (Main). Common sizes: 1200×627 (1.91:1), 1080×1080 (1:1), 1080×1350 (4:5).",
"aliases": ["image_main", "hero_image"]
},
"icon": {
"description": "Small square brand or app icon rendered alongside the main image.",
"asset_type": "image",
"typical_use": "App-install native units (app icon), publisher native cards (brand icon), content-recommendation widgets (source publisher icon). IAB OpenRTB Native 1.2 Image Asset type 1 (Icon). Typical size: 80×80 or 100×100, square aspect ratio.",
"aliases": ["icon_image", "brand_icon"]
},
"advertiser_name": {
"description": "Plain-text name of the advertiser running the creative (rendered with the unit).",
"asset_type": "text",
"typical_use": "Native in-feed units, content-recommendation widgets, sponsored-content cards. Maps to the IAB OpenRTB Native 1.2 `sponsoredBy` field — the renderer pairs this with `sponsored_label` (e.g., '<sponsored_label> by <advertiser_name>'). Required on most native surfaces for transparency.",
"aliases": ["sponsor_name", "brand_name_native"]
},
"sponsored_label": {
"description": "Disclosure label that identifies the unit as sponsored / promoted / advertising content (e.g., \"Sponsored\", \"Promoted\", \"Ad\").",
"asset_type": "text",
"typical_use": "Native in-feed units, content-recommendation widgets. The renderer pairs this with `advertiser_name` to surface the IAB-required disclosure. Default value typically \"Sponsored\" or \"Ad\"; sellers may localize.",
"aliases": ["disclosure_label", "ad_label"]
},
"cards": {
"description": "Per-item carousel card array.",
Expand All @@ -103,9 +133,9 @@
"aliases": ["cta_text", "call_to_action", "action_text", "button_text"]
},
"price": {
"description": "Product price slot rendered on the creative.",
"description": "Product price slot rendered on the creative — the regular (non-discounted) price.",
"asset_type": "text",
"typical_use": "Pinterest shopping pins, Amazon Sponsored Brand product cards, retail-media catalog rendering, IAB OpenRTB Native 1.2 Data Asset type 6. Often pulled from a catalog field via `field_bindings`."
"typical_use": "Pinterest shopping pins, Amazon Sponsored Brand product cards, retail-media catalog rendering. IAB OpenRTB Native 1.2 Data Asset type 6. Often pulled from a catalog field via `field_bindings`. Pair with `saleprice` (type 7) to render the discount pattern — by IAB convention `price` is the original (struck-through) and `saleprice` is the discounted amount."
},
"display_url": {
"description": "Visible URL or domain string shown above/below the headline on native units (does not need to match the click destination).",
Expand All @@ -127,7 +157,34 @@
"phone_number": {
"description": "Phone number for click-to-call placements (E.164 preferred).",
"asset_type": "text",
"typical_use": "Google call-only ads, Bing call extensions, click-to-call retail/local placements."
"typical_use": "Google call-only ads, Bing call extensions, click-to-call retail/local placements. IAB OpenRTB Native 1.2 Data Asset type 8."
},
"likes": {
"description": "Social-proof like / favorite count rendered on the creative (e.g., \"12.4K likes\").",
"asset_type": "text",
"typical_use": "Social-feed native units, app-install native showing aggregate engagement. IAB OpenRTB Native 1.2 Data Asset type 4."
},
"downloads": {
"description": "Download count rendered on the creative (e.g., \"1M+ downloads\").",
"asset_type": "text",
"typical_use": "App-install native units (Google Play, App Store ad placements, in-feed app promos). IAB OpenRTB Native 1.2 Data Asset type 5."
},
"saleprice": {
"description": "Sale / discounted price rendered alongside the original `price` slot (e.g., struck-through original + sale price).",
"asset_type": "text",
"typical_use": "Retail / e-commerce native showing a discount or promotion. IAB OpenRTB Native 1.2 Data Asset type 7. Pair with `price` (regular price) and optionally `promo_code`.",
"aliases": ["sale_price", "discount_price"]
},
"address": {
"description": "Physical address rendered on the creative (e.g., for local-business or click-to-visit placements).",
"asset_type": "text",
"typical_use": "Local-business native, retail-store ads, click-to-directions placements. IAB OpenRTB Native 1.2 Data Asset type 9."
},
"secondary_body_text": {
"description": "Secondary descriptive copy rendered below the primary `body_text` (extended description).",
"asset_type": "text",
"typical_use": "Long-form native units where the renderer accepts a second descriptive paragraph (Yahoo Native long-form, content-recommendation extended cards). IAB OpenRTB Native 1.2 Data Asset type 10 (\"desc2\").",
"aliases": ["desc2", "body_text_2", "secondary_description"]
},
"promo_code": {
"description": "Promotional / offer code rendered on the creative.",
Expand Down
Loading
Loading