diff --git a/.changeset/v2-phase1-vocab-scenes-zip.md b/.changeset/v2-phase1-vocab-scenes-zip.md
new file mode 100644
index 0000000000..675cdd892d
--- /dev/null
+++ b/.changeset/v2-phase1-vocab-scenes-zip.md
@@ -0,0 +1,30 @@
+---
+"adcontextprotocol": minor
+---
+
+feat(creative): v2 Phase 1 — asset_group_id vocabulary registry, `scenes` schema, `zip` asset type, video/audio mdx asset_type fixes
+
+First PR implementing the v2 creative formats RFC (#3305). Backwards-compatible additions only — no v1 producers are affected. Minor bump because this introduces new schemas (`asset-group-vocabulary.json`, `scenes.json`, `zip-asset.json`), which are additive features rather than bug fixes.
+
+**New schemas:**
+
+- `static/schemas/source/core/asset-group-vocabulary.json` — canonical registry of `asset_group_id` values (the seven existing catalog vocab entries plus 12 audit-driven additions: `video_vertical`, `video_horizontal`, `audio`, `companion_image`, `companion_banner`, `brand_name`, `body_text`, `cards`, `landing_page_url`, `privacy_policy_url`, `youtube_video_id`, `pin_id`). Includes the `landing_page_url` aliases canonicalizing six different field names today (`click_url`, `link`, `final_url`, `link_url`, `click_through_url`, `landing_url`). Non-canonical IDs remain valid for platform-specific extensions; validators MAY soft-warn on non-canonical usage.
+
+- `static/schemas/source/creative/scenes.json` — typed scene-by-scene structure used as input to `build_creative` for generative video platforms. Each scene has `order`, `duration_ms`, `description`, optional `vo` and `caption`. Renamed from "storyboard" to avoid collision with the testing-harness storyboard concept; description disambiguates from `reference-asset.json` `purpose: "storyboard"` (which describes a reference asset, not a structured plan).
+
+- `static/schemas/source/core/assets/zip-asset.json` — new asset type for bundled creatives delivered as zip archives (HTML5 banners with index.html + CSS + JS + images, MRAID-compatible interactive ads). Carries `url`, optional `max_file_size_kb`, `entry_point`, `allowed_inner_extensions`, `backup_image_url`, and SHA-256 `digest` for integrity. Distinct from inline HTML (`html` asset) and from third-party tag URLs (`url` asset with appropriate `url_type`).
+
+**Registry updates:**
+
+- `static/schemas/source/creative/asset-types/index.json` — added `zip` entry pointing at the new schema
+- `static/schemas/source/core/format.json` — added `IndividualZipAsset` and `GroupZipAsset` branches to the format declaration oneOf
+- `static/schemas/source/core/offering-asset-group.json`, `creative-manifest.json`, `creative-asset.json`, `creative/list-creatives-response.json` — added `zip-asset.json` to manifest/asset-group oneOf branches so manifests can carry zip assets
+
+**Doc fixes:**
+
+- `docs/creative/channels/video.mdx` — corrected three format-definition examples that used `asset_type: "url"` + `asset_role: "vast_url"` / `"vpaid_url"`, contradicting the schema-correct `asset_type: "vast"` used elsewhere in the same file. Updated VPAID examples to use `asset_type: "vast"` with `vpaid_enabled: true` in requirements.
+- `docs/creative/channels/audio.mdx:200` — same bug pattern: `asset_type: "url"` for what should be a VAST audio tag. Corrected to `asset_type: "vast"` with `delivery_type: "url"`; renamed slot key from `vast_url` to `vast_tag` for clarity.
+
+**Why minor (not patch):** new schemas and a new asset type are additive features — patch is reserved for bug fixes only. **Why not major:** no breaking changes; v1 producers and consumers continue to work unchanged. The new `zip` asset type is purely additive — receivers that don't recognize it ignore it via standard discriminator-mismatch handling.
+
+Tracks #3305 (v2 RFC). Phase 1 lays foundational primitives; subsequent phases build the canonical format catalog, `ProductFormatDeclaration` schema, and tools on top of these primitives.
diff --git a/.changeset/v2-review-feedback-format-options.md b/.changeset/v2-review-feedback-format-options.md
new file mode 100644
index 0000000000..0bb87c4a02
--- /dev/null
+++ b/.changeset/v2-review-feedback-format-options.md
@@ -0,0 +1,84 @@
+---
+"adcontextprotocol": minor
+---
+
+feat(creative): v2 review-feedback round — `format_options` array, canonical `status`, hosting paragraph, third-party creative-agent worked example
+
+Addresses external review feedback on RFC #3305 / PR #3307 before the 3.1.0 beta cycle opens.
+
+**Schema changes:**
+
+- **`product.format` → `product.format_options: [ProductFormatDeclaration]` (array).** Restores v1 `format_ids` cardinality on the v2 path. The 90% case is a single-element array (one canonical narrowed for the product); multi-element arrays declare that the product accepts any of the listed format options (e.g., a placement that takes EITHER Flashtalking-served `html5` OR an internal `display_tag`; a video product that accepts a hosted upload OR a VAST tag). Buyers pick which option they're shipping at `sync_creatives` time by aligning their manifest to the matching declaration's `format_kind`. Mutually exclusive with `format_ids` via the existing `oneOf`.
+- **`status: "stable" | "preview" | "deprecated"` field on canonical format `_base.json`.** Default `stable`. Lets the spec ship not-yet-fully-settled canonicals (`agent_placement` and `responsive_creative` in 3.1) with explicit notice that their parameter shape and tracking model MAY break in 3.2 once 2-3 adopters have built against them. The other 9 canonicals are anchored in stable IAB / platform standards and stay `stable`.
+
+**Doc changes:**
+
+- **Worked example: third-party creative agent path (Flashtalking + NYTimes display).** Adds a multi-actor walkthrough alongside the existing single-actor host-read example: buyer reads NYTimes capabilities → sees declared `creative_agents` and the resolved `supported_formats` projection → calls Flashtalking's `build_creative` → ships the manifest to NYTimes via `sync_creatives`. The seller validates against the canonical, NOT against Flashtalking's narrowing — that's the creative agent's contract with the buyer. Closes a gap where the r4 collapse of `build_capability` into format slots wasn't documented for the third-party-creative-agent flow.
+- **Platform extension hosting expectations.** Adds a paragraph to the "Platform extensions — distribution" section documenting hosting role (publisher's subdomain hosts the canonical artifact), caching expectations (`Cache-Control: public, max-age=31536000, immutable` enabled by digest pinning), availability targets (≥99.9% / 30 days), and graceful-degradation semantics on 404 (treat extension as unavailable; don't fail the buy). AAO mirror is best-effort fallback, not normative.
+- **Adoption-driven `format_ids` removal trigger.** v1 `format_ids` is removed in 5.0 — but the trigger is adoption-driven, not date-driven. AAO computes the ratio of registered sales agents declaring `format_options` from cached `get_products` capabilities responses. When `format_options` adoption crosses 80% and stays there for 30 consecutive days, the 5.0 cut sequence opens. Until then, both shapes remain valid.
+
+**Schema housekeeping:**
+
+- Added a description note on `validate-input-response.json` documenting the intent behind the 3-schema split (`request` / `response` / `result`): the `Result` type is split for planned reuse by adjacent async-validation surfaces (per-batch result envelopes on `build_creative` async paths, asynchronous canonical-against-product validation in `sync_creatives`). Producers that only need the synchronous batch shape today MAY treat the split as YAGNI; the schema reuse anchors the violation/retry shape so downstream surfaces don't drift.
+- Updated all 12 v2 reference fixtures (`static/examples/products/v2/*.json`) plus the `meta_with_bundled_extensions.json` get_products response fixture to use the new `format_options` array shape. All 13 fixtures still validate via `npm run test:v2-fixtures`.
+- Updated `tests/schema-validation.test.cjs` core-required-fields rule to assert `format_options` (not `format`) on the v2 oneOf branch.
+
+**Why minor:** structural rename of `product.format` → `product.format_options` is technically breaking for anyone who built against the v2 path during the preview window, but the v2 path was only landed in this PR (#3307) and is not yet released — no published 3.x version carries `format`. The shipping shape is `format_options`. Anyone building against the preview branch should re-pull. The other changes are additive.
+
+**Red-team round (must-fix + should-fix + nits)** — substantive cleanup against three parallel red teams (protocol-expert, adtech-product-expert, docs-expert):
+
+Schema fixes:
+- Manifest v2 path. `creative-manifest.json` and `creative-asset.json` now carry `oneOf(format_id v1 path | format_kind v2 path)` with explicit `not` on each branch. New `/schemas/core/canonical-format-kind.json` enum backs the v2 path. Optional `capability_id` field disambiguates when a product's `format_options` carries multiple declarations sharing the same `format_kind`. Without this, v2 products had no v2 manifest counterpart.
+- `ProductFormatDeclaration` grows `capability_id` (stable identifier for routing) and `applies_to_channels` (subset of the product's channels this declaration applies to — lets a multi-channel product carry channel-specific format_options).
+- `audio_source` enum widened to match `image_source` / `video_source` (now 5-value: `buyer_uploaded | publisher_host_recorded | seller_pre_rendered_from_brief | seller_human_designed | agent_synthesized`). TTS-from-brief and studio-produced audio now expressible.
+- `product.json` oneOf branches got explicit `not: required: [other]` to truly exclude both `format_ids` AND `format_options` being present.
+- Stale "inputs" references in `get-adcp-capabilities-response.json supported_formats` descriptions replaced (the concept was dropped in r4 — collapsed into slots).
+- `image_carousel` got a default slots declaration (`cards` slot, asset_type: object) plus a normative `card_shape` parameter documenting the per-card object structure (media + headline + landing_page_url). `assets.cards` is now the unambiguous array-under-one-key contract; per-card key conventions (card_0_headline, cards.0.headline) are forbidden.
+- Slots inline default added to all 11 canonicals (previously only on 3). SDK codegen now produces typed slot lists for every canonical.
+- `synthesis_nondeterministic` × `*_source` compatibility documented in `_base.json` (incompatible with `buyer_uploaded` and `publisher_host_recorded`).
+- `platform-extension-ref` digest collision behavior documented (within a single response, divergent digests for the same uri MUST fail closed; across responses, divergence is normal).
+- `status: preview` deprecation pathway: `since_version` + `migration_target_version` siblings on canonical `_base.json`, plus a stabilization rubric ("preview → stable when 2 adopters ship + 90 days no breaking change").
+- Veo fixture used `audio_source` / `buyer_audio_acceptance` on a `video_hosted` format. Renamed to `video_source` / `buyer_video_acceptance`.
+
+Doc additions:
+- v2-overview.mdx glossary covering ~25 v2 terms.
+- Asset group vocabulary table (was previously only in the JSON schema).
+- "Two axes" section refined to show the unified 5-value source enum.
+- Tracker assembly under seller-rendered sources documented (macro-substituted vs sync-creatives tracker block).
+- "Channels not yet canonicalized" section (native, linear/addressable TV, OOH/DOOH, audio DAI, in-game, live streaming).
+- Worked examples added for: generative DSP (universalads-class, `image_source: seller_pre_rendered_from_brief`), multi-format product (Flashtalking html5 OR internal display_tag), `sponsored_placement` with `item_production_model` (1 brief × N items → N creatives).
+- Hosting reframed as two paths: open-ecosystem (publisher-hosted) vs closed-platform (AAO-mirror-translated, normative for walled gardens).
+- `validate_input` "when to use" decision rule + comparison table with `build_creative` and `sync_creatives`.
+- Discovery + validation scaling guidance (client-side filter + multi-target validate_input).
+- Generative-DSP narrative weight tuned (demoted to forward-looking subsection — universalads/Pencil/AdCreative.ai are real but small share of 2026 spend).
+- Creative-agent business-model paragraph clarifying that v2 disaggregation is conceptual; creative agents continue to host their produced creatives' bytes and instrument tracking via platform extensions.
+- Preview canonicals stabilization rubric (`responsive_creative` and `agent_placement` re-evaluated for stable status by 3.3 if adopters land in 3.1-3.2).
+- Phase 4 SDK codegen blocker callout in the status banner.
+- Phase 3 fixture count reconciled (12 product fixtures + 1 response fixture).
+
+Migration doc additions:
+- v1 deprecation calendar floor + ceiling (2027-Q4 floor, 2029-Q1 ceiling) bounding the adoption-driven trigger.
+- Adoption-trigger metric definition (denominator + numerator + AAO publishing surface).
+- `creative_id` stability invariant across v1 ↔ v2.
+- "What v2 gives you that OpenRTB doesn't" subsection (canonical-as-contract decoupling, runtime discovery, declared production source, canonical tracking model).
+
+Cross-doc references:
+- v2 preview banners on `formats.mdx`, `key-concepts.mdx`, `generative-creative.mdx`, `specification.mdx`, `implementing-creative-agents.mdx`, `asset-types.mdx` so readers landing from search have a signpost.
+
+`asset-types.mdx` updated for v2 with `asset_group_id` framing, full v2 asset_type table including `brief` / `catalog` / `zip` / `markdown` / `webhook` / `object`.
+
+**Production-source taxonomy (universalads / generative-DSP gap):**
+
+The audio_hosted canonical previously handled "who renders" via `audio_source` but with a narrower 3-value enum than image/video. The asymmetry forced generative-DSP-shaped adopters to either fudge `composition_model` or invent platform extensions to express what's actually a common pattern.
+
+This change adds:
+
+- `image_source` on `image` — `buyer_uploaded | seller_pre_rendered_from_brief | seller_human_designed | agent_synthesized` (default `buyer_uploaded`). Plus `buyer_image_acceptance: accepted | rejected`.
+- `video_source` on `video_hosted` — same enum and pattern as `image_source`. Plus `buyer_video_acceptance: accepted | rejected`.
+- `item_production_model` on `sponsored_placement` — same enum, applied per catalog item. Captures the multi-output generative pattern (1 brief × N catalog items → N rendered creatives) under the existing `sponsored_placement` canonical without requiring a 12th canonical.
+
+These are informational fields, not the binding contract — the format's `slots` declaration is the contract. The `*_source` fields let buyers pick products whose production model fits their workflow (in-house pre-rendered vs upstream creative agent vs seller-driven generative).
+
+The v2-overview.mdx narrative now explicitly differentiates the two orthogonal axes — `composition_model` (how the surface composes per-impression: deterministic vs algorithmic) and per-canonical production source (who renders, and when). Conflating them was the gap that left generative DSPs without a clean expression in v2.
+
+Tracks #3305 (v2 RFC) and #3307 (preview branch).
diff --git a/docs/creative/asset-types.mdx b/docs/creative/asset-types.mdx
index bfb59fe20b..c8b3b25655 100644
--- a/docs/creative/asset-types.mdx
+++ b/docs/creative/asset-types.mdx
@@ -4,11 +4,39 @@ description: "AdCP asset types define standardized properties for images, video,
"og:title": "AdCP — Asset Types"
---
+> **v2 readers**: this page describes asset types and their payload shapes — the same in v1 and v2. For how assets map to v2 format slots via `asset_group_id` (canonical vocabulary), see [v2-overview](/docs/creative/v2-overview) and [v2-migration](/docs/creative/v2-migration). v1 uses `asset_id` + `asset_role`; v2 uses `asset_group_id` referencing the canonical vocabulary registry. Both paths use the same asset payload schemas — only the slot-key vocabulary differs.
Creative formats in AdCP use standardized asset types with well-defined properties. Assets are the discrete, typed building blocks used by formats to define requirements and by manifests to supply concrete values.
Standardizing asset types ensures consistency across formats and makes requirements easier for buyers and systems to understand.
+## Asset types in v2
+
+The full set of asset types valid in a v2 format `slots` declaration's `asset_type` field:
+
+| asset_type | What it carries | Where defined |
+|---|---|---|
+| `image` | Image file (jpg/png/gif/webp/svg) | `/schemas/core/assets/image-asset.json` |
+| `video` | Video file (mp4/webm/mov) | `/schemas/core/assets/video-asset.json` |
+| `audio` | Audio file (mp3/aac/wav) | `/schemas/core/assets/audio-asset.json` |
+| `text` | Plain text (headline, body, script) | `/schemas/core/assets/text-asset.json` |
+| `markdown` | Markdown text | `/schemas/core/assets/markdown-asset.json` |
+| `url` | URL with `url_type` discriminator (clickthrough, tracker_pixel, third-party tag) | `/schemas/core/assets/url-asset.json` |
+| `html` | Inline HTML | `/schemas/core/assets/html-asset.json` |
+| `css` | CSS rules | `/schemas/core/assets/css-asset.json` |
+| `javascript` | JavaScript | `/schemas/core/assets/javascript-asset.json` |
+| `vast` | VAST tag (URL or inline XML), VAST 2.x-4.x | `/schemas/core/assets/vast-asset.json` |
+| `daast` | DAAST tag (URL or inline XML), 1.0-1.1 | `/schemas/core/assets/daast-asset.json` |
+| `webhook` | Webhook URL for async creative production | `/schemas/core/assets/webhook-asset.json` |
+| `brief` | Free-text creative brief (input to generative production) | `/schemas/core/assets/brief-asset.json` |
+| `catalog` | Reference to a synced catalog (sponsored_placement) | `/schemas/core/assets/catalog-asset.json` |
+| `zip` | Zip archive (HTML5 banner bundle) | `/schemas/core/assets/zip-asset.json` |
+| `vast_tracker` | Single VAST `Tracking` event URL (decomposed) | `/schemas/core/assets/vast-tracker-asset.json` |
+| `daast_tracker` | Single DAAST `Tracking` event URL (decomposed) | `/schemas/core/assets/daast-tracker-asset.json` |
+| `object` | Structured object (image_carousel `cards`, scenes for generative video) — sub-shape declared by the canonical | (no standalone schema; per-canonical sub-shape) |
+
+In a v2 manifest's `assets` map, the **slot key** is the canonical's `asset_group_id` (e.g., `image_main`, `video_main`, `script`, `cards`, `landing_page_url`) and the **value** carries the matching asset payload with its `asset_type` discriminator. The format declaration's `slots[].asset_type` tells the validator which payload schema applies.
+
## Important: Payload vs Requirements
For payload schemas (the structure of the actual asset data supplied in creative manifests), see:
diff --git a/docs/creative/channels/audio.mdx b/docs/creative/channels/audio.mdx
index 4bb3f34eda..ee6a30a7c8 100644
--- a/docs/creative/channels/audio.mdx
+++ b/docs/creative/channels/audio.mdx
@@ -197,9 +197,9 @@ Multi-segment audio assembled dynamically:
"id": "audio_30s_vast"
},
"assets": {
- "vast_url": {
- "asset_type": "url",
- "url_type": "tracker",
+ "vast_tag": {
+ "asset_type": "vast",
+ "delivery_type": "url",
"url": "https://ad-server.brand.com/audio-vast?campaign={MEDIA_BUY_ID}&cb={CACHEBUSTER}"
}
}
diff --git a/docs/creative/channels/video.mdx b/docs/creative/channels/video.mdx
index d9af5b392d..a39730fb59 100644
--- a/docs/creative/channels/video.mdx
+++ b/docs/creative/channels/video.mdx
@@ -419,12 +419,11 @@ For third-party ad servers:
"assets": [
{
"asset_id": "vast_tag",
- "asset_type": "url",
- "asset_role": "vast_url",
+ "asset_type": "vast",
"item_type": "individual",
"required": true,
"requirements": {
- "vast_version": ["3.0", "4.0", "4.1", "4.2"]
+ "vast_version": "4.2"
}
}
]
@@ -445,13 +444,13 @@ For third-party ad servers:
"assets": [
{
"asset_id": "vpaid_tag",
- "asset_type": "url",
- "asset_role": "vpaid_url",
+ "asset_type": "vast",
"item_type": "individual",
"required": true,
"requirements": {
"vpaid_version": ["2.0"],
- "api_framework": "VPAID"
+ "api_framework": "VPAID",
+ "vpaid_enabled": true
}
}
]
@@ -762,13 +761,13 @@ VPAID (Video Player Ad-Serving Interface Definition) enables interactive video a
"assets": [
{
"asset_id": "vpaid_tag",
- "asset_type": "url",
- "asset_role": "vpaid_url",
+ "asset_type": "vast",
"item_type": "individual",
"required": true,
"requirements": {
"vpaid_version": ["2.0"],
- "api_framework": "VPAID"
+ "api_framework": "VPAID",
+ "vpaid_enabled": true
}
}
]
diff --git a/docs/creative/formats.mdx b/docs/creative/formats.mdx
index 09612997f1..590f295d7c 100644
--- a/docs/creative/formats.mdx
+++ b/docs/creative/formats.mdx
@@ -4,6 +4,7 @@ description: "Creative formats in AdCP define asset requirements, technical cons
"og:title": "AdCP — Creative Formats"
---
+> **v2 preview**: starting in 3.1, formats are declared inline on products via `format_options` (an array of `ProductFormatDeclaration`s narrowing one of 11 canonical formats). This page describes the v1 format-registry model, which remains a first-class path through 4.x. For the v2 model, see [v2-overview](/docs/creative/v2-overview) and [v2-migration](/docs/creative/v2-migration).
Creative formats define the structural and technical requirements used to instantiate advertising creatives. A format specifies:
- The asset types required (video, image, text, audio, etc.) via the `assets` array
diff --git a/docs/creative/generative-creative.mdx b/docs/creative/generative-creative.mdx
index a2596e2914..6d2d728306 100644
--- a/docs/creative/generative-creative.mdx
+++ b/docs/creative/generative-creative.mdx
@@ -5,6 +5,8 @@ description: "Generative creative in AdCP uses AI to produce ad assets from a br
"og:image": /images/walkthrough/diagram-generative-tiers.png
---
+> **v2 preview**: in v2, the "generative" category dissolves at the protocol level — production mechanism (generative AI, host recording, transcoding, asset rendering) is invisible to the buyer. Production source is declared per-canonical via `*_source` enums (`audio_source`, `image_source`, `video_source`, `item_production_model`); `synthesis_nondeterministic: true` flags Veo / Sora / Runway-class flows that need post-synthesis QA-loop semantics. See [v2-overview](/docs/creative/v2-overview) §"Two axes" for the v2 model.
+
The Creative Protocol enables AI-powered creative generation and asset management for advertising campaigns. This guide will help you create your first creative in 5 minutes.
diff --git a/docs/creative/implementing-creative-agents.mdx b/docs/creative/implementing-creative-agents.mdx
index 8fe90ac72f..7d7da5bf4e 100644
--- a/docs/creative/implementing-creative-agents.mdx
+++ b/docs/creative/implementing-creative-agents.mdx
@@ -4,6 +4,7 @@ description: "How to build an AdCP creative agent that defines formats, validate
"og:title": "AdCP — Implementing Creative Agents"
---
+> **v2 preview**: in v2, creative agents declare what they can produce via `creative.supported_formats` on `get_adcp_capabilities` (replacing the v1 `list_creative_formats` overload for creative agents). Each entry uses the same `ProductFormatDeclaration` shape as a product's inline `format_options[i]`. See [v2-overview](/docs/creative/v2-overview).
This guide explains how to implement a creative agent that defines and manages creative formats.
diff --git a/docs/creative/key-concepts.mdx b/docs/creative/key-concepts.mdx
index 40c5071d6c..c9bf8df3c2 100644
--- a/docs/creative/key-concepts.mdx
+++ b/docs/creative/key-concepts.mdx
@@ -4,6 +4,7 @@ description: "Assets, formats, manifests, and creative agents are the four build
"og:title": "AdCP — Creative key concepts"
---
+> **v2 preview**: this page describes the v1 model. For the v2 architectural shift (canonical formats, inline `format_options` on products, production-source axis, `validate_input` primitive), see [v2-overview](/docs/creative/v2-overview).
One upload, every format. This guide explains how creatives work in AdCP, from defining format requirements to assembling and delivering ads.
diff --git a/docs/creative/specification.mdx b/docs/creative/specification.mdx
index 0b3c09ecb5..cd8246e362 100644
--- a/docs/creative/specification.mdx
+++ b/docs/creative/specification.mdx
@@ -9,6 +9,8 @@ sidebarTitle: Specification
**AdCP 3.0 Proposal** - This specification is under development for AdCP 3.0. Feedback welcome via [GitHub Discussions](https://github.com/adcontextprotocol/adcp/discussions).
+> **v2 preview**: this page describes the v1 specification model. For the v2 model (canonical formats, inline `format_options` on products, `validate_input` primitive), see [v2-overview](/docs/creative/v2-overview) and [v2-migration](/docs/creative/v2-migration).
+
**Status**: Request for Comments
**Last Updated**: March 2026
diff --git a/docs/creative/v2-migration.mdx b/docs/creative/v2-migration.mdx
new file mode 100644
index 0000000000..81f37c72b5
--- /dev/null
+++ b/docs/creative/v2-migration.mdx
@@ -0,0 +1,512 @@
+---
+title: Creative Formats v1 → v2 Migration
+description: "Concrete migration paths for sellers, creative agents, buyers, and tooling moving from v1 named formats to v2 product-bound declarations."
+"og:title": "AdCP — Creative Formats v1 → v2 Migration"
+testable: true
+---
+
+# Migration: v1 → v2 Creative Formats
+
+This guide walks through the shift from v1 named formats (`format_id` as `{ agent_url, id }` referencing a separately-defined format file) to v2 product-bound declarations introduced by [RFC #3305](https://github.com/adcontextprotocol/adcp/issues/3305). v1 named formats remain a first-class path through 4.x; v2 is the new path, opt-in indefinitely.
+
+For the architecture, read [`v2-overview`](/docs/creative/v2-overview) first. This page is just the migration mechanics.
+
+## What stays unchanged
+
+Most of AdCP doesn't change. v2 builds on the existing primitives:
+
+- All asset primitive schemas (`image-asset.json`, `video-asset.json`, `audio-asset.json`, `vast-asset.json`, `daast-asset.json`) — unchanged
+- Catalog and offering schemas — unchanged
+- Manifest envelope shape (`creative-manifest.json` keyed by `assets` map) — unchanged
+- Response envelopes, error schemas, common types — unchanged
+- v1 named formats (`format.json` with compound `format_id`) — still supported through 4.x
+- v1 `list_creative_formats` tool — deprecated but functional through 4.x; removed at 5.0
+- All existing producers and consumers — continue to work without changes
+
+## Side-by-side: a v1 named format → v2 product format declaration
+
+### v1 — separate format file referenced by product
+
+```json test=false
+// formats/meta_reels.json
+{
+ "format_id": { "agent_url": "https://creative.adcontextprotocol.org", "id": "meta_reels" },
+ "name": "Meta Reels",
+ "type": "video",
+ "assets": [
+ {
+ "asset_id": "video_file",
+ "asset_type": "video",
+ "asset_role": "hero_video",
+ "item_type": "individual",
+ "required": true,
+ "requirements": {
+ "min_duration_ms": 3000,
+ "max_duration_ms": 90000,
+ "aspect_ratio": "9:16",
+ "min_width": 1080,
+ "min_height": 1920,
+ "containers": ["mp4"],
+ "codecs": ["h264"]
+ }
+ }
+ ]
+}
+
+// products/meta_reels_us.json
+{
+ "product_id": "meta_reels_us",
+ "name": "Meta Reels — United States",
+ "format_ids": [
+ { "agent_url": "https://creative.adcontextprotocol.org", "id": "meta_reels" }
+ ],
+ // ... pricing, targeting, etc.
+}
+```
+
+### v2 — inline format declarations on the product
+
+```json test=false
+{
+ "product_id": "meta_reels_us",
+ "name": "Meta Reels — United States",
+ "format_options": [
+ {
+ "format_kind": "video_hosted",
+ "params": {
+ "orientation": "vertical",
+ "aspect_ratio": "9:16",
+ "duration_ms_range": [3000, 90000],
+ "min_width": 1080,
+ "min_height": 1920,
+ "video_codecs": ["h264"],
+ "containers": ["mp4"],
+ "headline_max_chars": 25,
+ "primary_text_max_chars": 72,
+ "cta_values": ["LEARN_MORE", "SHOP_NOW", "DOWNLOAD", "SIGN_UP"],
+ "composition_model": "deterministic",
+ "platform_extensions": [
+ { "uri": "https://meta.adcp/extensions/meta_pixel", "digest": "sha256:..." }
+ ]
+ }
+ }
+ ],
+ // ... pricing, targeting, etc.
+}
+```
+
+`format_options` is an array. The 90% case is one element — one canonical narrowed for the product. Multi-element arrays declare that the product accepts any of the listed format options, picked by the buyer at `sync_creatives` time. Common multi-element use cases: a placement that accepts EITHER a third-party-hosted creative (e.g., Flashtalking-served `html5`) OR an internal `display_tag`; a video product that accepts a hosted upload (`video_hosted`) OR a tag (`video_vast`). Each entry is a discriminated union: `format_kind` names the canonical format; `params` carries that canonical's parameter schema. SDKs codegen clean tagged unions in TypeScript and Pydantic. A v2 product is **either** v1 (uses `format_ids`) **or** v2 (uses `format_options`) — not both. The product schema's `oneOf` enforces this.
+
+For 12 fully-validated reference Product fixtures spanning all 11 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`), 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/v2/` and `static/examples/get_products_responses/v2/`. The Veo fixture exercises `synthesis_nondeterministic: true` and `provenance_required: true`. Each fixture passes `npm run test:v2-fixtures`.
+
+## Slot name mapping (v1 → canonical)
+
+If a v1 format slot uses an author-invented name that the canonical vocabulary covers, the format declaration carries an optional `asset_group_id` field on the slot pointing at the canonical entry. Same as the existing `asset_role` field, but referencing the [canonical vocabulary](https://adcontextprotocol.org/schemas/v3/core/asset-group-vocabulary.json) rather than free text.
+
+```json test=false
+// v1 format with author-invented slot name
+{
+ "asset_id": "click_url",
+ "asset_type": "url",
+ "item_type": "individual",
+ "required": true
+}
+
+// Same v1 format with canonical pointer (additive — backwards-compatible)
+{
+ "asset_id": "click_url",
+ "asset_type": "url",
+ "asset_group_id": "landing_page_url",
+ "item_type": "individual",
+ "required": true
+}
+```
+
+The vocabulary registry's `aliases` field captures common v1 alias names per canonical entry (e.g., `landing_page_url` aliases include `click_url`, `link`, `final_url`, `link_url`, `click_through_url`, `landing_url`). Six different names for the same field collapse to one canonical.
+
+Common alias mappings (from the audit-grounded set in `asset-group-vocabulary.json`):
+
+| Canonical | Common v1 aliases |
+|---|---|
+| `headlines` | `headline`, `title`, `tagline`, `headline_text` |
+| `descriptions` | `description`, `body`, `body_text`, `text`, `content` |
+| `images_landscape` | `image`, `hero_image`, `landscape_image`, `banner_image` |
+| `images_vertical` | `vertical_image`, `story_image`, `portrait_image` |
+| `images_square` | `square_image`, `feed_image` |
+| `logo` | `brand_logo`, `logo_image` |
+| `video` | `video_file`, `hero_video`, `video_asset`, `video_main` |
+| `audio` | `audio_file`, `hero_audio`, `audio_asset`, `audio_main` |
+| `landing_page_url` | `click_url`, `link`, `final_url`, `link_url`, `click_through_url`, `landing_url` |
+
+## Discovery surface migration
+
+`list_creative_formats` is uniformly deprecated. Replacements:
+
+| Role | v1 path | v2 path |
+|---|---|---|
+| Sales agent | `list_creative_formats` returns the seller's accepted formats | `get_products` — each product carries its `format` declaration inline. Optionally also `creative.supported_formats` on `get_adcp_capabilities` for a flat summary. |
+| Creative agent (no inventory) | `list_creative_formats` overloaded as "what I can produce" | `creative.supported_formats` on `get_adcp_capabilities`. Each entry uses the same `ProductFormatDeclaration` shape as products' inline `format`. |
+
+Sellers SHOULD provide a server-side flatten wrapper that derives the v1 `list_creative_formats` shape from v2 product format declarations through 4.0, so existing dashboards and tooling keep working. The wrapper iterates over `get_products`, reads each product's `format` declaration, and emits a v1-compatible format file plus a `format_ids` reference.
+
+## Generative formats — `*_generated_*` files dissolve
+
+The agentic-adapters audit found ~30 `*_generated_*` format files (e.g., `meta_generated_reels`, `tiktok_generated_video_9x16`) that mirror their non-generated counterparts but accept a `creative_brief` instead of an asset upload. In v2 these collapse:
+
+- The format declaration's `slots` array enumerates everything the buyer ships in the manifest's `assets` map — each entry is a canonical `asset_group_id` paired with an `asset_type`. Some slots are rendered verbatim (image / video / audio); some are consumed for production (text script → host-read audio; brief → synthesized image; scenes → generated video). The seller dispatches per slot.
+- Whether the seller's internal production is generative AI, host recording, transcoding, or asset rendering is **invisible to the buyer**
+- A single canonical format (e.g., `audio_hosted`) handles both buyer-uploaded audio and agent-produced audio; the format's `audio_source` and `buyer_audio_acceptance` parameters describe which flows are accepted
+
+Side-by-side for an audio format:
+
+```json test=false
+// v1: separate generated format file
+{
+ "format_id": { "agent_url": "...", "id": "audiostack_audio_30s_generated" },
+ "name": "AudioStack 30s Audio (Generated)",
+ "assets": [
+ {
+ "asset_id": "creative_brief",
+ "asset_type": "brief",
+ "required": true
+ },
+ {
+ "asset_id": "audio_output",
+ "asset_type": "audio",
+ "required": false
+ }
+ ]
+}
+
+// v2: same canonical (audio_hosted), buyer-shipped assets declared as slots
+{
+ "format_kind": "audio_hosted",
+ "params": {
+ "duration_ms_exact": 30000,
+ "audio_codecs": ["mp3"],
+ "loudness_lufs": -16,
+ "audio_source": "agent_synthesized",
+ "buyer_audio_acceptance": "rejected",
+ "slots": [
+ { "asset_group_id": "creative_brief", "required": true, "asset_type": "brief", "max_chars": 1000 },
+ { "asset_group_id": "voice_id", "required": false, "asset_type": "text" }
+ ],
+ "production_window_business_days": 0
+ }
+}
+```
+
+Note the v2 manifest has no separate `inputs` map — the buyer ships the brief and voice_id as `text`/`brief` assets in the `assets` map under those slot names. The seller dispatches per the format's slot declaration: brief → consume for synthesis; rendered audio is what comes out the other side.
+
+## Brand identity — slots disappear
+
+v1 formats sometimes redeclared `brand_logo`, `brand_colors`, `brand_voice`, `brand_tagline` as explicit slots. v2 formats don't. When the manifest carries a [`BrandRef`](https://adcontextprotocol.org/schemas/v3/core/brand-ref.json) (`brand: { domain: "acme.com" }`, optionally with `brand_id` for house-of-brands), the seller fetches `brand.json` for context automatically.
+
+For the case where `brand.json` is missing or stale, the manifest carries `brand_kit_override`:
+
+```json test=false
+{
+ "format_id": { "agent_url": "...", "id": "..." },
+ "assets": { ... },
+ "brand": { "domain": "acme.example" },
+ "brand_kit_override": {
+ "logo": { "asset_type": "image", "url": "https://cdn.acme.example/logo-2026.png", "width": 200, "height": 100 },
+ "colors": { "primary": "#0066CC", "accent": "#FF6600" },
+ "tagline": "Spring savings, all season"
+ }
+}
+```
+
+Override fields take precedence over `brand.json` for that creative.
+
+## Tools — what's new vs unchanged
+
+| Tool | v1 | v2 |
+|---|---|---|
+| `get_products` | Returns products with `format_ids` | Returns products with either `format_ids` (v1 path) or `format` (v2 inline) |
+| `sync_creatives` | Submit creative manifest | Unchanged. Sales agents accept manifests with `assets` keyed by slot name per the format's `slots` declaration. |
+| `preview_creative` | Submit manifest, get preview | Same surface; preview shows output regardless of whether the slots ship rendered creative (image/video/audio) or production content (script/brief/scenes). The single-render hoist in #3268 lands alongside v2. |
+| `validate_input` | (didn't exist) | New buyer dry-run primitive. Validates a manifest against canonical formats and/or specific products without committing to a render. Cheap; `predicted` field carries pre-flight estimates. |
+| `build_creative` | Generative tool on creative agents | Same role; creative-agent surface only. Sales agents do **not** expose `build_creative`. Creative agents may **also** expose `sync_creatives` for ad-server use cases. |
+| `list_creative_formats` | Both sales and creative agents | Deprecated. Sales agents migrate to `get_products`; creative agents to `creative.supported_formats`. v1 tool stays functional through 4.x. |
+
+## Adopter migration paths
+
+### Sales agents (DSPs, SSPs, retail media networks, walled gardens)
+
+1. **Inventory**: enumerate your existing v1 named formats. Confirm each maps to one of the 11 v2 canonicals OR to a custom shape (see "Shipping a custom format" below). Composed/coordinated/sponsorship shapes (multi-placement takeover, roadblock, branded content, cross-screen sponsorship, sponsorship lockup, newsletter, AR lens, playable, live event sponsorship) ship as `format_kind: "custom"` with a `format_shape` registry classifier and a `format_schema` URI+digest reference.
+2. **Translate**: for each named format, write a v2 `ProductFormatDeclaration` narrowing the canonical with your platform's parameters. For custom shapes, author a JSON Schema describing your format's `params` and `slots`, host it at a stable URI on your subdomain (or via the AAO mirror for walled-garden sellers), and reference it from `format_schema`.
+3. **Be honest about runtime readiness**: set `runtime_status` on each declaration. `stable` (default) means your runtime fully honors the declared format and production source. `preview` means the basic path works but advanced axes (per-item fan-out under `item_production_model`, brief-driven overrides, advanced `platform_extensions`) may be partial. `declared_only` means the catalog declaration is forward-looking and your runtime does NOT yet implement the path — common during migration when you port v1 catalog declarations forward but haven't wired the new production-source axis yet. Buyers can filter on this; compliance storyboards skip-gate `declared_only` entries gracefully. Upgrade the value as your runtime catches up.
+4. **Test**: validate translated declarations against `/schemas/core/product.json` (use the `npm run test:v2-fixtures` pattern).
+5. **Publish dual**: keep your v1 named formats and `list_creative_formats` working through 4.x. Add the v2 `format_options` field on products that have it.
+6. **Flatten wrapper**: implement a server-side wrapper that derives the v1 `list_creative_formats` shape from v2 product declarations. Lets v1-era dashboards and tooling keep working.
+7. **Deprecate timing**: at 5.0, remove v1 `format_ids` references on your products. Until then, both paths coexist.
+
+#### Server-side implementation considerations
+
+Three concrete hooks v2 introduces that existing seller implementations don't have today:
+
+- **`sync_creatives` provenance verification when `provenance_required: true`.** When a v2 product's format declaration carries `provenance_required: true` (and the buyer's manifest contains synthesized assets — typically video/image from generative platforms), `sync_creatives` MUST verify a C2PA-compatible provenance manifest is attached and reject unsigned synthesized assets. This is a natural extension of existing AI-provenance tracking on Creative (EU AI Act Article 50 work) — the new piece is a validation hook that gates submission. Sellers without existing provenance plumbing only need this once they ship a v2 product with the flag set; until then it's no-op.
+- **`get_products` response gathers extension definitions.** When products carry v2 `format.params.platform_extensions` references, the response SHOULD include the referenced extension definitions in the `extensions` map keyed by `@sha256:`. Implementations gather extensions referenced by any product in the response, dedupe by digest, and emit. Buyers cache by URI@digest; subsequent responses MAY omit definitions the buyer already has cached. Trivial when no products use v2 declarations; only kicks in when tenants opt in.
+- **`production_window_business_days` on host-read / agent-produced products.** Today most server implementations don't model production turnaround on Products — the field is a v2 addition. Only matters once a tenant ships a v2 host-read or generative-video product (audio_hosted with `audio_source: 'publisher_host_recorded'`, or any product with `synthesis_nondeterministic: true`). Today many of these flows route through hand-trafficked sponsorships and don't surface turnaround over the protocol; v2 makes it declarable.
+
+#### Shipping a custom format
+
+Sellers with creative structures that don't fit the 11 canonicals (multi-placement takeover, roadblock, branded content, cross-screen sponsorship, sponsorship lockup, newsletter sponsorship, AR lens, playable, live event sponsorship) ship via `format_kind: "custom"`. Three pieces:
+
+1. **Pick a `format_shape`** from the [vocabulary registry](https://adcontextprotocol.org/schemas/v3/core/format-shape-vocabulary.json). If your shape isn't there, file a vocabulary PR — adding entries is governance-light, doesn't require a major version bump, and helps the working group track adoption velocity per shape.
+2. **Author a JSON Schema** describing your format's `params` and `slots`. The schema's job is to give buyer agents enough structure to validate manifests and reason about what assets you accept, how you track, what the impression contract is. Treat it like authoring a v1 named format file — same level of rigor, just hosted at your URI rather than under AdCP's roof. Industry-shared schemas (e.g., a shared `multi_placement_takeover_v1` schema several publishers converge on) are encouraged and accelerate canonical promotion.
+3. **Host the schema at a stable URI** with `Cache-Control: public, max-age=31536000, immutable` and a digest. Open-ecosystem publishers host on their own subdomain (`https://yourpub.adcp/schemas/formats/your_shape_v1`); walled-garden sellers route through the AAO mirror at `https://mirror.adcontextprotocol.org/translated//` (AAO accepts translation submissions; same hosting / immutability contract). Reference the schema from `format_schema: { uri, digest }` on your `ProductFormatDeclaration`.
+
+Buyer agents fetch the schema by `uri@digest`, cache it (immutable), and validate manifests structurally. **No human-in-the-loop is required for buyer agents to interpret your format** — that's the load-bearing claim and the reason custom + format_schema isn't `ext`. Ext remains for genuinely experimental shapes that don't even fit a `format_shape` entry yet, but that's the rare case.
+
+When 2+ adopters ship the same `format_shape` with substantively similar `format_schema` content for 90+ days, the working group promotes the shape to a first-class canonical (creates `/schemas/formats/canonical/.json`, adds the value to `canonical-format-kind.json`, retires the registry entry). Adopters migrate from `format_kind: "custom"` to `format_kind: ""` at that point. The promotion queue is tracked at [adcp#3666](https://github.com/adcontextprotocol/adcp/issues/3666).
+
+### Creative agents (Flashtalking, AudioStack, generative platforms, AI rendering services)
+
+The spec has historically read sales-agent-first. v2 reshapes the creative-agent path enough to warrant its own walkthrough — both for ad-server-shaped creative agents (Flashtalking, Innovid, Sizmek-class) and transformation-shaped creative agents (AudioStack, Pencil, AdCreative.ai-class).
+
+#### What changes for a creative agent in v2
+
+| Concern | v1 | v2 |
+|---|---|---|
+| Format catalog publishing | `list_creative_formats` returned your producible catalog; sales agents could reference it via `creative_agents[]` recursive-discovery hint on their own `list_creative_formats` | `creative.supported_formats` on `get_adcp_capabilities` (each entry is a `ProductFormatDeclaration` — same shape as a sales agent's product `format_options[i]`). v1 `list_creative_formats` stays functional through 4.x. |
+| Format authoring | You authored named formats keyed under `your-domain.adcp` and published them | You declare which AdCP-defined canonical formats you can produce (`format_kind` discriminator) with your platform-specific narrowing (`params`). No more publishing free-text named formats. |
+| Discovery | Sales agents pointed buyers at you via `creative_agents[]` (recursive query); buyers fetched your `list_creative_formats` to learn what you produce | Buyers reach you directly — through brand-side relationships, AAO registry, direct knowledge. Sales agents in v2 do NOT carry a list of "approved creative agents." Each side is independent. |
+| `build_creative` contract | Buyer shipped a manifest with `format_id` + `assets` + `inputs` (separate "production inputs" map) | Buyer ships the same envelope, but `inputs` is collapsed into `assets` — everything goes through one `assets` map keyed by canonical `asset_group_id`. The format's `slots` declaration tells you which assets to expect, each typed by `asset_type`. The seller (you) dispatches per slot — render verbatim for `image` / `video` / `audio` slots; consume for production for `text` / `brief` / `object` slots (e.g., `script` text → host-recorded audio; `creative_brief` brief → generated image). |
+| Production-source declaration | Implicit (the named format's name implied the model — `*_generated_*` for AI-produced) | Explicit per-canonical: `audio_source` / `image_source` / `video_source` enums declare who renders and when (`buyer_uploaded` / `publisher_host_recorded` / `seller_pre_rendered_from_brief` / `seller_human_designed` / `agent_synthesized`). Plus `synthesis_nondeterministic: true` for Veo/Sora-class flows that need post-synthesis QA-loop semantics. |
+| Tracking integration | Your platform's pixel IDs, viewability vendors, OM-SDK partners lived in your named format's `tracking_events` field — sellers and buyers parsed your free-text declarations | Declare via `platform_extensions: [{uri, digest}]` and `tracking_extensions` on each `supported_formats[].format`. Each extension is a URI you host (or the AAO mirror translates) describing the schema for your platform's tracking surface (pixel IDs, conversion event taxonomies). Sellers and buyers cache by `uri@digest`; SDK codegen produces typed extension handlers. |
+| Hosting of produced bytes | Your CDN, your call | Your CDN, your call. v2 disaggregation is conceptual (the spec separates production from serving from tracking) — operationally, produced asset URLs in the manifest you return from `build_creative` continue to point at your CDN, your tracking JS continues to instrument, your platform extensions document the integration. |
+
+#### Concrete example: Flashtalking-shaped creative agent
+
+A creative agent that produces image / VAST / html5 creatives across multiple sizes and surfaces. Pre-v2, it published 30+ named formats (one per size × surface combination). v2 collapses to a smaller `supported_formats` set:
+
+```json test=false
+// GET https://flashtalking.adcp/.well-known/agent.json
+// → get_adcp_capabilities response (excerpt)
+{
+ "creative": {
+ "supports_generation": true,
+ "supports_transformation": true,
+ "has_creative_library": true,
+ "supported_formats": [
+ {
+ "capability_id": "flashtalking_image_iab_standard",
+ "format": {
+ "format_kind": "image",
+ "params": {
+ "image_formats": ["jpg", "png", "gif"],
+ "max_file_size_kb": 200,
+ "ssl_required": true,
+ "image_source": "buyer_uploaded",
+ "platform_extensions": [
+ { "uri": "https://flashtalking.adcp/extensions/flashtalking_pixel_v2", "digest": "sha256:..." }
+ ]
+ }
+ }
+ },
+ {
+ "capability_id": "flashtalking_video_vast_42",
+ "format": {
+ "format_kind": "video_vast",
+ "params": {
+ "vast_version": "4.2",
+ "duration_ms_range": [6000, 60000],
+ "linear_required": true,
+ "ssl_required": true,
+ "platform_extensions": [
+ { "uri": "https://flashtalking.adcp/extensions/flashtalking_vpaid_2_0", "digest": "sha256:..." }
+ ]
+ }
+ }
+ },
+ {
+ "capability_id": "flashtalking_html5_iab_standard",
+ "format": {
+ "format_kind": "html5",
+ "params": {
+ "max_initial_load_kb": 200,
+ "om_sdk_required": true,
+ "backup_image_required": true,
+ "ssl_required": true
+ }
+ }
+ }
+ ]
+ }
+}
+```
+
+What disappears: 30+ named-format files, each with its own `tracking_events` array, each with its own slot vocabulary. What replaces them: 3 canonical declarations narrowed by params + platform_extensions for Flashtalking-specific tracking. SDK codegen on the buyer side produces a typed handler per `format_kind` rather than a typed handler per Flashtalking-named-format — a 10× reduction in surface area for buyers integrating Flashtalking.
+
+The buyer flow is unchanged from a Flashtalking perspective: `build_creative` still receives a brief / assets / brand reference; you produce a manifest with rendered asset URLs on Flashtalking's CDN; the buyer submits that manifest to whatever sales agent they're shipping to. The sales agent validates against canonical `image` / `video_vast` / `html5` — NOT against your Flashtalking narrowing. Your platform_extensions remain attached to the manifest so the sales agent honors Flashtalking pixel IDs and viewability vendors at serve time.
+
+#### Concrete example: AudioStack-shaped transformation agent
+
+A transformation agent that takes a buyer's brief or script and produces a rendered audio file. Pre-v2, AudioStack published `audiostack_audio_30s_generated` and similar. v2 collapses to a per-canonical declaration with explicit production-source semantics:
+
+```json test=false
+// GET https://audiostack.adcp/.well-known/agent.json
+{
+ "creative": {
+ "supports_generation": true,
+ "supports_transformation": true,
+ "supported_formats": [
+ {
+ "capability_id": "audiostack_audio_brief_to_30s",
+ "format": {
+ "format_kind": "audio_hosted",
+ "params": {
+ "duration_ms_exact": 30000,
+ "audio_codecs": ["mp3", "aac"],
+ "audio_sample_rates": [44100, 48000],
+ "audio_channels": ["stereo"],
+ "loudness_lufs": -16,
+ "audio_source": "seller_pre_rendered_from_brief",
+ "buyer_audio_acceptance": "rejected",
+ "production_window_business_days": 1,
+ "slots": [
+ { "asset_group_id": "creative_brief", "asset_type": "brief", "required": true, "max_chars": 500 },
+ { "asset_group_id": "voice_id", "asset_type": "text", "required": false },
+ { "asset_group_id": "landing_page_url", "asset_type": "url", "required": false }
+ ]
+ }
+ }
+ },
+ {
+ "capability_id": "audiostack_audio_script_to_30s",
+ "format": {
+ "format_kind": "audio_hosted",
+ "params": {
+ "duration_ms_exact": 30000,
+ "audio_codecs": ["mp3"],
+ "audio_source": "agent_synthesized",
+ "buyer_audio_acceptance": "rejected",
+ "synthesis_nondeterministic": false,
+ "production_window_business_days": 0,
+ "slots": [
+ { "asset_group_id": "script", "asset_type": "text", "required": true, "max_chars": 800 },
+ { "asset_group_id": "voice_id", "asset_type": "text", "required": true }
+ ]
+ }
+ }
+ }
+ ]
+ }
+}
+```
+
+Two distinct capabilities — brief-to-audio (creative direction → produced ad) and script-to-audio (deterministic TTS from verbatim script) — declared as two `supported_formats` entries sharing `format_kind: audio_hosted` but with different `audio_source` values. `capability_id` disambiguates which capability the buyer is invoking when they call `build_creative`.
+
+#### Server-side hooks for creative agents
+
+Three implementation considerations specific to creative agents in v2:
+
+- **`creative.supported_formats` is your public contract.** Buyers and sales agents read it to know what you produce. Keep `capability_id` values stable across releases — buyers reference them in their `build_creative` calls.
+- **`synthesis_nondeterministic: true` implies a QA-loop obligation.** When you declare it, you commit to: validating each synthesis attempt against the format's parameter constraints; reseeding up to N times to produce in-spec output; returning `task_failed` with `synthesis_failed` reason if the loop exhausts. There is no protocol state for orphaned out-of-spec artifacts — the buyer never sees a partial result.
+- **`provenance_required: true` requires C2PA attestation.** When a sales agent's product carries `provenance_required: true` and the buyer is shipping your produced asset, the manifest you return MUST include a C2PA-compatible provenance manifest attributing synthesis to your agent (not the buyer, not the seller). EU AI Act Article 50 alignment.
+
+#### Migration timing for creative agents
+
+| Item | Timing |
+|---|---|
+| Continue to expose `list_creative_formats` (v1) | Through 4.x. Sales agents and buyers may still call it. |
+| Add `creative.supported_formats` to `get_adcp_capabilities` | Anytime in 3.1+. Additive — doesn't break v1 callers. |
+| Stop publishing new v1 named formats | When 80% of your buyers are reading `supported_formats` (track via your own analytics — buyers calling `get_adcp_capabilities` vs `list_creative_formats`). |
+| Drop `list_creative_formats` | Coordinated with the v1 deprecation calendar (2027-Q4 floor / 2029-Q1 ceiling) — same window as sales agents dropping `format_ids`. |
+
+### Buyers / DSPs
+
+1. Update your client to read either `format_ids` (v1) or `format` (v2 inline) on products.
+2. Use `validate_input` for cheap dry-run validation before committing to renders.
+3. Use the canonical [`asset_group_id` vocabulary](https://adcontextprotocol.org/schemas/v3/core/asset-group-vocabulary.json) when constructing manifests; the registry's `aliases` field maps v1-era slot names to canonical equivalents.
+4. Submit creative via `sync_creatives` as before. For products whose slots accept production content (host-read podcasts ship a `script` text asset; generative video ships a `creative_brief` and `scenes`), either pre-produce via a creative agent's `build_creative` to get back a manifest with rendered assets OR submit the production-content assets directly and let the seller produce internally — both flows are valid.
+
+### Publisher direct (GAM/prebid path)
+
+1. The `zip` asset type (Phase 1) handles HTML5 banner bundles cleanly. URL-delivered HTML/JS routes through `url-asset.json` with appropriate `url_type`.
+2. Tag-based delivery (VAST, third-party display tags) maps to the `display_tag`, `video_vast`, and `audio_daast` canonical formats.
+3. Native canonical format is deferred to 3.2 after the TemplateCreative + OpenRTB Native 1.2 audit; until then, native formats stay on the v1 path.
+
+## Realistic timeline by adopter type
+
+| Adopter | Cost | Realistic timeline |
+|---|---|---|
+| DSP buyer agents (TTD-shaped) | Low | 3.1-3.2 |
+| SSP/sales agents (Magnite, PubMatic, retail media) | Medium-high | 3.3-4.0 |
+| Walled gardens (Meta, Google, Amazon, TikTok, Snap, Pinterest) | High, low motivation | 4.0-5.0 if at all (gated on AAO providing a translator from existing format docs) |
+| Creative agents (AudioStack-shaped) | Low, high motivation | 3.1-3.2 |
+| Publisher direct (GAM/prebid path) | Medium | Blocked on native canonical pre-audit |
+
+### When does v1 `format_ids` get removed?
+
+The `oneOf(format_ids, format_options)` shape on `Product` persists through 4.x — every validator, codegen, and adopter has to handle both shapes. The 5.0 cut is **adoption-driven, with calendar floor and ceiling**:
+
+- **Floor (minimum end-of-life)**: v1 `format_ids` is supported through at least **2027-Q4** regardless of adoption signal. Adopters whose org reality (legacy ad-server integrations, walled-garden translation gaps, slow procurement cycles) prevents immediate migration get an unconditional 18-month runway from 3.1 GA.
+- **Ceiling (maximum end-of-life)**: v1 `format_ids` is removed no later than **2029-Q1** regardless of adoption signal. Caps the long-tail liability for SDK authors and validator maintainers; matches the v2-sunset-policy pattern from 3.0.
+- **Adoption trigger (within the floor / ceiling window)**: AAO computes the ratio of registered sales agents declaring `format_options` (vs `format_ids`) from cached `get_products` capabilities responses. The trigger is denominator = sales agents declaring `creative` in `supported_protocols`; numerator = those whose latest `get_products` response carries `format_options` on every product. When the ratio crosses **80% and stays there for 30 consecutive days** within the floor/ceiling window, the 5.0 cut sequence opens (deprecation warnings escalate; the next major drops `format_ids`). Until that signal trips, both shapes remain valid.
+
+The trigger metric, denominator, refresh cadence, and certification path are published at `https://adcontextprotocol.org/registry/format-options-adoption.json` (concrete shape and refresh cadence to be committed in a follow-up issue before 3.1 GA). Adopters who want to influence the timing should migrate early and watch the public ratio. Walled gardens that don't migrate are absorbed by the ceiling.
+
+### `creative_id` stability across v1 ↔ v2
+
+A creative registered against v1 `format_id` retains the same `creative_id` when later viewed via the v2 flatten path. `sync_creatives` request and response shapes are unchanged; the manifest envelope is unchanged. Migration is read-side: existing creatives keep working and resolve identically through both paths. SDK authors building flatten wrappers MUST honor this invariant.
+
+## `product_card` and `product_card_detailed` are typed inline
+
+The `product_card` and `product_card_detailed` fields on the `Product` object are typed inline structures in v2 — no `format_id` indirection, no manifest. They describe the **UI rendering of the product itself** (what catalog browsers, dashboards, and admin interfaces display so humans and agents can see what a product is). Distinct from `format` (which describes the ad creative the product accepts).
+
+```json test=false
+"product_card": {
+ "image": { "asset_type": "image", "url": "https://...", "width": 300, "height": 400 },
+ "title": "Meta Reels — United States",
+ "description": "9:16 vertical short-form video on Meta Reels.",
+ "price_label": "From $5.50 CPM",
+ "cta_label": "View details"
+}
+
+"product_card_detailed": {
+ "hero_image": { "asset_type": "image", "url": "...", "width": 1200, "height": 600 },
+ "carousel_images": [ { "asset_type": "image", "url": "...", ... }, ... ],
+ "title": "Meta Reels — United States",
+ "description": "Full markdown-friendly product description...",
+ "specifications": [
+ { "label": "Aspect ratio", "value": "9:16" },
+ { "label": "Duration", "value": "3-90s" }
+ ],
+ "price_label": "From $5.50 CPM",
+ "cta_label": "Get proposal"
+}
+```
+
+Migration from v1 (where `product_card` was `{ format_id, manifest }` referencing a `product_card_standard` format file): drop the format reference; populate the typed fields directly. The image, title, and description flatten out of what was previously a manifest. v2-only adopters who don't render product cards can ignore both fields entirely.
+
+## What v2 gives you that OpenRTB doesn't
+
+Adopters with existing OpenRTB Native / Display / Audio pipelines reasonably ask "why migrate to v2 when I already have a working creative-spec model?" The differential value:
+
+- **Buyer validates against the canonical, not the seller's narrowing.** OpenRTB has no canonical layer. Each SSP authors its own native asset spec (Native 1.2 left placement-specific assets to "see the impl"); each video player authors its own VAST extensions; each retail-media network publishes its own catalog field shape. Buyers must discover and validate against per-seller specs at runtime. v2's canonical-as-contract decouples buyer validation from per-seller schema discovery — the buyer ships a manifest that satisfies canonical `image` and knows it's structurally valid against any seller speaking that canonical, BEFORE knowing which seller wins the auction.
+- **Discovery is operational, not implicit.** OpenRTB Display 1.x and Native 1.2 expect adopters to read the spec, write per-version handlers, and pre-bake support for what each seller might require. v2 carries the format declaration inline on the product (`format_options[i]` on `get_products`) and on the creative agent (`creative.supported_formats` on `get_adcp_capabilities`). Buyers fetch what's accepted at runtime; SDK codegen produces typed handlers for the canonicals; new sellers don't require buyer-side code changes.
+- **Production source is first-class.** OpenRTB has no notion of "buyer ships a brief; seller renders." The closest expression is OpenRTB Native's `nobid_reason` after submission. v2 makes production source a declared parameter (`*_source` enums), so generative DSPs and host-read products are visible at discovery time, not only after rejection.
+- **Tracking model is canonical-defined.** OpenRTB tracking (impression NURL, click NURL, third-party trackers) is consistent for VAST but fragmented for native and display. v2 bakes the tracking model into each canonical (impression pixel for image, MRAID + OM-SDK for html5, VAST events for video_vast, per-card pixels for image_carousel) — buyers know what tracking shape applies from the canonical alone.
+
+v2 doesn't replace OpenRTB at the auction layer (where OpenRTB Display, Video, Audio specs continue to drive the bid request / response shape). v2 is the creative-payload layer above the auction, and it adds what OpenRTB intentionally left implementation-specific.
+
+## Validating your migration
+
+Run the fixture validation against your translated products:
+
+```bash test=false
+npm run test:v2-fixtures
+```
+
+The reference fixtures at `static/examples/products/v2/` are validated against `/schemas/core/product.json`. Adopters can drop their own translated products into a sibling directory and reuse the same validator pattern.
+
+## Related
+
+- [Creative Formats v2 overview](/docs/creative/v2-overview)
+- [RFC #3305](https://github.com/adcontextprotocol/adcp/issues/3305) — architectural decisions and rationale
+- [Asset group vocabulary](https://adcontextprotocol.org/schemas/v3/core/asset-group-vocabulary.json)
+- [BrandRef schema](https://adcontextprotocol.org/schemas/v3/core/brand-ref.json)
+- [Universal macros](/docs/creative/universal-macros) — substitution patterns referenced from canonical tracking
diff --git a/docs/creative/v2-overview.mdx b/docs/creative/v2-overview.mdx
new file mode 100644
index 0000000000..64409de930
--- /dev/null
+++ b/docs/creative/v2-overview.mdx
@@ -0,0 +1,826 @@
+---
+title: Creative Formats v2 (preview)
+description: "Canonical formats live on AdCP; sellers' products narrow them inline. Phase 1 + Phase 2 preview against the v2 RFC #3305."
+"og:title": "AdCP — Creative Formats v2"
+testable: true
+---
+
+# Creative Formats v2 (preview)
+
+> **Status:** Preview track. The v2 surface is being designed in flight against [RFC #3305](https://github.com/adcontextprotocol/adcp/issues/3305) and the [#3307 implementation branch](https://github.com/adcontextprotocol/adcp/pull/3307). v1 named formats (`format_id` as `{ agent_url, id }`) remain a first-class path through 4.x with a 5.0 sunset; v2 is opt-in and additive at the schema layer. **The typed-tagged-union ergonomics v2's design earns require SDK codegen to deliver — Phase 4 (TypeScript and Python codegen) is the gating dependency for adopter consumption. Until Phase 4 ships, adopters can build against the schemas directly, but the buyer-side mental simplification v2 promises lands fully only with codegen.**
+
+v2 collapses today's separate format registry into product-bound declarations. AdCP defines a small set of **canonical formats** (universal building blocks); sellers' products carry inline `ProductFormatDeclaration`s that narrow canonicals with platform-specific parameters. Creative agents become transformation services declaring `build_creative` capabilities targeting canonical formats. Most existing concepts (CTAs, destinations, tracking, brand identity) are reused or stay in their current homes — v2 doesn't create a new vocabulary layer for those.
+
+## Glossary
+
+| Term | One-line definition |
+|---|---|
+| **Canonical format** | One of 11 AdCP-defined format archetypes that products narrow (e.g., `image`, `video_vast`, `audio_hosted`). The buyer's stable validation target. |
+| **`format_kind`** | Discriminator value naming a canonical format (e.g., `"image"`). Selects which canonical's parameter schema applies. |
+| **`format_options`** | Array of `ProductFormatDeclaration`s on a v2 product. The 90% case is single-element; multi-element declares "accepts any of." |
+| **`ProductFormatDeclaration`** | Inline format declaration: `format_kind` + `params` + optional `capability_id` + optional `applies_to_channels` + optional `runtime_status`. |
+| **`capability_id`** | Stable identifier for a format declaration, used to disambiguate when `format_options` carries multiple declarations sharing the same `format_kind`. |
+| **`applies_to_channels`** | Subset of the product's declared channels this format declaration applies to. Lets multi-channel products carry per-channel format options. |
+| **`runtime_status`** | Per-product-declaration adopter-runtime readiness: `stable` (default), `preview` (partial paths), `declared_only` (forward-looking — runtime not wired yet). Distinct from canonical-level `status` (which is about spec maturity). |
+| **`slots`** | Programmatic declaration on a format of which `asset_group_id` slots a manifest must (or may) populate, each paired with an `asset_type`. |
+| **`asset_group_id`** | Canonical slot-name vocabulary (e.g., `image_main`, `script`, `landing_page_url`). Replaces v1's free-text `asset_role`. |
+| **`composition_model`** | How the surface composes per-impression: `deterministic` (buyer-predictable per-slot) vs `algorithmic` (surface picks combinations from a pool). |
+| **`*_source`** | Per-canonical production-source declaration: `audio_source`, `image_source`, `video_source`, `item_production_model`. Describes who renders the asset and when. |
+| **`synthesis_nondeterministic`** | When true, the production pipeline cannot guarantee in-spec output (Veo/Sora-class). Implies QA-loop + retry semantics. |
+| **`provenance_required`** | When true, the product rejects unsigned synthesized assets. Builders attach C2PA-compatible provenance manifests. |
+| **`platform_extensions`** | URI+digest references to platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies). |
+| **`tracking_extensions`** | Subset of `platform_extensions` specifically scoped to tracking concerns (pixel IDs, viewability vendors, OM-SDK partners). |
+| **`status: preview`** | Canonical is shipped for early adoption but parameter shape MAY break in 3.2 once 2-3 adopters land. |
+| **`since_version` / `migration_target_version`** | Release-precision lifecycle metadata on canonicals — when introduced, when stabilization or breaking revision is expected. |
+| **`validate_input`** | Spec-defined dry-run primitive — buyers verify a manifest against canonicals/products without committing to a render. |
+| **`build_creative`** | Creative-agent surface that produces a manifest from inputs (brief, scenes, brand). Sales agents do NOT expose `build_creative`. |
+| **`creative.supported_formats`** | Capabilities-response field on creative agents declaring which canonicals they can produce via `build_creative`. |
+| **`BrandRef`** | `{domain, brand_id?}` reference. Resolves brand context (logos, colors, voice) from `brand.json` automatically. |
+| **`brand_kit_override`** | Per-creative override for the case where `brand.json` is missing, stale, or inappropriate. |
+| **`fanout_mode`** | On `sponsored_placement`: how items map to delivery — `per_item`, `multi_item_in_creative`, `single_item`. |
+| **`item_production_model`** | On `sponsored_placement`: how each per-item creative is produced. Captures multi-output generative (1 brief × N items → N creatives). |
+| **`format_kind: "custom"`** | Adopter-defined shape that doesn't fit the 11 canonicals (multi-placement takeover, branded content, AR lens, etc.). Requires `format_shape` (registry classifier) and `format_schema` (URI+digest reference to a fetchable schema). |
+| **`format_shape`** | Recognized global pattern from the [format-shape vocabulary registry](https://adcontextprotocol.org/schemas/v3/core/format-shape-vocabulary.json). Required when `format_kind: "custom"`. |
+| **`format_schema`** | URI+digest reference to the fetchable schema describing a custom shape's `params` and `slots`. Required when `format_kind: "custom"`. Same hosting model as `platform_extensions`. |
+
+## Architectural shift
+
+| Concept | v1 | v2 |
+|---|---|---|
+| Format identity | Compound `{ agent_url, id }` referencing a separately-defined format file | Canonical name (e.g., `image`) keyed under `format` on the product, narrowed inline |
+| Format authoring | Each platform authors its own named format files | Platforms narrow AdCP-defined canonicals; canonical IS the contract buyers validate against |
+| Format submission contract | Each platform publishes a parallel set of `*_generated_*` format files for AI-produced creative alongside the asset-upload version (~30 duplicate files in agentic-adapters) | The format declares a single `slots` array enumerating everything the buyer ships in the manifest's `assets` map, each entry a canonical `asset_group_id` paired with an `asset_type` (image / video / audio for direct rendering; text / brief / object / url for content the seller consumes for production). Buyer mental model is uniform — one `assets` map, no separate "inputs" concept. **Whether the seller's internal production is generative AI, host recording, transcoding, or asset rendering is invisible to the buyer.** No "generative" category at the protocol level; the production mechanism is implementation detail. |
+| Discovery | `list_creative_formats` (overloaded — used by both sales and creative agents) | `creative.supported_formats` on `get_adcp_capabilities` (uniform replacement, same `ProductFormatDeclaration` shape regardless of agent role); sales agents additionally expose `get_products` for product-level detail with `format` inline |
+| Tracking | Mixed across asset types and format definitions | Baked into each canonical format (VAST events for `video_vast`, MRAID+OM-SDK for `html5`, impression pixel for `image`) |
+| Brand identity | Sometimes redeclared as format slots | Implicit via `brand` (a [`BrandRef`](https://adcontextprotocol.org/schemas/v3/core/brand-ref.json) — `domain` plus optional `brand_id` for house-of-brands) resolving brand.json; explicit override via `brand_kit_override` on the manifest |
+
+## The 11 canonical formats
+
+Each canonical lives at `/schemas/formats/canonical/.json`. Tracking model is **format-specific** (split by tracking model is why we have 11 instead of, say, 5).
+
+| Canonical | Status | What it is | Tracking |
+|---|---|---|---|
+| `image` | stable | Static image, file or hosted URL redirect | Impression pixel + click URL via `universal_macros` |
+| `html5` | stable | Interactive HTML5 banner (zip asset) | MRAID + OM-SDK + click-tag macro + backup image |
+| `display_tag` | stable | Third-party JS/iframe tag URL | Opaque to seller |
+| `image_carousel` | stable | Multi-card swipe (polymorphic image/video items) | Per-card pixels + carousel engagement |
+| `video_hosted` | stable | Direct video file, orientation parameter | OM-SDK + external impression/click/quartile trackers |
+| `video_vast` | stable | VAST tag (URL or inline XML), VAST 2-4.x | Inherent VAST events |
+| `audio_hosted` | stable | Direct audio file (or host-read produced via build_creative) | Standard audio impression/completion |
+| `audio_daast` | stable | DAAST tag | Inherent DAAST events |
+| `sponsored_placement` | stable | Retail-media catalog-driven (Amazon SP, Criteo SP, CitrusAd SP) | Per-item catalog-keyed events |
+| `responsive_creative` | **preview** | Buyer asset pool, surface composes combinations (Google Responsive Display/Search Ads, Performance Max, Demand Gen; Meta Advantage+ creative) | Per-asset performance breakdown |
+| `agent_placement` | **preview** | Sponsored placement composed by an AI surface in response to a user query (ChatGPT, Perplexity, voice assistants, sponsored search snippets). Distinct from `si_chat` (brand-owned conversation; user → brand's agent). | Mention-level impression + attribution |
+
+The two `preview` canonicals (`responsive_creative`, `agent_placement`) carry surfaces whose composition models are still settling — Google PMax / Meta Advantage+ for responsive; ChatGPT / Perplexity / voice assistants for agent_placement. Their parameter shape and tracking model MAY break in 3.2 once 2-3 adopters have built against them. Buyers SHOULD plan for migration; sellers SHOULD treat preview-canonical narrowing as experimental contract surface, not a long-term commitment. The other 9 canonicals are anchored in stable IAB / platform standards (IAB display dimensions, IAB VAST 4.2, IAB DAAST 1.1, retail-media catalog conventions) and are committed.
+
+**Stabilization rubric for preview canonicals.** A preview canonical is promoted to `stable` when (a) at least 2 production adopters have built against it AND (b) 90 consecutive days have passed without a breaking change to its parameter shape. The default escalation date is the next minor release after both conditions are met — i.e., `responsive_creative` and `agent_placement` re-evaluated for stable status by 3.3 if adopters land in 3.1-3.2. To avoid the coordination problem where `preview` reads as "don't build against this" and therefore never stabilizes, sellers shipping preview canonicals SHOULD also publish a `migration_target_version` (carried on the canonical's `_base.json` `migration_target_version` field) so adopters know when to expect either stabilization or a breaking revision.
+
+### Two stability axes: canonical `status` vs declaration `runtime_status`
+
+Stability splits across two independent axes — the spec-maturity axis on the canonical, and the adopter-runtime axis on each product-format declaration:
+
+| Axis | Field location | Question it answers |
+|---|---|---|
+| **Spec maturity** | `params.status` on the canonical (`/schemas/formats/canonical/.json`) | "Has the v2 working group stabilized this format definition?" Values: `stable` / `preview` / `deprecated`. |
+| **Adopter runtime** | `runtime_status` on the `ProductFormatDeclaration` (per-product) | "Does THIS seller's runtime actually honor what they declared on THIS product?" Values: `stable` / `preview` / `declared_only`. |
+
+The two vary independently. A `stable` canonical can have `declared_only` adopters (the spec is settled but the seller hasn't wired the runtime path yet — common during v2 migration when adopters port v1 catalog declarations forward but their adapters still implement only the asset-upload path). A `preview` canonical can have `stable` adopters (a seller built a real working runtime against the preview shape and their integration honors what they declared).
+
+Why both exist: without `runtime_status`, sellers in mid-migration silently lie about what they support. They declare the shiny new production-source axis (`item_production_model: seller_pre_rendered_from_brief`) on a forward-looking product, but the actual `sync_creatives` runtime is still a buyer-uploaded-bytes loop. Buyers discover the mismatch only at submission time — exactly what v2's canonical-as-contract is supposed to prevent.
+
+The rule of thumb:
+
+- Default `stable` (or omit the field). The runtime honors what's declared.
+- `preview` — the basic path works; advanced axes (per-item fan-out, brief-driven overrides, advanced `platform_extensions`) may be partial. Buyers should `validate_input` before committing.
+- `declared_only` — forward-looking declaration. Runtime not wired yet. Buyers MUST treat as informational; compliance storyboards skip-gate gracefully rather than fail; budget should not flow until upgraded to `preview` or `stable`.
+
+Sellers MUST upgrade the value as the runtime catches up. Buyers cache it like any other capability field; subsequent `get_products` responses surface the new value naturally.
+
+## Two axes: composition (per-impression) vs production (who renders)
+
+Two orthogonal patterns govern how a creative is produced and how it serves. Conflating them is the most common authoring mistake.
+
+**Composition model** — `composition_model: deterministic | algorithmic` on the format declaration. Describes how the **surface composes per-impression**:
+- `deterministic` — buyer can predict per-slot rendering. The surface serves what it received. (`image`, `video_hosted`, `audio_hosted`, `video_vast`, `audio_daast`, `sponsored_placement`.)
+- `algorithmic` — surface picks combinations from a buyer-supplied asset pool per-impression. The buyer ships a pool; the surface composes. (`responsive_creative` for Google PMax / Meta Advantage+; `agent_placement` for AI-surface composition.)
+
+**Production source** — per-canonical `*_source` parameters, all sharing a single 5-value enum: `buyer_uploaded | publisher_host_recorded | seller_pre_rendered_from_brief | seller_human_designed | agent_synthesized`. Describes **who renders the rendered asset, and when**:
+- `audio_source` on `audio_hosted`
+- `image_source` on `image`
+- `video_source` on `video_hosted`
+- `item_production_model` on `sponsored_placement` — same enum, applied per catalog item (the multi-output generative case: 1 brief × N catalog items → N rendered creatives)
+
+The two axes don't collapse. A generative DSP that produces ONE rendered image from a brief is `composition_model: deterministic` (the surface serves what it received) + `image_source: seller_pre_rendered_from_brief` (seller produced it from inputs at sync_creatives time). A retail-media surface that runs an AI synthesis pipeline per catalog item is `composition_model: deterministic` + `item_production_model: agent_synthesized`. Google PMax is `composition_model: algorithmic` + (production-source unspecified — buyer ships a pool of pre-rendered assets so the production-source question doesn't apply at the format level).
+
+The production-source enums are informational, not the binding contract. The format's `slots` declaration is the contract — what the buyer ships, in what shape. The `*_source` field tells the buyer "here's how this product produces the rendered creative" so they can pick products whose production model fits their workflow (in-house pre-rendered vs upstream creative agent vs seller-driven generative).
+
+### Tracker assembly under seller-rendered sources
+
+When `*_source` is `buyer_uploaded`, the buyer ships rendered assets and any tracker URLs attached to those assets are buyer-controlled (universal_macros for impression/click; `vast_tracker` / `daast_tracker` assets for decomposed VAST/DAAST trackers). When `*_source` is any of the seller-rendered values (`seller_pre_rendered_from_brief`, `seller_human_designed`, `agent_synthesized`) or `publisher_host_recorded`, the buyer never sees the rendered artifact directly. Two normative paths apply:
+
+- **Macro-substituted tracking (default).** The seller honors AdCP universal_macros at impression time — `{IMPRESSION_TRACKER}`, `{CLICK_TRACKER}`, etc. — and substitutes buyer-supplied tracker URLs (declared on the manifest's optional `landing_page_url` and the buyer's measurement-vendor pixels declared via `tracking_extensions` on the format) into the rendered creative's serving template. The buyer registers their measurement pixels client-side; the seller calls them at serve time. This is the dominant path for image / video / audio production where serving and tracking are decoupled.
+- **Sync-creatives tracker block.** For products where the seller produces a serving artifact that embeds tracker URLs directly (e.g., a generated VAST tag or a stitched companion banner), the seller's `sync_creatives` response SHOULD include a `tracker_block` field listing the impression URL pattern and click URL pattern. Buyers register those with their measurement vendor at sync time. This path covers the generative-DSP pattern where the serving artifact and the tracking shape are produced together.
+
+`vast_tracker` and `daast_tracker` decomposed tracker assets work for both `buyer_uploaded` and seller-rendered sources — when the seller renders, those tracker assets are inputs to the rendered tag, attached to the appropriate VAST/DAAST `` block at production time. When the buyer ships a complete `vast` or `daast` tag, the trackers travel inside the tag.
+
+## Custom formats — shapes the 11 canonicals don't cover
+
+The 11 canonicals cover atomic creative shapes (one image, one video, one display tag, one carousel, one catalog placement, one AI-surface mention). They don't cover composed / coordinated / sponsorship shapes that high-end publishers and broadcast networks sell as headline products: multi-placement takeover, roadblock, branded content, cross-screen sponsorship, sponsorship lockup, newsletter sponsorship, AR lens, playable, live event sponsorship.
+
+These shapes are real ad-industry product types — but they're either multi-canonical compositions (takeover = image + video + display_tag + lockup, sold as a unit) or genuinely novel structures (branded content's editorial-sponsorship production model isn't a composition of the 11). v2 handles them via a structured custom mechanism that buyer agents can reason about, NOT via free-form `ext`.
+
+### The mechanism
+
+```json test=false
+{
+ "format_options": [
+ {
+ "format_kind": "custom",
+ "format_shape": "multi_placement_takeover",
+ "format_schema": {
+ "uri": "https://nytimes.adcp/schemas/formats/homepage_takeover_v3",
+ "digest": "sha256:e1d4f6a9c2b5e8d1f4a7c0b3e6d9f2a5c8b1e4d7f0a3c6b9e2d5f8a1c4b7e0a3"
+ },
+ "capability_id": "nytimes_homepage_takeover_premium",
+ "applies_to_channels": ["display", "olv"],
+ "params": {
+ "components": [
+ { "placement_type": "homepage_skin", "required": true },
+ { "placement_type": "preroll_video", "required": true },
+ { "placement_type": "sponsorship_lockup", "required": true }
+ ],
+ "exclusivity_window_hours": 24
+ }
+ }
+ ]
+}
+```
+
+Three required pieces when `format_kind: "custom"`:
+
+1. **`format_shape`** — recognized global pattern from the [format-shape vocabulary registry](https://adcontextprotocol.org/schemas/v3/core/format-shape-vocabulary.json). Tells buyer agents what kind of pattern they're looking at (`multi_placement_takeover`, `branded_content`, `ar_lens`, etc.). The registry currently lists 9 shapes; non-canonical values are valid (validators MAY soft-warn) so adopters CAN ship a shape that isn't yet in the registry — adding entries is a vocabulary PR, not a major-version bump.
+2. **`format_schema`** — URI+digest reference to a fetchable schema describing the shape's actual `params` and `slots`. **Same hosting model as `platform_extensions`**: open-ecosystem publishers host the artifact at the canonical URI on their subdomain; closed-platform / walled-garden shapes resolve through the AAO mirror at `https://mirror.adcontextprotocol.org/translated/...`. Buyer agents fetch by `uri@digest` (immutable per digest, aggressive caching), validate `params` and `slots` against the fetched schema, and reason about manifests structurally.
+3. **`params`** — the actual structure, governed by the schema fetched from `format_schema.uri`. AdCP doesn't bake the params shape; the seller's schema does.
+
+### Why custom + format_schema instead of `ext`
+
+A buyer agent calling `get_products` and seeing a format with interesting structure buried in `ext` has no spec-level definition to reason against. There's no schema, no required fields, no defined semantics — the agent can see the blob but can't interpret it reliably. A human has to step in to evaluate whether the format fits the campaign brief, what assets are needed, how it tracks, what the impression contract is, whether the price makes sense.
+
+That breaks the load-bearing claim of v2: **buyer agents can reason structurally without per-seller integration code.** ext-only puts interesting structure in a free-form bag, regressing to human-in-the-loop. Custom + `format_shape` + `format_schema` keeps the agentic-first contract: the shape has a registered classifier, the structure has a fetchable schema, the buyer agent reasons over both. Same caching mechanics buyer agents already have for `platform_extensions`.
+
+`ext` remains for genuinely experimental shapes that don't even fit a `format_shape` entry yet — but that's the rare case, not the default. The dominant path for novel shapes is custom + format_shape + format_schema.
+
+### Promotion to canonical
+
+A `format_shape` entry is promoted to a first-class `format_kind` when:
+1. At least 2 production adopters ship it via custom + format_schema
+2. 90 consecutive days without a breaking change to the shape adopters converged on
+3. The shape has a defined tracking model (which signals fire, which trackers attach, what the impression contract is)
+4. The working group opens a per-canonical promotion issue, drafts a canonical schema (`/schemas/formats/canonical/.json`), lands a fixture, and ships in the next minor release
+
+Same governance pattern that produced the 11 canonicals from the v1 audit. The promotion queue lives at [adcp#3666](https://github.com/adcontextprotocol/adcp/issues/3666); current candidates are the 9 entries in the format-shape registry.
+
+## Asset group vocabulary
+
+Format `slots` reference canonical `asset_group_id` values from the [vocabulary registry](https://adcontextprotocol.org/schemas/v3/core/asset-group-vocabulary.json). The current canonical entries:
+
+| asset_group_id | asset_type | Common aliases (v1 → v2) |
+|---|---|---|
+| `headlines` | text | headline, title, tagline, headline_text |
+| `long_headlines` | text | long_headline_pool, extended_headlines |
+| `descriptions` | text | description, body, body_text, text, content |
+| `images_landscape` | image | image, hero_image, landscape_image, banner_image |
+| `images_vertical` | image | vertical_image, story_image, portrait_image |
+| `images_square` | image | square_image, feed_image |
+| `image_main` | image | (per-canonical default for `image`) |
+| `logo` | image | brand_logo, logo_image |
+| `video` | video | video_file, hero_video, video_asset, video_main |
+| `video_main` | video | (per-canonical default for `video_hosted`) |
+| `video_vertical` / `video_horizontal` | video | — |
+| `audio` / `audio_main` | audio | audio_file, hero_audio, audio_asset |
+| `companion_image` / `companion_banner` | image | — |
+| `brand_name` / `body_text` | text | — |
+| `cards` | object | carousel_cards, slides, carousel_items, carousel_slides |
+| `cta` | text | cta_text, call_to_action, action_text, button_text |
+| `price` / `phone_number` / `promo_code` / `disclaimer` | text | (various) |
+| `subtitle_file` | url | caption_file, captions, subtitles |
+| `landing_page_url` | url | click_url, link, final_url, link_url, click_through_url |
+| `privacy_policy_url` | url | — |
+| `source_catalog` | catalog | (sponsored_placement) |
+| `hero_asset` | image | hero_banner, collection_hero |
+| `script` | text | script_text, host_script, voiceover_script |
+| `creative_brief` | brief | brief, creative_direction, talking_points |
+| `scenes` | object | storyboard |
+| `voice_id` / `offering_ref` / `youtube_video_id` / `pin_id` | text | — |
+| `style_reference` | image | reference_image, style_image, inspiration_image |
+| `starter_assets` | object | — |
+| `vast_tag` | vast | (video_vast default) |
+| `daast_tag` | daast | (audio_daast default) |
+| `tag_url` | url | (display_tag default) |
+| `html5_bundle` | zip | (html5 default) |
+| `backup_image` | image | (html5 / display_tag default) |
+
+Non-canonical `asset_group_id` values remain valid for platform-specific extensions; validators MAY emit soft warnings on non-canonical IDs to encourage convergence. Aliases are recognized one-way (v1 alias → v2 canonical) when migrating; new manifests SHOULD use the canonical IDs.
+
+## Worked example — Meta Reels
+
+Meta Reels narrows `video_hosted` (vertical orientation) with Meta-specific parameters and platform extensions:
+
+```json test=false
+{
+ "product_id": "meta_reels_us",
+ "name": "Meta Reels — United States",
+ "publisher_properties": [
+ { "publisher_domain": "meta.com", "selection_type": "all" }
+ ],
+ "channels": ["social"],
+ "format_options": [
+ {
+ "format_kind": "video_hosted",
+ "params": {
+ "orientation": "vertical",
+ "aspect_ratio": "9:16",
+ "duration_ms_range": [3000, 90000],
+ "min_width": 1080,
+ "min_height": 1920,
+ "max_file_size_mb": 200,
+ "video_codecs": ["h264"],
+ "audio_codecs": ["aac"],
+ "headline_max_chars": 25,
+ "primary_text_max_chars": 72,
+ "captions": "recommended",
+ "cta_values": ["LEARN_MORE", "SHOP_NOW", "DOWNLOAD", "SIGN_UP"],
+ "composition_model": "deterministic",
+ "platform_extensions": [
+ {
+ "uri": "https://meta.adcp/extensions/meta_pixel",
+ "digest": "sha256:a3f5b7c9d8e2f1a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2"
+ },
+ {
+ "uri": "https://meta.adcp/extensions/meta_placements_reels",
+ "digest": "sha256:b8e1d4f6a9c2b5e8d1f4a7c0b3e6d9f2a5c8b1e4d7f0a3c6b9e2d5f8a1c4b7e0"
+ }
+ ]
+ }
+ }
+ ],
+ "pricing_options": [
+ { "pricing_option_id": "cpm_floor", "pricing_model": "cpm", "currency": "USD", "fixed_price": 5.50 }
+ ]
+}
+```
+
+Buyer's manifest validates against canonical `video_hosted` first (does it satisfy the contract any seller speaking that canonical accepts?), then narrows against this product's specific parameters. The Meta-specific extensions (pixel, placements) are bundled in the `get_products` response under an `extensions` map keyed by `uri@digest` — buyers fetch them once and cache by digest.
+
+## Worked example — IAB MREC (300×250)
+
+The canonical-as-contract value is clearest for IAB-standard formats. NYTimes and any other publisher selling MREC narrow `image` with the same IAB-standard parameters; buyers validate against canonical `image` *before* knowing which publisher wins.
+
+```json test=false
+{
+ "product_id": "nytimes_homepage_mrec",
+ "name": "NYTimes.com Homepage MREC (300×250)",
+ "publisher_properties": [
+ { "publisher_domain": "nytimes.com", "selection_type": "all" }
+ ],
+ "channels": ["display"],
+ "format_options": [
+ {
+ "format_kind": "image",
+ "params": {
+ "width": 300,
+ "height": 250,
+ "max_file_size_kb": 200,
+ "image_formats": ["jpg", "png", "gif"],
+ "ssl_required": true,
+ "cta_values": ["LEARN_MORE", "SHOP_NOW", "GET_OFFER"],
+ "composition_model": "deterministic",
+ "platform_extensions": [
+ {
+ "uri": "https://nytimes.adcp/extensions/nytimes_om_strict",
+ "digest": "sha256:c9d2f5b8e1a4c7b0e3d6f9a2c5b8e1d4f7a0c3b6e9d2f5a8c1b4e7d0f3a6c9b2"
+ }
+ ]
+ }
+ }
+ ],
+ "pricing_options": [
+ { "pricing_option_id": "cpm_homepage", "pricing_model": "cpm", "currency": "USD", "fixed_price": 22.00 }
+ ]
+}
+```
+
+For HTML5 banners on the same placement, NYTimes publishes a *separate* product narrowing `html5`:
+
+```json test=false
+{
+ "product_id": "nytimes_homepage_html5",
+ "name": "NYTimes.com Homepage HTML5 Banner (300×250)",
+ "publisher_properties": [
+ { "publisher_domain": "nytimes.com", "selection_type": "all" }
+ ],
+ "channels": ["display"],
+ "format_options": [
+ {
+ "format_kind": "html5",
+ "params": {
+ "width": 300,
+ "height": 250,
+ "max_initial_load_kb": 200,
+ "max_polite_load_kb": 500,
+ "host_initiated_subload": true,
+ "max_animation_duration_ms": 30000,
+ "max_cpu_load_percent": 30,
+ "om_sdk_required": true,
+ "clicktag_macro": "clickTag",
+ "backup_image_required": true,
+ "backup_image_max_size_kb": 50,
+ "ssl_required": true,
+ "composition_model": "deterministic"
+ }
+ }
+ ],
+ "pricing_options": [
+ { "pricing_option_id": "cpm_homepage_html5", "pricing_model": "cpm", "currency": "USD", "fixed_price": 28.00 }
+ ]
+}
+```
+
+Different canonical (`html5`, not `image`) because the tracking model is fundamentally different — MRAID + OM-SDK + click-tag rather than impression pixel + click URL. Adopters that want to express "this placement accepts either image or HTML5" publish two products.
+
+## Worked example — Podcast 30s host-read
+
+Host-reads are the host-recorded-from-buyer-script pattern. The product declares `audio_hosted` narrowed to publisher-host-recorded mode with `slots` describing what the buyer ships (a `script` text asset; the publisher's host records audio from it):
+
+```json test=false
+{
+ "product_id": "the_daily_30s_host_read_us",
+ "name": "The Daily — 30s Host-Read Pre-roll (US)",
+ "publisher_properties": [
+ { "publisher_domain": "thedailypod.example", "selection_type": "all" }
+ ],
+ "channels": ["podcast"],
+ "format_options": [
+ {
+ "format_kind": "audio_hosted",
+ "params": {
+ "duration_ms_exact": 30000,
+ "audio_codecs": ["mp3", "aac"],
+ "audio_sample_rates": [44100, 48000],
+ "audio_channels": ["stereo"],
+ "loudness_lufs": -16,
+ "audio_source": "publisher_host_recorded",
+ "buyer_audio_acceptance": "rejected",
+ "composition_model": "deterministic",
+ "slots": [
+ {
+ "asset_group_id": "script",
+ "required": true,
+ "asset_type": "text",
+ "max_chars": 800,
+ "description": "Verbatim script the host reads."
+ },
+ {
+ "asset_group_id": "offering_ref",
+ "required": false,
+ "asset_type": "text"
+ }
+ ],
+ "production_window_business_days": 7
+ }
+ }
+ ],
+ "pricing_options": [
+ { "pricing_option_id": "cpm_host_read", "pricing_model": "cpm", "currency": "USD", "fixed_price": 35.00 }
+ ]
+}
+```
+
+The format declaration tells the buyer everything they need to know — no extra capability lookup. The buyer ships a `script` text asset under that slot in the manifest's `assets` map; brand context comes from the manifest's top-level `brand` BrandRef. There is no separate "inputs" map — everything the buyer ships lives in `assets`. The buyer has two flows depending on whether the seller doubles as a creative agent and whether the buyer wants to pre-produce externally.
+
+### Flow 1 — buyer pre-produces (upstream creative agent)
+
+The buyer calls a creative agent's `build_creative` independently, gets back a rendered manifest, and submits that to the seller. Useful when the buyer has a preferred production partner (their in-house studio, AudioStack-style services) or the seller exposes itself as a creative agent.
+
+1. Buyer reads The Daily's product format → sees `slots: [{ asset_group_id: "script", asset_type: "text", required: true }]` declared
+2. Buyer calls `build_creative({ format: , assets: { script: { asset_type: "text", content: "..." } }, brand: { domain: "..." } })` on a creative agent — this could be The Daily's own creative-agent surface (if they expose one), or any other agent that declares it can produce this format via its `creative.supported_formats` on `get_adcp_capabilities`
+3. Receives a rendered manifest with audio asset
+4. Submits the rendered manifest via `sync_creatives` to The Daily's sales agent
+
+### Flow 2 — seller produces internally
+
+The buyer submits assets directly to the seller; the seller produces internally (calls its own creative team or an upstream creative agent under the hood) and returns a registered creative.
+
+1. Buyer reads the same product format
+2. Buyer submits via `sync_creatives` with the assets in the manifest (e.g., a `script` text asset under that slot in the `assets` map)
+3. Seller produces internally; how is invisible to the buyer
+4. Returns async status; buyer polls or waits for completion
+
+The format's `audio_source: "publisher_host_recorded"` + `buyer_audio_acceptance: "rejected"` tells the buyer which flows are accepted. For The Daily's host-read, both flows are valid because the publisher's host needs to be the producer in either case — the difference is whether the buyer drives the build call or the seller drives it. Other products might accept Flow 1 only (buyer must pre-produce) or Flow 2 only.
+
+For brief-driven (talking-points-style) host-reads, the same shape applies with a `creative_brief` slot (asset_type `brief`) in place of the `script` slot. Same target format (`audio_hosted`); different slot declaration.
+
+## Worked example — third-party creative agent (Flashtalking + NYTimes display)
+
+The host-read example above is single-actor by necessity: the publisher's host has to be the producer. The opposite case is the multi-actor display path, where the buyer chooses a third-party creative agent independently and ships the produced manifest to the seller. The seller does **not** compose creatives — it just accepts canonical-conformant manifests.
+
+Three actors:
+
+- **Buyer** (Acme DSP) — discovers products, picks a creative agent (out-of-band: brand-side relationships, AAO registry, direct knowledge), submits manifests
+- **Sales agent** (NYTimes) — sells the placement, validates manifests against the canonical its product narrows, doesn't compose creatives, doesn't maintain a list of "approved creative agents" in v2
+- **Creative agent** (Flashtalking) — produces creatives via `build_creative`, declares its own producible catalog via `creative.supported_formats` on its OWN `get_adcp_capabilities`
+
+The buyer chooses the creative agent independently of the seller. Sellers do not declare a list of creative agents in v2 — the v1 `creative_agents[]` recursive-discovery hint on `list_creative_formats` is part of the deprecated v1 surface. Buyers reason about creative-agent ↔ seller-product compatibility client-side: "Flashtalking can produce `image` 300×250 ≤200KB; NYTimes accepts `image` 300×250 ≤200KB; they're compatible."
+
+### 1. Buyer reads NYTimes products
+
+Buyer calls `get_products` on NYTimes. The MREC product narrows canonical `image`:
+
+```json test=false
+{
+ "product_id": "nytimes_homepage_mrec",
+ "format_options": [
+ {
+ "format_kind": "image",
+ "params": { "width": 300, "height": 250, "max_file_size_kb": 200, "ssl_required": true }
+ }
+ ]
+}
+```
+
+The product narrows the canonical; the canonical is what NYTimes commits to validating against. NYTimes does NOT validate against Flashtalking's narrowing — buyers don't need to know which creative agent produced the manifest, and Flashtalking-specific parameters (e.g., a Flashtalking placement ID) live in Flashtalking's platform extensions if at all.
+
+### 2. Buyer calls Flashtalking's `build_creative`
+
+```json test=false
+// POST https://flashtalking.adcp/build_creative
+{
+ "format": {
+ "format_kind": "image",
+ "params": { "width": 300, "height": 250 }
+ },
+ "brand": { "domain": "acme.example" },
+ "assets": {
+ "creative_brief": { "asset_type": "brief", "content": "Spring sale, 50% off, blue background, urgent CTA." },
+ "landing_page_url": { "asset_type": "url", "url": "https://acme.example/spring" }
+ }
+}
+```
+
+Flashtalking renders an MREC PNG, returns a manifest with the produced asset:
+
+```json test=false
+{
+ "creative_id": "ft_mrec_88299",
+ "manifest": {
+ "format_id": { "agent_url": "https://flashtalking.adcp", "id": "image_300x250" },
+ "assets": {
+ "image": { "asset_type": "image", "url": "https://cdn.flashtalking.com/ft_mrec_88299.png", "width": 300, "height": 250 }
+ }
+ }
+}
+```
+
+### 3. Buyer ships to NYTimes
+
+Buyer calls `sync_creatives` on NYTimes with the manifest from Flashtalking. NYTimes:
+
+1. Validates the manifest against canonical `image` (300×250, ≤200KB, SSL).
+2. Validates against the product's narrowing (matches — same params).
+3. Does NOT validate against Flashtalking's narrowing — that's the creative agent's contract with the buyer, not the seller's contract.
+4. If valid → creative registered. If not → returns canonical violations (`width` mismatch, `max_file_size_kb` exceeded).
+
+The seller's validation contract is the canonical, not the creative agent. This is what makes the third-party path additive rather than coupled: the buyer can swap creative agents without changing the seller-facing flow.
+
+## Worked example — generative DSP (universalads-class, image_source: seller_pre_rendered_from_brief)
+
+A generative DSP (universalads, Pencil, AdCreative.ai-shaped tools) is a sales agent that ALSO renders creatives inline at `sync_creatives` time — it is NOT a creative agent the buyer calls separately. The buyer ships a brief plus structured copy; the seller renders ONE image and serves it like any deterministic creative.
+
+```json test=false
+{
+ "product_id": "universalads_brief_driven_display_300x250",
+ "name": "Universal Ads — Brief-Driven Display (300×250)",
+ "publisher_properties": [
+ { "publisher_domain": "universalads.example", "selection_type": "all" }
+ ],
+ "channels": ["display"],
+ "format_options": [
+ {
+ "format_kind": "image",
+ "params": {
+ "width": 300,
+ "height": 250,
+ "max_file_size_kb": 200,
+ "image_formats": ["jpg", "png"],
+ "ssl_required": true,
+ "composition_model": "deterministic",
+ "image_source": "seller_pre_rendered_from_brief",
+ "buyer_image_acceptance": "rejected",
+ "production_window_business_days": 0,
+ "slots": [
+ { "asset_group_id": "creative_brief", "asset_type": "brief", "required": true, "max_chars": 500 },
+ { "asset_group_id": "headline", "asset_type": "text", "required": true, "max_chars": 30 },
+ { "asset_group_id": "landing_page_url", "asset_type": "url", "required": true }
+ ]
+ }
+ }
+ ],
+ "delivery_type": "non_guaranteed",
+ "pricing_options": [
+ { "pricing_option_id": "cpm_brief", "pricing_model": "cpm", "currency": "USD", "floor_price": 8.00 }
+ ]
+}
+```
+
+Buyer's manifest carries the brief, headline, and clickthrough URL — no rendered image asset. Seller's `sync_creatives` produces the rendered MREC PNG and registers it. Two axes: `composition_model: deterministic` (the surface serves what it received), `image_source: seller_pre_rendered_from_brief` (the seller renders from inputs at sync time). `buyer_image_acceptance: "rejected"` makes it explicit that the buyer cannot ship a pre-rendered image directly — the production model is brief-driven only.
+
+## Worked example — multi-format product (Flashtalking html5 OR internal display_tag)
+
+A placement that accepts EITHER a third-party-hosted creative OR an internal tag — buyer picks at sync_creatives time by aligning their manifest's `format_kind` (and `capability_id` if needed) to the matching declaration:
+
+```json test=false
+{
+ "product_id": "regional_news_homepage_300x250",
+ "channels": ["display"],
+ "format_options": [
+ {
+ "capability_id": "html5_flashtalking_hosted",
+ "format_kind": "html5",
+ "params": {
+ "width": 300,
+ "height": 250,
+ "max_initial_load_kb": 200,
+ "ssl_required": true,
+ "composition_model": "deterministic"
+ }
+ },
+ {
+ "capability_id": "display_tag_internal",
+ "format_kind": "display_tag",
+ "params": {
+ "width": 300,
+ "height": 250,
+ "ssl_required": true,
+ "composition_model": "deterministic"
+ }
+ }
+ ]
+}
+```
+
+Buyer's manifest: `{ "format_kind": "html5", "capability_id": "html5_flashtalking_hosted", "assets": { "html5_bundle": {...}, "backup_image": {...} } }` — `format_kind` selects the canonical's slot vocabulary; `capability_id` disambiguates because both options share `format_kind: html5`/`display_tag`-shaped slots are different anyway, but `capability_id` removes any ambiguity for the seller's router. (When `format_options` carries one element per `format_kind`, `capability_id` is optional — `format_kind` alone routes the manifest.)
+
+## Worked example — sponsored_placement with item_production_model
+
+A retail-media product that accepts a catalog reference plus a brief, and renders one creative per catalog item at sync time:
+
+```json test=false
+{
+ "product_id": "regional_retailer_generative_offerings",
+ "channels": ["display"],
+ "catalog_types": ["product"],
+ "format_options": [
+ {
+ "format_kind": "sponsored_placement",
+ "params": {
+ "supported_catalog_types": ["product"],
+ "min_items": 5,
+ "max_items": 200,
+ "fanout_mode": "per_item",
+ "supported_id_types": ["sku", "gtin"],
+ "item_production_model": "seller_pre_rendered_from_brief",
+ "composition_model": "deterministic",
+ "slots": [
+ { "asset_group_id": "source_catalog", "asset_type": "catalog", "required": true },
+ { "asset_group_id": "creative_brief", "asset_type": "brief", "required": true, "max_chars": 500 }
+ ]
+ }
+ }
+ ]
+}
+```
+
+`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.
+
+## Validation flow — `validate_input`
+
+Buyers can dry-run a manifest against canonicals and/or specific products without committing to a render:
+
+```json test=false
+{
+ "manifest": {
+ "format_id": { "agent_url": "https://creative.adcontextprotocol.org", "id": "video_30s" },
+ "assets": {
+ "video": {
+ "asset_type": "video",
+ "url": "https://cdn.acme.example/spring-30s.mp4",
+ "duration_ms": 30000,
+ "width": 1080,
+ "height": 1920
+ }
+ },
+ "brand": { "domain": "acme.example" }
+ },
+ "format_ids": ["video_hosted"],
+ "product_ids": ["meta_reels_us"]
+}
+```
+
+Response carries per-target results:
+
+```json test=false
+{
+ "results": [
+ {
+ "target": { "kind": "canonical", "id": "video_hosted" },
+ "ok": true
+ },
+ {
+ "target": { "kind": "product", "id": "meta_reels_us" },
+ "ok": false,
+ "violations": [
+ {
+ "rule": "duration_ms_range",
+ "expected": "3000-90000",
+ "predicted": 30000,
+ "field": "assets.video_main.duration_ms"
+ }
+ ]
+ }
+ ]
+}
+```
+
+`validate_input` is the predictable-case primitive. For genuinely nondeterministic synthesis (Veo / Sora / Runway-class), predictive validation is impossible and the platform's own post-synthesis QA loop applies — submission returns `task_failed` with a `synthesis_failed` reason if the QA loop exhausts without producing a valid artifact. There is **no protocol state for orphaned out-of-spec artifacts**.
+
+### When to use `validate_input`
+
+A decision rule, not a one-size primitive:
+
+- **Pre-flight before an expensive `build_creative` call.** If the manifest can't even narrow against canonical, the buyer saves the synthesis cost. Especially relevant for nondeterministic-synthesis products where each retry has real GPU cost.
+- **Multi-target dry-run during product selection.** A buyer comparing 10 candidate products asks `validate_input` once with all 10 product_ids; gets back per-target results. Cheaper than 10 separate `sync_creatives` round-trips.
+- **Debugging a rejected manifest.** When `sync_creatives` returns violations, calling `validate_input` against the canonical alone narrows the question to "is my manifest fundamentally broken vs is the product's narrowing the gating constraint."
+- **Preview-render gating** (formats with `composition_model: algorithmic` or `synthesis_nondeterministic: true`). The platform's preview surface is a richer follow-on; `validate_input` is the cheap pre-flight that gates whether previewing is even worth attempting.
+
+When NOT to use `validate_input`:
+
+- For a manifest you intend to submit anyway. `sync_creatives` returns the same violations and registers on success — `validate_input` adds a round-trip without reducing total work.
+- For products where the seller's narrowing is unknowable client-side without fetching extensions. `validate_input` pulls extensions same as `sync_creatives` does — there's no discovery shortcut.
+- For high-volume per-impression decisions. `validate_input` is per-target, not per-impression. Operational scale (hundreds of products × N format_options) belongs to client-side filtering against the cached `get_products` response.
+
+### `validate_input` vs `build_creative` vs `sync_creatives`
+
+| Tool | Who calls | What it does | Side effects |
+|---|---|---|---|
+| `validate_input` | Buyer | Dry-run validation against canonicals and/or products. Returns per-target `ok` + violations. | None (no creative registered, no synthesis triggered). |
+| `build_creative` | Buyer (calling a creative agent) | Produces a creative manifest from inputs (brief, scenes, brand). For deterministic flows: one round-trip. For nondeterministic flows: returns task with QA-loop semantics. | Synthesis happens; output manifest is returned. May register a creative on the creative agent's library if the agent supports `has_creative_library`. Does NOT register on the seller. |
+| `sync_creatives` | Buyer (calling a seller) | Submits a manifest to the sales agent for the seller to register against a product. | Validates against canonical + product narrowing; registers creative on the seller's library on success; returns violations on failure. |
+
+For the third-party creative-agent flow: `validate_input` first (cheap pre-flight) → `build_creative` on the creative agent → `sync_creatives` on the sales agent. For the in-house pre-rendered flow: skip `build_creative`; `validate_input` then `sync_creatives`. For the seller-renders-from-brief flow (universalads-class): skip `build_creative` (the seller does the rendering at `sync_creatives` time); `validate_input` then `sync_creatives` directly.
+
+See [`build_creative` task reference](/docs/creative/task-reference/build_creative) for the full request/response shape.
+
+### Discovery + validation at scale
+
+A high-product-count buyer (TTD-class with ~100s of products per get_products response) cannot pre-flight every product via `validate_input` per round — N products × M format_options × per-target round-trips becomes operationally expensive. Two patterns address this:
+
+- **Client-side filtering against the cached `get_products` response.** Buyers who know their manifest's `format_kind` and parameter bucket (canonical, dimensions, duration) filter the product list client-side before validating. The format declarations are already inline on each product — buyers don't need a separate fetch to filter. This is the dominant pattern for "validate against the products that could possibly accept my creative" and reduces the validate_input set by an order of magnitude.
+- **Multi-target `validate_input`.** When the filtered set is still wide (5-50 products), call `validate_input` once with all candidate product_ids in `targets[]`. The response carries per-target results in a single round-trip. Cheaper than per-product calls and structurally aligned with the schema (one request, many results).
+
+For genuinely high-volume scenarios (hundreds of candidate products, real-time bidding pre-flight), buyers should rely on cached `get_products` responses + client-side filtering as the primary path; `validate_input` is reserved for the narrowed candidate set or for debugging unexpected rejections. The `applies_to_channels` field on each format_options element narrows further when a product spans multiple channels.
+
+## Preview as the universal "what does this produce" surface
+
+Buyers ship assets per the format's `slots` declaration; `preview_creative` shows what the output renders as. The seller's response to a creative submission can also include a preview URL — the buyer doesn't need a separate preview call to verify that their submission produced the intended output. Same surface, two production paths:
+
+- **Direct rendering**: buyer ships finished creative assets (image, video, audio) → seller renders them on the placement → preview shows the rendered output (with seller-side composition, overlays, CTA buttons applied).
+- **Seller-side production**: buyer ships content the seller consumes (script text, creative_brief, voice_id selection) → seller produces the rendered asset internally (host recording, generative AI synthesis, transcoding — invisible) → preview shows the produced output.
+
+The buyer can iterate on shipped assets and inspect previews before committing to a buy. Different sellers may produce differently internally; the preview surface is uniform. This is what makes "production mechanism is invisible to the buyer" workable in practice — the buyer doesn't need to know HOW the output was produced because they can see WHAT was produced.
+
+## Brand identity via brand.json (with override)
+
+v2 formats no longer redeclare `brand_logo`, `brand_colors`, `brand_voice`, `brand_tagline` as explicit slots. When a manifest carries a [`BrandRef`](https://adcontextprotocol.org/schemas/v3/core/brand-ref.json) like `brand: { domain: "acme.example" }` (or with `brand_id` for house-of-brands), the seller fetches `https://acme.example/.well-known/brand.json` for brand context.
+
+For the case where brand.json is missing or stale, the manifest includes `brand_kit_override`:
+
+```json test=false
+{
+ "format_id": { "agent_url": "https://creative.adcontextprotocol.org", "id": "image_300x250" },
+ "assets": {
+ "image_main": { "asset_type": "image", "url": "https://cdn.acme.example/banner.jpg", "width": 300, "height": 250 }
+ },
+ "brand": { "domain": "acme.example" },
+ "brand_kit_override": {
+ "logo": { "asset_type": "image", "url": "https://cdn.acme.example/logo-2026.png", "width": 200, "height": 100 },
+ "colors": { "primary": "#0066CC", "accent": "#FF6600" },
+ "tagline": "Spring savings, all season"
+ }
+}
+```
+
+Override fields take precedence over `brand.json` for that creative.
+
+## Platform extensions — distribution
+
+Platform extensions are narrow, truly platform-specific additions (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). They live at well-known paths on the owning agent:
+
+```
+https://meta.adcp/extensions/meta_pixel
+https://tiktok.adcp/extensions/tiktok_pixel
+https://nytimes.adcp/extensions/nytimes_om_strict
+```
+
+Each extension's response carries the schema, the canonical pattern or slot it extends, a version, and a content digest.
+
+**Hosting paths — two separate flows.** v2 supports two hosting models, depending on whether the canonical URI's owner participates in the open AdCP ecosystem or operates as a closed platform that AAO translates on its behalf.
+
+*Open-ecosystem path (publisher-hosted)*. Used when the publisher owning the URI subdomain participates directly in AdCP — independent publishers, SSPs, retail-media networks running their own canonical extensions. The publisher hosts the artifact at the canonical URI on their subdomain. Because URIs are digest-pinned (`uri@sha256:…`), responses are immutable per digest — publishers SHOULD serve them with `Cache-Control: public, max-age=31536000, immutable` and target ≥99.9% / 30-day availability. SDKs cache aggressively by `uri@digest`; a hit is always correct. On 404 or resolution failure, buyers MUST degrade gracefully (treat as unavailable, skip platform-specific narrowing, don't fail the buy).
+
+*Closed-platform path (AAO-translated)*. Used for walled gardens (Meta, Google, Amazon, TikTok, Snap, Pinterest). These platforms are unlikely to host AdCP-shaped extension artifacts on their own subdomains (they have native SDKs and APIs that protect their revenue model; serving an immutable extension CDN gives them no benefit). Instead, AAO runs a translator that maps closed-platform format documentation into AdCP extension artifacts and hosts them under an AAO mirror namespace (e.g., `https://mirror.adcontextprotocol.org/translated//@`). Worked-example fixtures in this repo that reference `https://meta.adcp/extensions/...` are illustrative — production usage of those extensions should resolve through the AAO mirror until/unless Meta participates directly. AAO commits to the same digest-pinning + immutability contract; refresh cadence and translation methodology are documented at `https://adcontextprotocol.org/registry/translated-extensions`. Buyers cache and resolve identically across both paths — `uri@digest` is the cache key, regardless of who hosts.
+
+The two paths share the digest-pinned cache and graceful-degradation semantics. They differ only in the resolution authority. The mirror is normative for closed-platform extensions (not "best effort") because there is no other path; for open-ecosystem extensions, the mirror is opt-in fallback.
+
+**Distribution path: bundled in `get_products`.** The sales agent's response includes definitions for every extension referenced by any product in the response, keyed by `uri@digest`:
+
+```json test=false
+{
+ "products": [ { "...": "..." } ],
+ "extensions": {
+ "https://meta.adcp/extensions/meta_pixel@sha256:a3f5...": {
+ "extends": "tracking",
+ "fields": {
+ "pixel_id": { "type": "string", "required": true },
+ "conversion_event": { "type": "string", "enum": ["PURCHASE", "LEAD"] }
+ },
+ "version": "2.1.0"
+ }
+ }
+}
+```
+
+Buyer's SDK caches by URI@digest. Subsequent `get_products` responses can reference by digest alone if the buyer has the extension cached. Direct URI fetch is supported for tooling but the primary path is bundled-in-`get_products`.
+
+## What's NOT in v2
+
+By design, v2 doesn't introduce new vocabulary for things AdCP already handles or that belong elsewhere:
+
+- **Brand safety vocabularies** — that's media-buy/campaign-level (`creative-policy.json` and broader campaign settings), not creative-format-level. Format declarations don't redeclare brand safety.
+- **Universal macros as a new schema** — already documented at [`/docs/creative/universal-macros`](/docs/creative/universal-macros). Canonical formats reference them by name.
+- **`destination_kinds` as a new schema** — `url-asset.json` already has `url_type` covering URL kind disambiguation. Platform-specific destinations (Meta `messenger_thread`, etc.) are platform extensions.
+- **`cta_vocabulary` as a canonical pattern** — CTAs vary meaningfully across surfaces; we let products declare `cta_values` arrays inline until cross-platform demand emerges.
+- **`list_build_capabilities` as a separate tool** — folded into `get_adcp_capabilities` under `creative.supported_formats`.
+- **`build_capability`, `build_capability_ref`, and a separate `inputs` map** — collapsed into the canonical `slots` model on the format declaration. The format declares slots (canonical `asset_group_id` + `asset_type` + constraints); the manifest has a single `assets` map keyed by slot name; the seller dispatches per the format (render assets verbatim or consume them for production). The format itself tells the buyer what it requires; how production happens is implementation detail.
+
+### Channels not yet canonicalized
+
+The 11 canonicals cover display, video, audio, retail-media, AI-surface, and responsive-creative archetypes. Several channels have no canonical home yet and stay on the v1 path until 3.2 or later:
+
+- **Native** — deferred to 3.2 pending TemplateCreative + OpenRTB Native 1.2 audit. The `image_carousel` canonical covers carousel-shaped native today; richer native templates are out of scope for 3.1.
+- **Linear / addressable TV** — broadcast-shaped buys (national spot, addressable household targeting) need their own tracking + measurement model that doesn't reduce to `video_vast` or `video_hosted`. Likely a `video_linear_tv` or `video_addressable` canonical in a later release.
+- **OOH / DOOH** — out-of-home and digital-out-of-home placements have impression models tied to physical-location-keyed measurement (Geopath, COMMB) that don't map to web/CTV impression pixels. Deferred until adopter demand surfaces.
+- **Audio dynamic ad insertion (DAI)** — ad-stitched audio with mid-stream insertion has a different tracking shape than `audio_hosted` or `audio_daast`. Likely covered by a specialized canonical or by `audio_daast` extension parameters when the pattern stabilizes.
+- **In-game** — playable / in-game ads have a SDK-specific composition model. Out of scope until cross-engine standards land.
+- **Live streaming** — live linear video (Twitch / YouTube Live / sports streaming with mid-roll) needs concurrent-impression and stream-state tracking. The `video_vast` canonical handles VAST-tag-driven live insertion today; richer live patterns deferred.
+
+For these channels, sellers continue to use v1 `format_ids` and adopt v2 incrementally per canonical as each one stabilizes.
+
+### Generative-DSP and multi-output patterns are forward-looking
+
+The `*_source` enums (including `seller_pre_rendered_from_brief` and `agent_synthesized`) and `item_production_model` on `sponsored_placement` are designed for generative-DSP and AI-rendered retail-media patterns that are emerging but not yet a large share of programmatic spend in 2026. Universalads-shaped tools, Pencil, AdCreative.ai, GenStudio-shaped tools — these are real adopters, but the volume is small relative to the boring 90% (buyer ships an MREC PNG; surface serves it). Reading too much into the schema breadth is a mistake. The fields exist so generative-DSP adopters have a clean v2 home; the worked examples include them so adopters can map their adapter cleanly. They are not a signal that the v2 narrative is AI-first. The dominant flows for 3.1 are still buyer-uploaded assets going through deterministic surfaces.
+
+### Creative-agent business model
+
+The third-party-creative-agent worked example assumes Flashtalking-shaped tools serve buyers via `build_creative` and let the buyer ship the produced manifest to the seller. Operators reading this should not infer that v2 strips creative agents of their hosting / serving / tracking revenue. Production happens at `build_creative`; the produced manifest can include hosted asset URLs on the creative agent's CDN (Flashtalking-hosted asset URLs in the example), and platform extensions can attach creative-agent-specific tracking (Flashtalking pixel IDs, viewability vendor configurations) that the seller honors at serve time. The v2 disaggregation is conceptual (the spec separates production from serving from tracking) — the operational integration path lets creative agents continue to host and instrument their produced creatives. v2 doesn't dictate where the asset bytes live or whose tracking JS runs; it only formalizes the production-vs-serving boundary that already exists implicitly.
+
+## Migration
+
+| Adopter | Cost | Realistic timeline |
+|---|---|---|
+| DSP buyer agents | Low | 3.1-3.2 |
+| SSP/sales agents | Medium-high | 3.3-4.0 |
+| Walled gardens (Meta, Google, Amazon, TikTok, Snap, Pinterest) | High, low motivation | 4.0-5.0 if at all (gated on AAO providing a translator from existing format docs) |
+| Creative agents (AudioStack-shaped) | Low, high motivation | 3.1-3.2 |
+| Publisher direct (GAM/prebid path) | Medium | Blocked on native canonical pre-audit |
+
+**v1 stays first-class.** v1 named formats remain supported; sellers SHOULD provide server-side flatten wrappers that derive the v1 `list_creative_formats` shape from v2 product format declarations through 4.0. v2 is the *new* path, not the only path.
+
+## Phase status
+
+| Phase | Status | What's in it |
+|---|---|---|
+| Phase 1 | ✅ in #3307 | `asset_group_id` vocabulary registry (canonical entries + audit-grounded aliases), `scenes` schema, `zip` asset type, video/audio doc fixes |
+| Phase 2 | ✅ in #3307 | 11 canonical format definitions with structured `slots` declaration, `ProductFormatDeclaration` (format_kind discriminator + params), `validate_input` primitive, `creative.supported_formats` on get_adcp_capabilities, `brand_kit_override`, `platform-extension-ref`, typed inline `product_card` / `product_card_detailed`, `format_ids` ⊕ `format_options` oneOf on Product |
+| Phase 3 | ✅ in #3307 | v1↔v2 migration guide, 12 fully-validated reference Product fixtures + 1 get_products response fixture with bundled extensions, fixture-validation test suite (`npm run test:v2-fixtures`) |
+| Phase 4 | ⚠️ blocking adoption | Reference SDK codegen (TypeScript first, then Python), server-side flatten wrapper reference implementation. Without Phase 4, adopters cannot consume v2 cleanly — the typed-tagged-union ergonomics this PR's design earns require codegen to deliver. v2 is opt-in and additive at the schema layer today; Phase 4 makes it usable. |
+| Native canonical | TBD | Deferred to 3.2 after TemplateCreative + OpenRTB Native 1.2 audit |
+
+## Related
+
+- [v1 → v2 migration guide](/docs/creative/v2-migration) — concrete migration paths for sellers, creative agents, buyers, and publisher-direct integrations
+- [RFC #3305](https://github.com/adcontextprotocol/adcp/issues/3305) — v2 architecture decisions and rationale
+- [PR #3307](https://github.com/adcontextprotocol/adcp/pull/3307) — Phase 1 + Phase 2 implementation, on hold pending 3.1.0 beta cycle
+- [Asset group vocabulary](https://adcontextprotocol.org/schemas/v3/core/asset-group-vocabulary.json) — canonical slot-name registry
+- [Scenes schema](https://adcontextprotocol.org/schemas/v3/creative/scenes.json) — typed scene-by-scene structure for build_creative
+- [Universal macros](/docs/creative/universal-macros) — substitution patterns referenced from canonical tracking
diff --git a/package.json b/package.json
index 1594617351..3ebbac0370 100644
--- a/package.json
+++ b/package.json
@@ -26,6 +26,7 @@
"test:extension-schemas": "node tests/extension-schemas.test.cjs",
"test:snippets": "node tests/snippet-validation.test.cjs",
"test:json-schema": "node tests/json-schema-validation.test.cjs",
+ "test:v2-fixtures": "node tests/v2-fixture-validation.test.cjs",
"test:error-handling": "node tests/check-error-handling.cjs",
"test:composed": "node tests/composed-schema-validation.test.cjs",
"test:migrations": "node tests/migration-validation.test.cjs",
diff --git a/static/examples/get_products_responses/v2/meta_with_bundled_extensions.json b/static/examples/get_products_responses/v2/meta_with_bundled_extensions.json
new file mode 100644
index 0000000000..28f1034feb
--- /dev/null
+++ b/static/examples/get_products_responses/v2/meta_with_bundled_extensions.json
@@ -0,0 +1,253 @@
+{
+ "$schema": "/schemas/media-buy/get-products-response.json",
+ "products": [
+ {
+ "product_id": "meta_reels_us",
+ "name": "Meta Reels — United States",
+ "description": "9:16 vertical short-form video on Meta Reels (Facebook + Instagram). Buyers upload H.264 mp4 (3-90s) plus headline and primary text; Meta serves to Reels feeds with placement-native UI overlays and Meta-specific tracking via the meta_pixel extension.",
+ "publisher_properties": [
+ {
+ "publisher_domain": "meta.com",
+ "selection_type": "all"
+ }
+ ],
+ "channels": [
+ "social"
+ ],
+ "delivery_type": "non_guaranteed",
+ "pricing_options": [
+ {
+ "pricing_option_id": "cpm_floor",
+ "pricing_model": "cpm",
+ "currency": "USD",
+ "floor_price": 5.5
+ }
+ ],
+ "reporting_capabilities": {
+ "available_reporting_frequencies": [
+ "hourly",
+ "daily"
+ ],
+ "expected_delay_minutes": 60,
+ "timezone": "America/Los_Angeles",
+ "supports_webhooks": true,
+ "available_metrics": [
+ "impressions",
+ "clicks",
+ "spend",
+ "completion_rate",
+ "viewability"
+ ],
+ "date_range_support": "date_range"
+ },
+ "format_options": [
+ {
+ "format_kind": "video_hosted",
+ "params": {
+ "orientation": "vertical",
+ "aspect_ratio": "9:16",
+ "duration_ms_range": [
+ 3000,
+ 90000
+ ],
+ "min_width": 1080,
+ "min_height": 1920,
+ "max_file_size_mb": 200,
+ "video_codecs": [
+ "h264"
+ ],
+ "audio_codecs": [
+ "aac"
+ ],
+ "containers": [
+ "mp4"
+ ],
+ "headline_max_chars": 25,
+ "primary_text_max_chars": 72,
+ "captions": "recommended",
+ "cta_values": [
+ "LEARN_MORE",
+ "SHOP_NOW",
+ "DOWNLOAD",
+ "SIGN_UP"
+ ],
+ "composition_model": "deterministic",
+ "platform_extensions": [
+ {
+ "uri": "https://meta.adcp/extensions/meta_pixel",
+ "digest": "sha256:a3f5b7c9d8e2f1a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2"
+ },
+ {
+ "uri": "https://meta.adcp/extensions/meta_placements_reels",
+ "digest": "sha256:b8e1d4f6a9c2b5e8d1f4a7c0b3e6d9f2a5c8b1e4d7f0a3c6b9e2d5f8a1c4b7e0"
+ }
+ ]
+ }
+ }
+ ]
+ },
+ {
+ "product_id": "meta_feed_image_us",
+ "name": "Meta Feed Image — United States",
+ "description": "Static image placement in Meta feed (Facebook + Instagram). 1.91:1, 1:1, or 4:5 aspect ratios accepted. Same Meta-specific tracking as Reels via meta_pixel extension.",
+ "publisher_properties": [
+ {
+ "publisher_domain": "meta.com",
+ "selection_type": "all"
+ }
+ ],
+ "channels": [
+ "social"
+ ],
+ "delivery_type": "non_guaranteed",
+ "pricing_options": [
+ {
+ "pricing_option_id": "cpm_floor",
+ "pricing_model": "cpm",
+ "currency": "USD",
+ "floor_price": 4
+ }
+ ],
+ "reporting_capabilities": {
+ "available_reporting_frequencies": [
+ "hourly",
+ "daily"
+ ],
+ "expected_delay_minutes": 60,
+ "timezone": "America/Los_Angeles",
+ "supports_webhooks": true,
+ "available_metrics": [
+ "impressions",
+ "clicks",
+ "spend",
+ "ctr",
+ "viewability"
+ ],
+ "date_range_support": "date_range"
+ },
+ "format_options": [
+ {
+ "format_kind": "image",
+ "params": {
+ "width": 1080,
+ "height": 1080,
+ "max_file_size_kb": 30000,
+ "image_formats": [
+ "jpg",
+ "png"
+ ],
+ "ssl_required": true,
+ "headline_max_chars": 40,
+ "body_text_max_chars": 125,
+ "cta_values": [
+ "LEARN_MORE",
+ "SHOP_NOW",
+ "DOWNLOAD",
+ "SIGN_UP"
+ ],
+ "composition_model": "deterministic",
+ "platform_extensions": [
+ {
+ "uri": "https://meta.adcp/extensions/meta_pixel",
+ "digest": "sha256:a3f5b7c9d8e2f1a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2"
+ },
+ {
+ "uri": "https://meta.adcp/extensions/meta_placements_feed",
+ "digest": "sha256:c2d4e6f8a0b2c4d6e8f0a2c4d6e8f0a2c4d6e8f0a2c4d6e8f0a2c4d6e8f0a2c4"
+ }
+ ]
+ }
+ }
+ ]
+ }
+ ],
+ "extensions": {
+ "https://meta.adcp/extensions/meta_pixel@sha256:a3f5b7c9d8e2f1a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2": {
+ "extends": "tracking",
+ "version": "2.1.0",
+ "description": "Meta Pixel + Conversions API integration. Required for conversion-event optimization on Meta products.",
+ "fields": {
+ "type": "object",
+ "required": [
+ "pixel_id"
+ ],
+ "properties": {
+ "pixel_id": {
+ "type": "string",
+ "pattern": "^[0-9]{15,16}$",
+ "description": "Meta Pixel ID (15-16 digit numeric)."
+ },
+ "conversion_event": {
+ "type": "string",
+ "enum": [
+ "PURCHASE",
+ "LEAD",
+ "COMPLETE_REGISTRATION",
+ "ADD_TO_CART",
+ "INITIATE_CHECKOUT",
+ "SUBSCRIBE",
+ "VIEW_CONTENT"
+ ],
+ "description": "Standard Meta conversion event the pixel fires on. Custom events use the `custom_event_name` field instead."
+ },
+ "custom_event_name": {
+ "type": "string",
+ "description": "Custom Meta conversion event name. Used when conversion_event is omitted."
+ },
+ "test_event_code": {
+ "type": "string",
+ "description": "Optional Meta Events Manager test event code for sandbox/preview verification."
+ }
+ }
+ }
+ },
+ "https://meta.adcp/extensions/meta_placements_reels@sha256:b8e1d4f6a9c2b5e8d1f4a7c0b3e6d9f2a5c8b1e4d7f0a3c6b9e2d5f8a1c4b7e0": {
+ "extends": "placement",
+ "version": "1.0.0",
+ "description": "Meta-specific placement enum scoped to Reels surfaces (Facebook Reels + Instagram Reels).",
+ "fields": {
+ "type": "object",
+ "properties": {
+ "placement": {
+ "type": "string",
+ "const": "reels",
+ "description": "Discriminator — products extending this surface accept only Reels placement."
+ },
+ "facebook_reels": {
+ "type": "boolean",
+ "default": true,
+ "description": "Whether Facebook Reels delivery is enabled."
+ },
+ "instagram_reels": {
+ "type": "boolean",
+ "default": true,
+ "description": "Whether Instagram Reels delivery is enabled."
+ }
+ }
+ }
+ },
+ "https://meta.adcp/extensions/meta_placements_feed@sha256:c2d4e6f8a0b2c4d6e8f0a2c4d6e8f0a2c4d6e8f0a2c4d6e8f0a2c4d6e8f0a2c4": {
+ "extends": "placement",
+ "version": "1.0.0",
+ "description": "Meta-specific placement enum scoped to feed surfaces (Facebook Feed + Instagram Feed).",
+ "fields": {
+ "type": "object",
+ "properties": {
+ "placement": {
+ "type": "string",
+ "const": "feed",
+ "description": "Discriminator — products extending this surface accept feed placements."
+ },
+ "facebook_feed": {
+ "type": "boolean",
+ "default": true
+ },
+ "instagram_feed": {
+ "type": "boolean",
+ "default": true
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/static/examples/products/v2/amazon_sponsored_products.json b/static/examples/products/v2/amazon_sponsored_products.json
new file mode 100644
index 0000000000..0aade51ac0
--- /dev/null
+++ b/static/examples/products/v2/amazon_sponsored_products.json
@@ -0,0 +1,67 @@
+{
+ "$schema": "/schemas/core/product.json",
+ "product_id": "amazon_sp_search",
+ "name": "Amazon Sponsored Products — Search",
+ "description": "Catalog-driven sponsored product placement in Amazon search results. Buyer supplies a product catalog with ASINs; Amazon's surface composes per-item rendering (product image + title + price + rating + Prime badge) using its native placement template. Composition is deterministic — buyer can predict per-slot rendering from the catalog item structure. No buyer creative slots; the catalog reference is the entire input.",
+ "publisher_properties": [
+ {
+ "publisher_domain": "amazon.com",
+ "selection_type": "all"
+ }
+ ],
+ "channels": [
+ "retail_media"
+ ],
+ "delivery_type": "non_guaranteed",
+ "pricing_options": [
+ {
+ "pricing_option_id": "cpc_auction",
+ "pricing_model": "cpc",
+ "currency": "USD",
+ "floor_price": 0.5
+ }
+ ],
+ "reporting_capabilities": {
+ "available_reporting_frequencies": [
+ "hourly",
+ "daily"
+ ],
+ "expected_delay_minutes": 60,
+ "timezone": "America/Los_Angeles",
+ "supports_webhooks": true,
+ "available_metrics": [
+ "impressions",
+ "clicks",
+ "spend",
+ "ctr",
+ "conversions",
+ "conversion_value",
+ "roas",
+ "new_to_brand_rate"
+ ],
+ "date_range_support": "date_range"
+ },
+ "format_options": [
+ {
+ "format_kind": "sponsored_placement",
+ "params": {
+ "supported_catalog_types": [
+ "product"
+ ],
+ "min_items": 1,
+ "max_items": 50,
+ "fanout_mode": "per_item",
+ "required_catalog_fields": [
+ "title",
+ "image_url",
+ "price"
+ ],
+ "supported_id_types": [
+ "asin"
+ ],
+ "hero_asset_supported": false,
+ "composition_model": "deterministic"
+ }
+ }
+ ]
+}
diff --git a/static/examples/products/v2/chatgpt_brand_mention.json b/static/examples/products/v2/chatgpt_brand_mention.json
new file mode 100644
index 0000000000..41e941b242
--- /dev/null
+++ b/static/examples/products/v2/chatgpt_brand_mention.json
@@ -0,0 +1,77 @@
+{
+ "$schema": "/schemas/core/product.json",
+ "product_id": "openai_chatgpt_sponsored_mention_us",
+ "name": "ChatGPT Sponsored Brand Mention — United States",
+ "description": "AI-surface sponsored placement on ChatGPT. Buyer supplies a BrandRef (resolving brand.json for context) plus optional offering reference; ChatGPT composes a natural-language sponsored mention within its response to a relevant user query. Composition is algorithmic — the agent chooses phrasing and presentation, with disclosure required and no buyer-fixed creative. Distinct from si_chat (which is the user-converses-with-brand's-agent pattern, brand-owned conversational surface). Parallels sponsored_placement structurally (both surface-composed) but for AI/agentic surfaces rather than retail-media catalog.",
+ "publisher_properties": [
+ {
+ "publisher_domain": "openai.com",
+ "selection_type": "all"
+ }
+ ],
+ "channels": [
+ "sponsored_intelligence"
+ ],
+ "delivery_type": "non_guaranteed",
+ "pricing_options": [
+ {
+ "pricing_option_id": "cpm_mention",
+ "pricing_model": "cpm",
+ "currency": "USD",
+ "floor_price": 18
+ }
+ ],
+ "reporting_capabilities": {
+ "available_reporting_frequencies": [
+ "daily"
+ ],
+ "expected_delay_minutes": 1440,
+ "timezone": "UTC",
+ "supports_webhooks": false,
+ "available_metrics": [
+ "impressions",
+ "clicks",
+ "spend",
+ "ctr",
+ "engagement_rate"
+ ],
+ "date_range_support": "date_range"
+ },
+ "format_options": [
+ {
+ "format_kind": "agent_placement",
+ "params": {
+ "output_modality": "text",
+ "max_mention_length_chars": 280,
+ "supports_offering_reference": true,
+ "supports_landing_page_url": true,
+ "tone_constraints": [
+ "factual",
+ "no_superlatives"
+ ],
+ "disclosure_required": true,
+ "composition_model": "algorithmic",
+ "slots": [
+ {
+ "asset_group_id": "offering_ref",
+ "required": false,
+ "asset_type": "text",
+ "description": "Optional offering identifier to focus the mention on a specific product, service, or campaign within the brand."
+ },
+ {
+ "asset_group_id": "landing_page_url",
+ "required": false,
+ "asset_type": "url",
+ "description": "Optional URL the surface MAY attach to mentions as a citation or learn-more link."
+ }
+ ],
+ "platform_extensions": [
+ {
+ "uri": "https://openai.adcp/extensions/chatgpt_response_card",
+ "digest": "sha256:f3a6c9b2e5d8f1a4c7b0e3d6f9a2c5b8e1d4f7a0c3b6e9d2f5a8c1b4e7d0f3a6"
+ }
+ ]
+ }
+ }
+ ]
+}
diff --git a/static/examples/products/v2/gam_3p_display_tag.json b/static/examples/products/v2/gam_3p_display_tag.json
new file mode 100644
index 0000000000..42c80ea179
--- /dev/null
+++ b/static/examples/products/v2/gam_3p_display_tag.json
@@ -0,0 +1,79 @@
+{
+ "$schema": "/schemas/core/product.json",
+ "product_id": "gam_publisher_3p_display_tag_300x250",
+ "name": "GAM Publisher — 3P Display Tag (300×250)",
+ "description": "Third-party-served display tag (JS or iframe) on a GAM-managed publisher placement. Buyer's adserver hosts the creative; the publisher calls the tag URL at impression time. 200KB max-snippet-size and a runtime allowlist (no eval, no document.write, no setTimeout, no javascript: / data: URLs in click trackers) apply at the GAM level — these are publisher-policy constraints, not protocol-level.",
+ "publisher_properties": [
+ {
+ "publisher_domain": "examplepublisher.example",
+ "selection_type": "all"
+ }
+ ],
+ "channels": [
+ "display"
+ ],
+ "delivery_type": "non_guaranteed",
+ "pricing_options": [
+ {
+ "pricing_option_id": "cpm_floor",
+ "pricing_model": "cpm",
+ "currency": "USD",
+ "floor_price": 1.5
+ }
+ ],
+ "reporting_capabilities": {
+ "available_reporting_frequencies": [
+ "daily"
+ ],
+ "expected_delay_minutes": 240,
+ "timezone": "UTC",
+ "supports_webhooks": false,
+ "available_metrics": [
+ "impressions",
+ "clicks",
+ "spend",
+ "ctr",
+ "viewability"
+ ],
+ "date_range_support": "date_range"
+ },
+ "format_options": [
+ {
+ "format_kind": "display_tag",
+ "params": {
+ "width": 300,
+ "height": 250,
+ "supported_tag_types": [
+ "iframe",
+ "javascript",
+ "1x1_redirect"
+ ],
+ "ssl_required": true,
+ "max_redirect_depth": 4,
+ "max_response_time_ms": 1500,
+ "backup_image_required": true,
+ "backup_image_max_size_kb": 50,
+ "om_sdk_required": false,
+ "composition_model": "deterministic",
+ "slots": [
+ {
+ "asset_group_id": "tag_url",
+ "asset_type": "url",
+ "required": true
+ },
+ {
+ "asset_group_id": "backup_image",
+ "asset_type": "image",
+ "required": true,
+ "max_size_kb": 50
+ },
+ {
+ "asset_group_id": "landing_page_url",
+ "asset_type": "url",
+ "required": false
+ }
+ ]
+ }
+ }
+ ]
+}
diff --git a/static/examples/products/v2/google_performance_max.json b/static/examples/products/v2/google_performance_max.json
new file mode 100644
index 0000000000..e7b216dd6b
--- /dev/null
+++ b/static/examples/products/v2/google_performance_max.json
@@ -0,0 +1,93 @@
+{
+ "$schema": "/schemas/core/product.json",
+ "product_id": "google_pmax_us",
+ "name": "Google Performance Max — United States",
+ "description": "Google Performance Max campaign — buyer supplies a pool of typed assets (multiple headlines, descriptions, landscape/square images, videos, logos) and Google's optimizer composes combinations across Search, Display, YouTube, Discover, Gmail, and Maps. Composition is algorithmic — surface picks combinations and reports per-asset performance breakdowns. 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).",
+ "publisher_properties": [
+ {
+ "publisher_domain": "google.com",
+ "selection_type": "all"
+ }
+ ],
+ "channels": [
+ "search",
+ "display",
+ "ctv",
+ "olv"
+ ],
+ "delivery_type": "non_guaranteed",
+ "pricing_options": [
+ {
+ "pricing_option_id": "cpa_purchase",
+ "pricing_model": "cpa",
+ "event_type": "purchase",
+ "currency": "USD",
+ "fixed_price": 25
+ }
+ ],
+ "reporting_capabilities": {
+ "available_reporting_frequencies": [
+ "daily"
+ ],
+ "expected_delay_minutes": 240,
+ "timezone": "America/Los_Angeles",
+ "supports_webhooks": false,
+ "available_metrics": [
+ "impressions",
+ "clicks",
+ "spend",
+ "ctr",
+ "conversions",
+ "conversion_value",
+ "cost_per_acquisition",
+ "roas",
+ "viewability"
+ ],
+ "date_range_support": "date_range"
+ },
+ "format_options": [
+ {
+ "format_kind": "responsive_creative",
+ "params": {
+ "headlines_min": 3,
+ "headlines_max": 15,
+ "headline_max_chars": 30,
+ "long_headlines_min": 1,
+ "long_headlines_max": 5,
+ "long_headline_max_chars": 90,
+ "descriptions_min": 2,
+ "descriptions_max": 5,
+ "description_max_chars": 90,
+ "images_landscape_min": 1,
+ "images_landscape_max": 20,
+ "images_landscape_aspect_ratio": "1.91:1",
+ "images_square_min": 1,
+ "images_square_max": 20,
+ "videos_min": 0,
+ "videos_max": 5,
+ "video_min_duration_ms": 10000,
+ "video_max_duration_ms": 600000,
+ "logo_min": 1,
+ "logo_max": 5,
+ "logo_aspect_ratios": [
+ "1:1",
+ "4:1"
+ ],
+ "business_name_max_chars": 25,
+ "asset_image_max_file_size_kb": 5120,
+ "supports_catalog_input": true,
+ "composition_model": "algorithmic",
+ "platform_extensions": [
+ {
+ "uri": "https://google.adcp/extensions/google_conversion_actions",
+ "digest": "sha256:d8f1a4c7b0e3d6f9a2c5b8e1d4f7a0c3b6e9d2f5a8c1b4e7d0f3a6c9b2e5d8f1"
+ },
+ {
+ "uri": "https://google.adcp/extensions/google_audience_signals",
+ "digest": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ }
+ ]
+ }
+ }
+ ]
+}
diff --git a/static/examples/products/v2/meta_carousel.json b/static/examples/products/v2/meta_carousel.json
new file mode 100644
index 0000000000..e0116914a0
--- /dev/null
+++ b/static/examples/products/v2/meta_carousel.json
@@ -0,0 +1,88 @@
+{
+ "$schema": "/schemas/core/product.json",
+ "product_id": "meta_carousel_us",
+ "name": "Meta Carousel — United States",
+ "description": "Multi-card swipeable carousel on Meta feed (Facebook + Instagram). 2-10 cards, square aspect, polymorphic per-card asset (image OR video). Each card carries its own headline + click URL. Surface composes the swipeable presentation; buyer ships the cards array.",
+ "publisher_properties": [
+ {
+ "publisher_domain": "meta.com",
+ "selection_type": "all"
+ }
+ ],
+ "channels": [
+ "social"
+ ],
+ "delivery_type": "non_guaranteed",
+ "pricing_options": [
+ {
+ "pricing_option_id": "cpm_floor",
+ "pricing_model": "cpm",
+ "currency": "USD",
+ "floor_price": 4.5
+ }
+ ],
+ "reporting_capabilities": {
+ "available_reporting_frequencies": [
+ "hourly",
+ "daily"
+ ],
+ "expected_delay_minutes": 60,
+ "timezone": "America/Los_Angeles",
+ "supports_webhooks": true,
+ "available_metrics": [
+ "impressions",
+ "clicks",
+ "spend",
+ "ctr",
+ "engagement_rate",
+ "viewability"
+ ],
+ "date_range_support": "date_range"
+ },
+ "format_options": [
+ {
+ "format_kind": "image_carousel",
+ "params": {
+ "card_aspect_ratio": "1:1",
+ "min_cards": 2,
+ "max_cards": 10,
+ "allowed_card_asset_types": [
+ "image",
+ "video"
+ ],
+ "card_image_max_file_size_kb": 30000,
+ "card_video_max_duration_ms": 240000,
+ "primary_text_max_chars": 125,
+ "card_headline_max_chars": 40,
+ "ssl_required": true,
+ "composition_model": "deterministic",
+ "slots": [
+ {
+ "asset_group_id": "cards",
+ "asset_type": "object",
+ "required": true,
+ "min": 2,
+ "max": 10
+ },
+ {
+ "asset_group_id": "primary_text",
+ "asset_type": "text",
+ "required": false,
+ "max_chars": 125
+ },
+ {
+ "asset_group_id": "landing_page_url",
+ "asset_type": "url",
+ "required": true
+ }
+ ],
+ "platform_extensions": [
+ {
+ "uri": "https://meta.adcp/extensions/meta_pixel",
+ "digest": "sha256:a3f5b7c9d8e2f1a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2"
+ }
+ ]
+ }
+ }
+ ]
+}
diff --git a/static/examples/products/v2/meta_reels_us.json b/static/examples/products/v2/meta_reels_us.json
new file mode 100644
index 0000000000..5ca79667e5
--- /dev/null
+++ b/static/examples/products/v2/meta_reels_us.json
@@ -0,0 +1,86 @@
+{
+ "$schema": "/schemas/core/product.json",
+ "product_id": "meta_reels_us",
+ "name": "Meta Reels — United States",
+ "description": "9:16 vertical short-form video on Meta Reels (Facebook + Instagram). Buyers upload H.264 mp4 (3-90s) plus headline and primary text; Meta serves to Reels feeds with placement-native UI overlays and Meta-specific tracking via the meta_pixel extension.",
+ "publisher_properties": [
+ {
+ "publisher_domain": "meta.com",
+ "selection_type": "all"
+ }
+ ],
+ "channels": [
+ "social"
+ ],
+ "delivery_type": "non_guaranteed",
+ "pricing_options": [
+ {
+ "pricing_option_id": "cpm_floor",
+ "pricing_model": "cpm",
+ "currency": "USD",
+ "floor_price": 5.5
+ }
+ ],
+ "reporting_capabilities": {
+ "available_reporting_frequencies": [
+ "hourly",
+ "daily"
+ ],
+ "expected_delay_minutes": 60,
+ "timezone": "America/Los_Angeles",
+ "supports_webhooks": true,
+ "available_metrics": [
+ "impressions",
+ "clicks",
+ "spend",
+ "completion_rate",
+ "viewability"
+ ],
+ "date_range_support": "date_range"
+ },
+ "format_options": [
+ {
+ "format_kind": "video_hosted",
+ "params": {
+ "orientation": "vertical",
+ "aspect_ratio": "9:16",
+ "duration_ms_range": [
+ 3000,
+ 90000
+ ],
+ "min_width": 1080,
+ "min_height": 1920,
+ "max_file_size_mb": 200,
+ "video_codecs": [
+ "h264"
+ ],
+ "audio_codecs": [
+ "aac"
+ ],
+ "containers": [
+ "mp4"
+ ],
+ "headline_max_chars": 25,
+ "primary_text_max_chars": 72,
+ "captions": "recommended",
+ "cta_values": [
+ "LEARN_MORE",
+ "SHOP_NOW",
+ "DOWNLOAD",
+ "SIGN_UP"
+ ],
+ "composition_model": "deterministic",
+ "platform_extensions": [
+ {
+ "uri": "https://meta.adcp/extensions/meta_pixel",
+ "digest": "sha256:a3f5b7c9d8e2f1a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2"
+ },
+ {
+ "uri": "https://meta.adcp/extensions/meta_placements_reels",
+ "digest": "sha256:b8e1d4f6a9c2b5e8d1f4a7c0b3e6d9f2a5c8b1e4d7f0a3c6b9e2d5f8a1c4b7e0"
+ }
+ ]
+ }
+ }
+ ]
+}
diff --git a/static/examples/products/v2/nytimes_homepage_html5.json b/static/examples/products/v2/nytimes_homepage_html5.json
new file mode 100644
index 0000000000..586a308396
--- /dev/null
+++ b/static/examples/products/v2/nytimes_homepage_html5.json
@@ -0,0 +1,64 @@
+{
+ "$schema": "/schemas/core/product.json",
+ "product_id": "nytimes_homepage_html5",
+ "name": "NYTimes.com Homepage HTML5 Banner (300×250)",
+ "description": "IAB Medium Rectangle (300×250) interactive HTML5 banner placement on the NYTimes.com homepage. Buyers upload an HTML5 zip bundle (≤200KB initial load, ≤500KB polite-load with host_initiated_subload, max 30s animation, OM-SDK + clickTag macro). Different canonical from the static image MREC because the tracking model is fundamentally different (MRAID + OM-SDK vs impression pixel + click URL).",
+ "publisher_properties": [
+ {
+ "publisher_domain": "nytimes.com",
+ "selection_type": "by_id",
+ "property_ids": [
+ "homepage_above_fold"
+ ]
+ }
+ ],
+ "channels": [
+ "display"
+ ],
+ "delivery_type": "guaranteed",
+ "pricing_options": [
+ {
+ "pricing_option_id": "cpm_homepage_html5",
+ "pricing_model": "cpm",
+ "currency": "USD",
+ "fixed_price": 28
+ }
+ ],
+ "reporting_capabilities": {
+ "available_reporting_frequencies": [
+ "daily"
+ ],
+ "expected_delay_minutes": 240,
+ "timezone": "America/New_York",
+ "supports_webhooks": false,
+ "available_metrics": [
+ "impressions",
+ "clicks",
+ "spend",
+ "ctr",
+ "viewability",
+ "engagement_rate"
+ ],
+ "date_range_support": "date_range"
+ },
+ "format_options": [
+ {
+ "format_kind": "html5",
+ "params": {
+ "width": 300,
+ "height": 250,
+ "max_initial_load_kb": 200,
+ "max_polite_load_kb": 500,
+ "host_initiated_subload": true,
+ "max_animation_duration_ms": 30000,
+ "max_cpu_load_percent": 30,
+ "om_sdk_required": true,
+ "clicktag_macro": "clickTag",
+ "backup_image_required": true,
+ "backup_image_max_size_kb": 50,
+ "ssl_required": true,
+ "composition_model": "deterministic"
+ }
+ }
+ ]
+}
diff --git a/static/examples/products/v2/nytimes_homepage_mrec.json b/static/examples/products/v2/nytimes_homepage_mrec.json
new file mode 100644
index 0000000000..447885bc15
--- /dev/null
+++ b/static/examples/products/v2/nytimes_homepage_mrec.json
@@ -0,0 +1,71 @@
+{
+ "$schema": "/schemas/core/product.json",
+ "product_id": "nytimes_homepage_mrec",
+ "name": "NYTimes.com Homepage MREC (300×250)",
+ "description": "IAB Medium Rectangle (300×250) static image placement on the NYTimes.com homepage above the fold. Buyers upload jpg/png/gif up to 200KB; HTTPS-only; standard impression pixel + click URL tracking via universal_macros plus IAB Open Measurement viewability via the nytimes_om_strict extension.",
+ "publisher_properties": [
+ {
+ "publisher_domain": "nytimes.com",
+ "selection_type": "by_id",
+ "property_ids": [
+ "homepage_above_fold"
+ ]
+ }
+ ],
+ "channels": [
+ "display"
+ ],
+ "delivery_type": "guaranteed",
+ "pricing_options": [
+ {
+ "pricing_option_id": "cpm_homepage_mrec",
+ "pricing_model": "cpm",
+ "currency": "USD",
+ "fixed_price": 22
+ }
+ ],
+ "reporting_capabilities": {
+ "available_reporting_frequencies": [
+ "daily"
+ ],
+ "expected_delay_minutes": 240,
+ "timezone": "America/New_York",
+ "supports_webhooks": false,
+ "available_metrics": [
+ "impressions",
+ "clicks",
+ "spend",
+ "ctr",
+ "viewability"
+ ],
+ "date_range_support": "date_range"
+ },
+ "format_options": [
+ {
+ "format_kind": "image",
+ "params": {
+ "width": 300,
+ "height": 250,
+ "max_file_size_kb": 200,
+ "image_formats": [
+ "jpg",
+ "png",
+ "gif"
+ ],
+ "ssl_required": true,
+ "cta_values": [
+ "LEARN_MORE",
+ "SHOP_NOW",
+ "GET_OFFER"
+ ],
+ "composition_model": "deterministic",
+ "platform_extensions": [
+ {
+ "uri": "https://nytimes.adcp/extensions/nytimes_om_strict",
+ "digest": "sha256:c9d2f5b8e1a4c7b0e3d6f9a2c5b8e1d4f7a0c3b6e9d2f5a8c1b4e7d0f3a6c9b2"
+ }
+ ]
+ }
+ }
+ ]
+}
diff --git a/static/examples/products/v2/the_daily_30s_host_read.json b/static/examples/products/v2/the_daily_30s_host_read.json
new file mode 100644
index 0000000000..4e13d06ea0
--- /dev/null
+++ b/static/examples/products/v2/the_daily_30s_host_read.json
@@ -0,0 +1,78 @@
+{
+ "$schema": "/schemas/core/product.json",
+ "product_id": "the_daily_30s_host_read_us",
+ "name": "The Daily — 30s Host-Read Pre-roll (US)",
+ "description": "30-second podcast host-read pre-roll on The Daily. Buyer-uploaded audio is rejected (audio_source: publisher_host_recorded); buyer submits a verbatim script (≤800 chars) as a text asset under the `script` slot, plus brand context via the manifest's BrandRef. The publisher's host records the audio, which is dynamically inserted at podcast playback time. 7-business-day production turnaround. A brief-driven host-read product would have the same shape with `creative_brief` (brief asset_type) in the slots instead of `script` (text asset_type).",
+ "publisher_properties": [
+ {
+ "publisher_domain": "thedailypod.example",
+ "selection_type": "all"
+ }
+ ],
+ "channels": [
+ "podcast"
+ ],
+ "delivery_type": "guaranteed",
+ "pricing_options": [
+ {
+ "pricing_option_id": "cpm_host_read",
+ "pricing_model": "cpm",
+ "currency": "USD",
+ "fixed_price": 35
+ }
+ ],
+ "reporting_capabilities": {
+ "available_reporting_frequencies": [
+ "daily"
+ ],
+ "expected_delay_minutes": 1440,
+ "timezone": "America/New_York",
+ "supports_webhooks": false,
+ "available_metrics": [
+ "impressions",
+ "spend",
+ "completion_rate",
+ "completed_views"
+ ],
+ "date_range_support": "date_range"
+ },
+ "format_options": [
+ {
+ "format_kind": "audio_hosted",
+ "params": {
+ "duration_ms_exact": 30000,
+ "audio_codecs": [
+ "mp3",
+ "aac"
+ ],
+ "audio_sample_rates": [
+ 44100,
+ 48000
+ ],
+ "audio_channels": [
+ "stereo"
+ ],
+ "loudness_lufs": -16,
+ "audio_source": "publisher_host_recorded",
+ "buyer_audio_acceptance": "rejected",
+ "composition_model": "deterministic",
+ "slots": [
+ {
+ "asset_group_id": "script",
+ "required": true,
+ "asset_type": "text",
+ "max_chars": 800,
+ "description": "Verbatim script the host reads — exact wording; no improvisation; legal pre-cleared."
+ },
+ {
+ "asset_group_id": "offering_ref",
+ "required": false,
+ "asset_type": "text",
+ "description": "Optional offering identifier from the buyer's catalog to focus the host-read."
+ }
+ ],
+ "production_window_business_days": 7
+ }
+ }
+ ]
+}
diff --git a/static/examples/products/v2/triton_daast_audio_30s.json b/static/examples/products/v2/triton_daast_audio_30s.json
new file mode 100644
index 0000000000..4481476ec9
--- /dev/null
+++ b/static/examples/products/v2/triton_daast_audio_30s.json
@@ -0,0 +1,67 @@
+{
+ "$schema": "/schemas/core/product.json",
+ "product_id": "triton_daast_audio_30s",
+ "name": "Triton Audio DAAST (30s)",
+ "description": "DAAST 1.1 audio tag on Triton-managed streaming radio inventory. Buyer ships a DAAST tag (URL or inline XML); the streaming server fires DAAST events (impression / quartiles / click / complete / error) inherent to the spec. Audio analog of VAST.",
+ "publisher_properties": [
+ {
+ "publisher_domain": "triton.example",
+ "selection_type": "all"
+ }
+ ],
+ "channels": [
+ "streaming_audio",
+ "radio"
+ ],
+ "delivery_type": "non_guaranteed",
+ "pricing_options": [
+ {
+ "pricing_option_id": "cpm_floor",
+ "pricing_model": "cpm",
+ "currency": "USD",
+ "floor_price": 12
+ }
+ ],
+ "reporting_capabilities": {
+ "available_reporting_frequencies": [
+ "daily"
+ ],
+ "expected_delay_minutes": 1440,
+ "timezone": "UTC",
+ "supports_webhooks": false,
+ "available_metrics": [
+ "impressions",
+ "spend",
+ "completion_rate",
+ "completed_views",
+ "quartile_data"
+ ],
+ "date_range_support": "date_range"
+ },
+ "format_options": [
+ {
+ "format_kind": "audio_daast",
+ "params": {
+ "daast_version": "1.1",
+ "duration_ms_exact": 30000,
+ "linear_required": true,
+ "max_wrapper_depth": 3,
+ "ssl_required": true,
+ "companion_image_required": false,
+ "composition_model": "deterministic",
+ "slots": [
+ {
+ "asset_group_id": "daast_tag",
+ "asset_type": "daast",
+ "required": true
+ },
+ {
+ "asset_group_id": "landing_page_url",
+ "asset_type": "url",
+ "required": false
+ }
+ ]
+ }
+ }
+ ]
+}
diff --git a/static/examples/products/v2/veo_generative_video_15s.json b/static/examples/products/v2/veo_generative_video_15s.json
new file mode 100644
index 0000000000..8112d764bc
--- /dev/null
+++ b/static/examples/products/v2/veo_generative_video_15s.json
@@ -0,0 +1,95 @@
+{
+ "$schema": "/schemas/core/product.json",
+ "product_id": "veo_generative_video_vertical_15s",
+ "name": "Veo — 15s Generative Vertical Video",
+ "description": "Generative video synthesis from a creative_brief plus structured scenes. Buyer ships a brief (≤500 chars) and a scenes plan (3 scenes summing to 15s); Veo synthesizes the video. Genuinely nondeterministic — synthesis from in-spec inputs may produce out-of-spec frames; the platform's post-synthesis QA loop validates and reseeds up to N attempts before surfacing output. If the QA loop exhausts, build_creative returns task_failed with synthesis_failed reason. provenance_required: true means every produced asset carries a C2PA provenance manifest attributing synthesis to Veo (not the seller).",
+ "publisher_properties": [
+ {
+ "publisher_domain": "veo.example",
+ "selection_type": "all"
+ }
+ ],
+ "channels": [
+ "social",
+ "olv"
+ ],
+ "delivery_type": "non_guaranteed",
+ "pricing_options": [
+ {
+ "pricing_option_id": "cpm_floor",
+ "pricing_model": "cpm",
+ "currency": "USD",
+ "floor_price": 18
+ }
+ ],
+ "reporting_capabilities": {
+ "available_reporting_frequencies": [
+ "daily"
+ ],
+ "expected_delay_minutes": 240,
+ "timezone": "UTC",
+ "supports_webhooks": false,
+ "available_metrics": [
+ "impressions",
+ "clicks",
+ "spend",
+ "completion_rate",
+ "viewability"
+ ],
+ "date_range_support": "date_range"
+ },
+ "format_options": [
+ {
+ "format_kind": "video_hosted",
+ "params": {
+ "orientation": "vertical",
+ "aspect_ratio": "9:16",
+ "duration_ms_exact": 15000,
+ "min_width": 1080,
+ "min_height": 1920,
+ "video_codecs": [
+ "h264"
+ ],
+ "containers": [
+ "mp4"
+ ],
+ "frame_rates": [
+ 24,
+ 30
+ ],
+ "video_source": "agent_synthesized",
+ "buyer_video_acceptance": "rejected",
+ "captions": "recommended",
+ "composition_model": "deterministic",
+ "synthesis_nondeterministic": true,
+ "provenance_required": true,
+ "production_window_business_days": 0,
+ "slots": [
+ {
+ "asset_group_id": "creative_brief",
+ "asset_type": "brief",
+ "required": true,
+ "max_chars": 500
+ },
+ {
+ "asset_group_id": "scenes",
+ "asset_type": "object",
+ "required": true,
+ "min": 1,
+ "max": 5
+ },
+ {
+ "asset_group_id": "style_reference",
+ "asset_type": "image",
+ "required": false
+ },
+ {
+ "asset_group_id": "landing_page_url",
+ "asset_type": "url",
+ "required": false
+ }
+ ]
+ }
+ }
+ ]
+}
diff --git a/static/examples/products/v2/youtube_vast_preroll.json b/static/examples/products/v2/youtube_vast_preroll.json
new file mode 100644
index 0000000000..cf83ef5476
--- /dev/null
+++ b/static/examples/products/v2/youtube_vast_preroll.json
@@ -0,0 +1,88 @@
+{
+ "$schema": "/schemas/core/product.json",
+ "product_id": "youtube_vast_preroll_15s_skippable",
+ "name": "YouTube VAST Pre-roll (15s skippable, In-Stream)",
+ "description": "VAST tag pre-roll on YouTube In-Stream inventory, 16:9 horizontal, 5-second skippable threshold. Buyer ships a VAST 4.x tag (URL or inline XML); YouTube fires VAST events (impression / quartiles / click / complete / error / skip) inherent to the spec. VPAID 2.0 supported but discouraged — Google deprecates VPAID in 2026.",
+ "publisher_properties": [
+ {
+ "publisher_domain": "youtube.com",
+ "selection_type": "all"
+ }
+ ],
+ "channels": [
+ "olv",
+ "ctv"
+ ],
+ "delivery_type": "non_guaranteed",
+ "pricing_options": [
+ {
+ "pricing_option_id": "cpv_skippable",
+ "pricing_model": "cpv",
+ "currency": "USD",
+ "floor_price": 0.05,
+ "parameters": {
+ "view_threshold": {
+ "duration_seconds": 30
+ }
+ }
+ }
+ ],
+ "reporting_capabilities": {
+ "available_reporting_frequencies": [
+ "daily"
+ ],
+ "expected_delay_minutes": 240,
+ "timezone": "America/Los_Angeles",
+ "supports_webhooks": false,
+ "available_metrics": [
+ "impressions",
+ "completed_views",
+ "spend",
+ "completion_rate",
+ "viewability",
+ "quartile_data"
+ ],
+ "date_range_support": "date_range"
+ },
+ "format_options": [
+ {
+ "format_kind": "video_vast",
+ "params": {
+ "orientation": "horizontal",
+ "aspect_ratio": "16:9",
+ "vast_version": "4.2",
+ "vpaid_enabled": false,
+ "simid_supported": false,
+ "duration_ms_range": [
+ 6000,
+ 30000
+ ],
+ "min_width": 1280,
+ "min_height": 720,
+ "linear_required": true,
+ "skippable_after_ms": 5000,
+ "max_wrapper_depth": 5,
+ "ssl_required": true,
+ "composition_model": "deterministic",
+ "slots": [
+ {
+ "asset_group_id": "vast_tag",
+ "asset_type": "vast",
+ "required": true
+ },
+ {
+ "asset_group_id": "landing_page_url",
+ "asset_type": "url",
+ "required": false
+ }
+ ],
+ "platform_extensions": [
+ {
+ "uri": "https://google.adcp/extensions/google_universal_ad_id",
+ "digest": "sha256:b3c5e7f9a1c3e5b7d9f1a3c5e7b9d1f3a5c7e9b1d3f5a7c9e1b3d5f7a9c1e3b5"
+ }
+ ]
+ }
+ }
+ ]
+}
diff --git a/static/schemas/source/core/asset-group-vocabulary.json b/static/schemas/source/core/asset-group-vocabulary.json
new file mode 100644
index 0000000000..393468e591
--- /dev/null
+++ b/static/schemas/source/core/asset-group-vocabulary.json
@@ -0,0 +1,219 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "/schemas/core/asset-group-vocabulary.json",
+ "title": "AdCP Asset Group Vocabulary Registry",
+ "description": "Canonical registry of asset_group_id values used in offering asset groups (OfferingAssetGroup) and in v2 product format declarations. Non-canonical IDs remain valid for platform-specific extensions; this registry codifies the recommended canonical set so that buyers and sellers share a vocabulary for the most common slot roles. Validators may emit soft warnings on non-canonical IDs to encourage convergence.\n\nThe registry covers everything the buyer ships in the manifest's `assets` map — both directly-rendered creative content (image, video, audio) AND content the seller consumes for production (script, creative_brief, scenes). The seller dispatches per the format's slot declaration. There is no separate \"inputs\" map on the manifest; everything is an asset.",
+ "version": "1.1.0",
+ "lastUpdated": "2026-04-28",
+ "vocabulary": {
+ "headlines": {
+ "description": "Pool of headline text variants for the surface to choose from.",
+ "asset_type": "text",
+ "typical_use": "Multiple short headline copy variants (Google PMax/RDA, Meta promoted_offerings, etc.)",
+ "aliases": ["headline", "title", "tagline", "headline_text"]
+ },
+ "long_headlines": {
+ "description": "Pool of longer headline text variants (typically 60-90 chars) used by responsive formats that render in placements with more available text space.",
+ "asset_type": "text",
+ "typical_use": "Google Responsive Display Ads / Performance Max / Demand Gen — render alongside short `headlines` when the placement supports a longer headline string. Distinct from `headlines` (short, ≤30 chars typical).",
+ "aliases": ["long_headline_pool", "extended_headlines"]
+ },
+ "descriptions": {
+ "description": "Pool of body description text variants.",
+ "asset_type": "text",
+ "typical_use": "Body-copy variants for surfaces that pick the best combination.",
+ "aliases": ["description", "body", "body_text", "text", "content"]
+ },
+ "images_landscape": {
+ "description": "Pool of landscape-orientation images (1.91:1 or 16:9 typical).",
+ "asset_type": "image",
+ "typical_use": "Hero images for landscape-format placements (Meta feed, LinkedIn feed, Google display).",
+ "aliases": ["image", "hero_image", "landscape_image", "banner_image"]
+ },
+ "images_vertical": {
+ "description": "Pool of vertical-orientation images (9:16 typical).",
+ "asset_type": "image",
+ "typical_use": "Hero images for stories, reels, and other vertical placements (Snap, TikTok, Meta Stories, Pinterest).",
+ "aliases": ["vertical_image", "story_image", "portrait_image"]
+ },
+ "images_square": {
+ "description": "Pool of square-orientation images (1:1).",
+ "asset_type": "image",
+ "typical_use": "Feed-context images, profile-style placements, square carousel cards.",
+ "aliases": ["square_image", "feed_image"]
+ },
+ "logo": {
+ "description": "Brand logo asset (typically 1:1 or 2:1).",
+ "asset_type": "image",
+ "typical_use": "Brand attribution overlay (Google PMax/RDA, Snap Story Ad, Amazon SB).",
+ "aliases": ["brand_logo", "logo_image"]
+ },
+ "video": {
+ "description": "Pool of video assets.",
+ "asset_type": "video",
+ "typical_use": "Video creative for video placements; orientation determined by platform constraints.",
+ "aliases": ["video_file", "hero_video", "video_asset", "video_main"]
+ },
+ "video_vertical": {
+ "description": "Pool of vertical-orientation video (9:16).",
+ "asset_type": "video",
+ "typical_use": "Reels, Stories, TikTok In-Feed, Snap Spotlight, vertical short-form video."
+ },
+ "video_horizontal": {
+ "description": "Pool of horizontal-orientation video (16:9).",
+ "asset_type": "video",
+ "typical_use": "Pre-roll, mid-roll, instream, CTV, in-feed horizontal video."
+ },
+ "audio": {
+ "description": "Audio asset.",
+ "asset_type": "audio",
+ "typical_use": "Audio ads for streaming, podcasts, broadcast radio.",
+ "aliases": ["audio_file", "hero_audio", "audio_asset", "audio_main"]
+ },
+ "companion_image": {
+ "description": "Image displayed alongside an audio asset.",
+ "asset_type": "image",
+ "typical_use": "Brand-attribution image paired with audio (Spotify standard 640x640, Amazon DSP audio 300x300)."
+ },
+ "companion_banner": {
+ "description": "Banner image displayed alongside a video asset.",
+ "asset_type": "image",
+ "typical_use": "Companion banner for instream video (Google video 300x60, Amazon DSP 1.91:1)."
+ },
+ "brand_name": {
+ "description": "Short brand attribution text (distinct from headline).",
+ "asset_type": "text",
+ "typical_use": "Brand name overlay on Snap, TikTok, Spotify, Pinterest creative."
+ },
+ "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."
+ },
+ "cards": {
+ "description": "Per-item carousel card array.",
+ "asset_type": "object",
+ "typical_use": "Carousel slides on Meta, TikTok, Pinterest, LinkedIn, Reddit. Each card carries its own image/video, headline, and link.",
+ "aliases": ["carousel_cards", "slides", "carousel_items", "carousel_slides"]
+ },
+ "cta": {
+ "description": "Call-to-action button label or text.",
+ "asset_type": "text",
+ "typical_use": "CTA button on display/video/social ad creative (e.g., \"Shop Now\", \"Learn More\"). Most products narrow to a fixed enum via `cta_values`; this canonical entry names the slot itself.",
+ "aliases": ["cta_text", "call_to_action", "action_text", "button_text"]
+ },
+ "price": {
+ "description": "Product price slot rendered on the creative.",
+ "asset_type": "text",
+ "typical_use": "Pinterest shopping pins, Amazon Sponsored Brand product cards, retail-media catalog rendering. Often pulled from a catalog field via `field_bindings`."
+ },
+ "disclaimer": {
+ "description": "Required legal disclaimer or fine print.",
+ "asset_type": "text",
+ "typical_use": "Pharma, financial services, alcohol, sweepstakes — non-optional in regulated verticals. Receivers may render at reduced size below the primary creative.",
+ "aliases": ["legal_text", "fine_print", "legal_disclaimer"]
+ },
+ "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."
+ },
+ "promo_code": {
+ "description": "Promotional / offer code rendered on the creative.",
+ "asset_type": "text",
+ "typical_use": "Retail / promo creative carrying a coupon code (e.g., \"SAVE20\"). Distinct from offering metadata; this is the rendered text.",
+ "aliases": ["offer_code", "coupon_code", "discount_code"]
+ },
+ "subtitle_file": {
+ "description": "Subtitle / closed-caption file for video assets.",
+ "asset_type": "url",
+ "typical_use": "WebVTT or SRT URL paired with a `video` slot. Required by accessibility-strict products; recommended for most video formats.",
+ "aliases": ["caption_file", "captions", "subtitles"]
+ },
+ "source_catalog": {
+ "description": "Catalog asset reference for catalog-driven products.",
+ "asset_type": "catalog",
+ "typical_use": "Required input on `sponsored_placement` formats. Buyer references a synced catalog by `catalog_id` plus optional item filters."
+ },
+ "hero_asset": {
+ "description": "Optional buyer-supplied hero/banner asset alongside a catalog reference.",
+ "asset_type": "image",
+ "typical_use": "Pinterest Collection, Snap Collection, Amazon SB Product Collection — buyer supplies a hero image; surface composes catalog tiles below it.",
+ "aliases": ["hero_banner", "collection_hero"]
+ },
+ "landing_page_url": {
+ "description": "Click-through destination URL for the ad.",
+ "asset_type": "url",
+ "typical_use": "Primary destination for ad click-through across all canonical formats.",
+ "aliases": [
+ "click_url",
+ "link",
+ "final_url",
+ "link_url",
+ "click_through_url",
+ "landing_url"
+ ],
+ "note": "Canonical name for the destination URL slot. Six different field names exist across platform format definitions today; adopters should standardize on `landing_page_url` for v2."
+ },
+ "privacy_policy_url": {
+ "description": "Privacy policy URL required for lead-form variants.",
+ "asset_type": "url",
+ "typical_use": "LinkedIn lead gen, Snap lead gen — required for any format that collects user data."
+ },
+ "youtube_video_id": {
+ "description": "Externally-hosted YouTube video reference.",
+ "asset_type": "text",
+ "typical_use": "Reference an existing YouTube video for Google instream/bumper/non-skippable, PMax.",
+ "aliases": ["existing_yt_video_id", "yt_video_id", "youtube_id"]
+ },
+ "pin_id": {
+ "description": "Externally-hosted Pinterest creative reference.",
+ "asset_type": "text",
+ "typical_use": "Reference an existing Pin (parallel to youtube_video_id)."
+ },
+ "script": {
+ "description": "Verbatim text the seller's production reads, dubs, or transforms into the rendered output.",
+ "asset_type": "text",
+ "typical_use": "Podcast host-read products (host reads buyer's script verbatim), TTS audio synthesis, video voiceover. Distinct from `creative_brief` (looser talking-points style guidance).",
+ "aliases": ["script_text", "host_script", "voiceover_script"]
+ },
+ "creative_brief": {
+ "description": "Talking points, brand context, and creative direction for seller-side or generative production.",
+ "asset_type": "brief",
+ "typical_use": "Generative AI products (text-to-image, text-to-video), brief-driven podcast host-reads (host has discretion within brief), creative-template platforms. Distinct from `script` (verbatim wording) — `creative_brief` gives the producer creative latitude.",
+ "aliases": ["brief", "creative_direction", "talking_points"]
+ },
+ "scenes": {
+ "description": "Structured scene-by-scene plan for generative video production.",
+ "asset_type": "object",
+ "typical_use": "Generative video products (Veo/Sora/Runway-class). Each scene declares order, duration_ms, description, optional voiceover and caption. See /schemas/creative/scenes.json for the typed structure.",
+ "aliases": ["storyboard"]
+ },
+ "voice_id": {
+ "description": "Voice selection from a format-defined enum or registered voice catalog.",
+ "asset_type": "text",
+ "typical_use": "TTS audio synthesis — buyer selects from the platform's voice library. The exact enum is per-format; the canonical name normalizes the slot key."
+ },
+ "offering_ref": {
+ "description": "Reference to a specific offering (product, service, campaign) within the buyer's brand catalog.",
+ "asset_type": "text",
+ "typical_use": "agent_placement products (focus the AI-surface mention on a specific offering); host-read products (specify which offering is being promoted in the read). Value is an offering_id matched against the buyer's catalog."
+ },
+ "style_reference": {
+ "description": "Reference asset (image) providing per-creative visual style guidance for generative production. Distinct from brand.json — brand.json carries declarative brand-level style (hex colors, voice descriptions, logo asset, tagline) that's stable across campaigns; `style_reference` carries a per-creative reference image (\"make it look like THIS\") that ships in the manifest. For pure brand-style consistency, the manifest's BrandRef resolving brand.json suffices and `style_reference` isn't needed.",
+ "asset_type": "image",
+ "typical_use": "Image-to-image variation (MidJourney --sref, Adobe Firefly structure/style reference), style transfer, reusing a hero shot's lighting/composition for new creative. Buyer supplies a reference image; seller's generative pipeline produces output matching the visual style.",
+ "aliases": ["reference_image", "style_image", "inspiration_image", "structure_reference"]
+ },
+ "starter_assets": {
+ "description": "Pool of reference assets the seller uses as a starting point for transformation or variation.",
+ "asset_type": "object",
+ "typical_use": "Generative platforms that take an existing creative and produce variations (different sizes, orientations, durations). Buyer supplies the starter; seller derives outputs."
+ }
+ },
+ "governance": {
+ "extension_policy": "Non-canonical asset_group_id values remain valid for platform-specific extensions. Validators may emit soft warnings to encourage adoption of canonical values where applicable. New canonical entries are added via PR with rationale, at least one reference adopter, and AAO maintainer review.",
+ "alias_policy": "Aliases listed for canonical entries (e.g., `landing_page_url` aliases) are recognized as v1-era variants. v2 adopters should standardize on the canonical name; v1 aliases continue to work for backwards compatibility.",
+ "versioning": "This registry is versioned independently of the AdCP spec. Bump `version` on any addition or alias change; preserve existing canonical entries to avoid breaking adopters that have standardized on them."
+ }
+}
diff --git a/static/schemas/source/core/assets/asset-union.json b/static/schemas/source/core/assets/asset-union.json
index a5480b58c2..a0ff0c376a 100644
--- a/static/schemas/source/core/assets/asset-union.json
+++ b/static/schemas/source/core/assets/asset-union.json
@@ -12,6 +12,7 @@
{ "$ref": "/schemas/core/assets/url-asset.json" },
{ "$ref": "/schemas/core/assets/html-asset.json" },
{ "$ref": "/schemas/core/assets/javascript-asset.json" },
+ { "$ref": "/schemas/core/assets/zip-asset.json" },
{ "$ref": "/schemas/core/assets/webhook-asset.json" },
{ "$ref": "/schemas/core/assets/css-asset.json" },
{ "$ref": "/schemas/core/assets/daast-asset.json" },
diff --git a/static/schemas/source/core/assets/html-asset.json b/static/schemas/source/core/assets/html-asset.json
index e28a47d581..76b89e6f4a 100644
--- a/static/schemas/source/core/assets/html-asset.json
+++ b/static/schemas/source/core/assets/html-asset.json
@@ -2,7 +2,7 @@
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "/schemas/core/assets/html-asset.json",
"title": "HTML Asset",
- "description": "HTML content asset",
+ "description": "Inline HTML content asset. For URL-delivered HTML5 banner bundles, use the zip asset type instead. For single-URL iframe-rendered tag references, use the url asset type with an appropriate url_type.",
"type": "object",
"properties": {
"asset_type": {
diff --git a/static/schemas/source/core/assets/javascript-asset.json b/static/schemas/source/core/assets/javascript-asset.json
index 0af6b3adb7..9b1522c7ad 100644
--- a/static/schemas/source/core/assets/javascript-asset.json
+++ b/static/schemas/source/core/assets/javascript-asset.json
@@ -2,7 +2,7 @@
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "/schemas/core/assets/javascript-asset.json",
"title": "JavaScript Asset",
- "description": "JavaScript code asset",
+ "description": "Inline JavaScript content asset. For URL-delivered third-party tag scripts, use the url asset type with url_type 'tracker_script'. For HTML5 banner bundles that include JavaScript, use the zip asset type.",
"type": "object",
"properties": {
"asset_type": {
diff --git a/static/schemas/source/core/assets/zip-asset.json b/static/schemas/source/core/assets/zip-asset.json
new file mode 100644
index 0000000000..9a7f1ca2dc
--- /dev/null
+++ b/static/schemas/source/core/assets/zip-asset.json
@@ -0,0 +1,89 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "/schemas/core/assets/zip-asset.json",
+ "title": "Zip Asset",
+ "description": "Bundled creative asset delivered as a zip archive — typically an HTML5 banner with index.html plus supporting CSS, JS, images, and fonts. Receivers unpack the zip, validate internal structure, and serve contents from CDN. Distinct from inline HTML (html asset) and from third-party tag URLs (url asset with url_type tracker_script).",
+ "type": "object",
+ "properties": {
+ "asset_type": {
+ "type": "string",
+ "const": "zip",
+ "description": "Discriminator identifying this as a zip-bundled asset. See /schemas/creative/asset-types for the registry."
+ },
+ "url": {
+ "type": "string",
+ "format": "uri",
+ "description": "URL where the zip archive is hosted. Must be HTTPS."
+ },
+ "max_file_size_kb": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "Maximum file size in kilobytes. Receivers should reject zips exceeding this."
+ },
+ "entry_point": {
+ "type": "string",
+ "description": "Relative path to the entry file within the zip (typically 'index.html'). Receivers default to 'index.html' if absent."
+ },
+ "allowed_inner_extensions": {
+ "type": "array",
+ "items": { "type": "string" },
+ "description": "File extensions permitted inside the zip (e.g., ['html', 'css', 'js', 'png', 'jpg', 'svg', 'webp', 'json', 'woff2']). Receivers may reject zips containing other extensions."
+ },
+ "backup_image_url": {
+ "type": "string",
+ "format": "uri",
+ "description": "Fallback image URL for environments that cannot render the bundled creative (e.g., non-HTML5 endpoints, ad blockers). Recommended for HTML5 banners."
+ },
+ "digest": {
+ "type": "string",
+ "pattern": "^sha256:[a-f0-9]{64}$",
+ "description": "Optional SHA-256 content digest of the zip archive (sha256:) for integrity verification. Lets receivers detect tampered or stale archives."
+ },
+ "accessibility": {
+ "type": "object",
+ "description": "Self-declared accessibility properties for this opaque creative",
+ "x-accessibility": true,
+ "properties": {
+ "alt_text": {
+ "type": "string",
+ "description": "Text alternative describing the creative content"
+ },
+ "keyboard_navigable": {
+ "type": "boolean",
+ "description": "Whether the creative can be fully operated via keyboard"
+ },
+ "motion_control": {
+ "type": "boolean",
+ "description": "Whether the creative respects prefers-reduced-motion or provides pause/stop controls"
+ },
+ "screen_reader_tested": {
+ "type": "boolean",
+ "description": "Whether the creative has been tested with screen readers"
+ }
+ }
+ },
+ "provenance": {
+ "$ref": "/schemas/core/provenance.json",
+ "description": "Provenance metadata for this asset, overrides manifest-level provenance"
+ }
+ },
+ "required": [
+ "asset_type",
+ "url"
+ ],
+ "additionalProperties": true,
+ "examples": [
+ {
+ "description": "HTML5 banner bundle with backup image",
+ "data": {
+ "asset_type": "zip",
+ "url": "https://cdn.acme.example/creatives/spring-300x250-html5.zip",
+ "max_file_size_kb": 200,
+ "entry_point": "index.html",
+ "allowed_inner_extensions": ["html", "css", "js", "png", "jpg", "svg", "json"],
+ "backup_image_url": "https://cdn.acme.example/creatives/spring-300x250-backup.jpg",
+ "digest": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ }
+ }
+ ]
+}
diff --git a/static/schemas/source/core/canonical-format-kind.json b/static/schemas/source/core/canonical-format-kind.json
new file mode 100644
index 0000000000..1d669254ac
--- /dev/null
+++ b/static/schemas/source/core/canonical-format-kind.json
@@ -0,0 +1,21 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "/schemas/core/canonical-format-kind.json",
+ "title": "Canonical Format Kind",
+ "description": "Discriminator value naming one of the 11 canonical creative formats — plus `custom` for adopter-defined shapes that don't fit the canonicals (multi-placement takeover, roadblock, branded content, cross-screen sponsorship, AR lens, etc.). Used by `product-format-declaration.json` (the product's inline format declaration), `creative-manifest.json` (the buyer's v2 manifest path), and any other surface that needs to identify which canonical a payload targets.\n\nWhen `format_kind: \"custom\"`, the declaration MUST also carry `format_shape` (referencing the [format-shape vocabulary registry](/schemas/core/format-shape-vocabulary.json) — recognized global pattern this custom shape is an instance of) and `format_schema` (URI+digest reference to a fetchable schema describing the shape's actual `params` and `slots`). Buyer agents fetch the schema, validate manifests structurally, and reason about manifests without per-seller integration code — same mechanic as `platform_extensions`. See [adcp#3666](https://github.com/adcontextprotocol/adcp/issues/3666) for the canonical promotion queue.\n\nThe canonical enum mirrors the `oneOf` branches in `product-format-declaration.json`; keep them in sync.",
+ "type": "string",
+ "enum": [
+ "image",
+ "html5",
+ "display_tag",
+ "image_carousel",
+ "video_hosted",
+ "video_vast",
+ "audio_hosted",
+ "audio_daast",
+ "sponsored_placement",
+ "responsive_creative",
+ "agent_placement",
+ "custom"
+ ]
+}
diff --git a/static/schemas/source/core/creative-asset.json b/static/schemas/source/core/creative-asset.json
index f33adc2d53..e6c691aaff 100644
--- a/static/schemas/source/core/creative-asset.json
+++ b/static/schemas/source/core/creative-asset.json
@@ -2,12 +2,12 @@
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "/schemas/core/creative-asset.json",
"title": "Creative Asset",
- "description": "Creative asset for upload to library - supports static assets, generative formats, and third-party snippets",
+ "description": "Creative asset for upload to library — supports static assets, generative formats, and third-party snippets. Identifies which format this creative conforms to via EITHER a v1 `format_id` (structured `{agent_url, id}`) OR v2 `format_kind` (canonical format name). Mutually exclusive — see the `oneOf` at the schema root.",
"type": "object",
"properties": {
"creative_id": {
"type": "string",
- "description": "Unique identifier for the creative",
+ "description": "Unique identifier for the creative. Stable across v1 and v2 paths — a creative registered against v1 `format_id` retains the same `creative_id` when later viewed via v2 flatten.",
"x-entity": "creative"
},
"name": {
@@ -16,7 +16,15 @@
},
"format_id": {
"$ref": "/schemas/core/format-id.json",
- "description": "Always a structured object {agent_url, id} — never a plain string. Format identifier specifying which format this creative conforms to. Can be: (1) concrete format_id referencing a format with fixed dimensions, (2) template format_id referencing a template format, or (3) parameterized format_id with dimensions/duration parameters for template formats."
+ "description": "v1 path. Always a structured object {agent_url, id} — never a plain string. Format identifier specifying which format this creative conforms to. Can be: (1) concrete format_id referencing a format with fixed dimensions, (2) template format_id referencing a template format, or (3) parameterized format_id with dimensions/duration parameters for template formats. Mutually exclusive with `format_kind`."
+ },
+ "format_kind": {
+ "$ref": "/schemas/core/canonical-format-kind.json",
+ "description": "v2 path. The canonical format name this creative targets (e.g., `image`, `video_hosted`). Mutually exclusive with `format_id`."
+ },
+ "capability_id": {
+ "type": "string",
+ "description": "v2 path, optional. Stable identifier matching one of the seller's product `format_options[i].capability_id` values. REQUIRED only when the target product has multiple `format_options` entries sharing the same `format_kind`."
},
"assets": {
"type": "object",
@@ -97,8 +105,21 @@
"required": [
"creative_id",
"name",
- "format_id",
"assets"
],
+ "oneOf": [
+ {
+ "title": "v1 creative (named-format reference)",
+ "description": "Creative references a named format via the structured `format_id` object. The v1 path; remains supported through 4.x.",
+ "required": ["format_id"],
+ "not": { "required": ["format_kind"] }
+ },
+ {
+ "title": "v2 creative (canonical format kind)",
+ "description": "Creative declares which canonical format it targets via `format_kind` (e.g., `image`). The v2 path introduced by RFC #3305.",
+ "required": ["format_kind"],
+ "not": { "required": ["format_id"] }
+ }
+ ],
"additionalProperties": true
}
diff --git a/static/schemas/source/core/creative-manifest.json b/static/schemas/source/core/creative-manifest.json
index 410387e4d7..4b2e132536 100644
--- a/static/schemas/source/core/creative-manifest.json
+++ b/static/schemas/source/core/creative-manifest.json
@@ -2,16 +2,24 @@
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "/schemas/core/creative-manifest.json",
"title": "Creative Manifest",
- "description": "Complete specification of a creative: format_id + assets. Everything the creative needs — images, text, briefs, catalogs — lives in the assets map, declared by the format. Each asset is typed according to its asset_type from the format specification.",
+ "description": "Complete specification of a creative: format identification + assets. A manifest carries EITHER a v1 `format_id` (structured `{agent_url, id}` reference to a named format) OR v2 `format_kind` (the canonical format name the manifest targets, paired with optional `capability_id` for routing when a product's `format_options` contains multiple declarations sharing the same `format_kind`). Mutually exclusive — see the `oneOf` at the schema root. Everything the creative needs — images, text, briefs, catalogs — lives in the assets map, declared by the matching format declaration. Each asset is typed according to its asset_type from the format specification.",
"type": "object",
"properties": {
"format_id": {
"$ref": "/schemas/core/format-id.json",
- "description": "Always a structured object {agent_url, id} — never a plain string. Format identifier this manifest is for. Can be a template format (id only) or a deterministic format (id + dimensions/duration). For dimension-specific creatives, include width/height in the format_id to create a unique identifier (e.g., {id: 'display_static', width: 300, height: 250})."
+ "description": "v1 path. Always a structured object {agent_url, id} — never a plain string. Format identifier this manifest is for. Can be a template format (id only) or a deterministic format (id + dimensions/duration). For dimension-specific creatives, include width/height in the format_id to create a unique identifier (e.g., {id: 'display_static', width: 300, height: 250}). Mutually exclusive with `format_kind`."
+ },
+ "format_kind": {
+ "$ref": "/schemas/core/canonical-format-kind.json",
+ "description": "v2 path. The canonical format name this manifest targets (e.g., `image`, `video_hosted`, `audio_daast`, `sponsored_placement`). Selects which canonical the seller validates the manifest's assets against. Mutually exclusive with `format_id`."
+ },
+ "capability_id": {
+ "type": "string",
+ "description": "v2 path, optional. Stable identifier matching one of the seller's product `format_options[i].capability_id` values. REQUIRED when the target product carries multiple `format_options` entries sharing the same `format_kind` (the buyer must disambiguate which option this manifest matches). When the product's `format_options` has a single entry — or multiple entries with distinct `format_kind` values — `capability_id` is OPTIONAL because `format_kind` alone routes the manifest to the right declaration."
},
"assets": {
"type": "object",
- "description": "Map of asset IDs to actual asset content. Each key MUST match an asset_id from the format's assets array (e.g., 'banner_image', 'clickthrough_url', 'video_file', 'vast_tag'). The asset_id is the technical identifier used to match assets to format requirements.\n\nEach asset value carries an `asset_type` discriminator (image, video, audio, vast, daast, text, markdown, url, html, css, webhook, javascript, brief, catalog) that selects the matching asset schema. Validators with OpenAPI-style discriminator support use `asset_type` to report errors against only the selected branch instead of all branches.",
+ "description": "Map of slot keys to actual asset content. v1 path: each key matches an `asset_id` from the format's `assets` array (e.g., 'banner_image', 'clickthrough_url', 'video_file', 'vast_tag'). v2 path: each key matches an `asset_group_id` from the format's `slots` declaration drawn from the canonical vocabulary registry (e.g., 'images_landscape', 'video', 'landing_page_url', 'vast_tag', 'script', 'creative_brief'). Either path produces the same envelope shape; only the slot-key vocabulary differs.\n\nEach asset value carries an `asset_type` discriminator (image, video, audio, vast, daast, text, markdown, url, html, css, webhook, javascript, brief, catalog, zip) that selects the matching asset schema. Validators with OpenAPI-style discriminator support use `asset_type` to report errors against only the selected branch instead of all branches.",
"patternProperties": {
"^[a-z0-9_]+$": {
"$ref": "/schemas/core/assets/asset-union.json"
@@ -19,6 +27,38 @@
},
"additionalProperties": true
},
+ "brand": {
+ "$ref": "/schemas/core/brand-ref.json",
+ "description": "Brand identity reference (BrandRef — `domain` plus optional `brand_id` for house-of-brands). When present, the seller pulls brand context (logos, colors, voice, taglines) from the brand's brand.json automatically. v2 formats no longer redeclare brand_logo / brand_colors / brand_voice as explicit slots — brand identity is implicit context."
+ },
+ "brand_kit_override": {
+ "type": "object",
+ "description": "Explicit brand-kit override for the case where brand.json is missing, stale, or inappropriate for this specific creative. When present, takes precedence over brand.json lookups for the supplied fields. Sellers use brand.json as the default and the override as the per-creative authoritative source.",
+ "properties": {
+ "logo": {
+ "$ref": "/schemas/core/assets/image-asset.json",
+ "description": "Override logo asset for this creative."
+ },
+ "colors": {
+ "type": "object",
+ "description": "Override brand colors (hex strings).",
+ "properties": {
+ "primary": { "type": "string", "pattern": "^#[0-9a-fA-F]{6}$" },
+ "secondary": { "type": "string", "pattern": "^#[0-9a-fA-F]{6}$" },
+ "accent": { "type": "string", "pattern": "^#[0-9a-fA-F]{6}$" }
+ },
+ "additionalProperties": true
+ },
+ "voice": {
+ "type": "string",
+ "description": "Override brand-voice description for surface-composed text/audio output."
+ },
+ "tagline": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": true
+ },
"rights": {
"type": "array",
"description": "Rights constraints attached to this creative. Each entry represents constraints from a single rights holder. A creative may combine multiple rights constraints (e.g., talent likeness + music license). For v1, rights constraints are informational metadata — the buyer/orchestrator manages creative lifecycle against these terms.",
@@ -43,8 +83,21 @@
}
},
"required": [
- "format_id",
"assets"
],
+ "oneOf": [
+ {
+ "title": "v1 manifest (named-format reference)",
+ "description": "Manifest references a named format via the structured `format_id` object. The v1 path; remains supported through 4.x.",
+ "required": ["format_id"],
+ "not": { "required": ["format_kind"] }
+ },
+ {
+ "title": "v2 manifest (canonical format kind)",
+ "description": "Manifest declares which canonical format it targets via `format_kind` (e.g., `image`). The v2 path introduced by RFC #3305.",
+ "required": ["format_kind"],
+ "not": { "required": ["format_id"] }
+ }
+ ],
"additionalProperties": true
}
diff --git a/static/schemas/source/core/format-shape-vocabulary.json b/static/schemas/source/core/format-shape-vocabulary.json
new file mode 100644
index 0000000000..4ebbf2bd08
--- /dev/null
+++ b/static/schemas/source/core/format-shape-vocabulary.json
@@ -0,0 +1,64 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "/schemas/core/format-shape-vocabulary.json",
+ "title": "AdCP Format Shape Vocabulary Registry",
+ "description": "Canonical registry of `format_shape` values used on `ProductFormatDeclaration` when `format_kind: \"custom\"`. Captures recognized creative-structure patterns that are NOT yet first-class canonical formats — composed/coordinated/sponsorship shapes that high-end publishers and broadcast networks sell as headline products. Each registry entry names a global shape; the seller's actual structure lives in `format_schema` (URI+digest reference to the seller-hosted or AAO-mirrored schema) so buyer agents can fetch and validate against a real schema rather than reasoning over an opaque ext blob.\n\n**Two-layer extensibility:**\n- **Canonical** (`format_kind: image`, `video_vast`, etc.): full spec coverage, stable contract.\n- **Custom + format_shape + format_schema** (`format_kind: \"custom\"`): recognized pattern, classified against this vocabulary, but the params/slots structure is supplied by a fetchable schema rather than baked into AdCP.\n\nNon-canonical `format_shape` values remain valid (validators MAY soft-warn) so adopters CAN ship a shape that isn't yet in the registry — adding entries is a vocabulary PR, not a major-version bump. Once a `format_shape` entry sees 2+ adopters with substantively similar `format_schema` content for 90+ days, the working group promotes it to a first-class canonical (creates `/schemas/formats/canonical/.json`, adds the value to `canonical-format-kind.json`, retires the registry entry). See [adcp#3666](https://github.com/adcontextprotocol/adcp/issues/3666) for the promotion queue.",
+ "version": "1.0.0",
+ "lastUpdated": "2026-04-30",
+ "vocabulary": {
+ "multi_placement_takeover": {
+ "description": "Coordinated buy targeting several placements at once on a single page or content unit, sold as a unit (homepage skin + banner + preroll + sponsorship lockup, all firing for the same advertiser concurrently). Used by NYTimes, WSJ, Hulu, premium publishers. Multi-canonical composition — the format_schema enumerates each component placement and its own format constraints.",
+ "typical_use": "Premium publisher homepage takeovers, day-part broadcast takeovers, content-section sponsorships",
+ "tracking_model_hint": "Per-component impressions plus a takeover-level engagement metric (time-on-page, scroll depth)",
+ "promotion_status": "tracking — see adcp#3666"
+ },
+ "roadblock": {
+ "description": "Single advertiser owns all spots in a content unit (every commercial break in a half-hour show, every banner on a section page, every preroll for a content category). Linear-TV-shaped but applies to digital too. Distinct from `multi_placement_takeover` because all spots share the SAME format (ten preroll videos), not different formats coordinated together.",
+ "typical_use": "Linear TV roadblocks, podcast network sponsorships of a content category, all-spots-on-section",
+ "tracking_model_hint": "Per-spot impressions plus exclusivity attestation",
+ "promotion_status": "tracking — see adcp#3666"
+ },
+ "branded_content": {
+ "description": "Publisher-produced editorial sponsorship (advertorial, sponsored articles, paid features, branded videos). Shape doesn't compose from canonicals — it's its own creative production model where the publisher's editorial team produces the asset from a buyer brief. Distinct from `agent_placement` (AI-surface composition) and `audio_hosted` host-read (shorter-form, scripted-from-buyer).",
+ "typical_use": "NYT T Brand Studio, WSJ Custom Content, Vox Creative",
+ "tracking_model_hint": "Engagement-keyed (time-on-page, scroll depth, video completion) rather than impression-keyed",
+ "promotion_status": "tracking — see adcp#3666"
+ },
+ "cross_screen_sponsorship": {
+ "description": "Synchronized buys across linear TV / CTV / display / audio. Frequency-capped at the user, not the screen. Concurrent-session-keyed tracking that ties impressions across devices to the same household / individual.",
+ "typical_use": "Live sports sponsorship spanning broadcast + streaming + social, multi-screen reach campaigns",
+ "tracking_model_hint": "Cross-device impression dedup; household-level reach measurement",
+ "promotion_status": "tracking — see adcp#3666"
+ },
+ "sponsorship_lockup": {
+ "description": "Persistent brand presence over a content window (Hulu's 'commercial-free experience presented by Brand X', podcast network sponsorship lockups, newsletter section sponsorships, Spotify-shaped audio-on-demand sponsorships). Long-duration, low-impression count, premium pricing. Structure: a brand reference + a duration window + (optionally) a small lockup creative (logo, tagline) that persists.",
+ "typical_use": "Premium streaming sponsorships, podcast network presenting sponsorships",
+ "tracking_model_hint": "Window-duration impressions plus a single lockup-creative-view event",
+ "promotion_status": "tracking — see adcp#3666"
+ },
+ "newsletter_sponsorship": {
+ "description": "Email-embedded creative with newsletter-issue-keyed measurement. Distinct from display because the impression event is open-tracking-pixel-shaped, not page-view-shaped. Distinct from email marketing because the surface is a third-party newsletter (the publisher's), not a brand-owned send.",
+ "typical_use": "Substack newsletter sponsorships, Morning Brew, Axios, The Skimm",
+ "tracking_model_hint": "Open-pixel impressions (proxied via the newsletter's email service); CTR via redirect URLs",
+ "promotion_status": "tracking — see adcp#3666"
+ },
+ "ar_lens": {
+ "description": "Interactive AR creative (Snap lens, Meta camera filter, TikTok effect). SDK-specific composition model; doesn't fit `html5` because the engagement shape is fundamentally different — face/world tracking, gesture interactions, share-out mechanics, rendered-on-device.",
+ "typical_use": "Snap branded lenses, Meta camera filters, TikTok branded effects",
+ "tracking_model_hint": "Lens-specific events (open, capture, share, time-played) tied to lens_id",
+ "promotion_status": "tracking — see adcp#3666"
+ },
+ "playable": {
+ "description": "Interactive HTML5 mini-game or experience (Unity playable, IAB MRAID 3.0 playable). Distinct from `html5` banner because engagement is the primary impression event, not view-through.",
+ "typical_use": "Mobile game ads, branded mini-experiences (IKEA Place-shaped AR-lite)",
+ "tracking_model_hint": "Engagement-event-keyed (game start, level complete, completion)",
+ "promotion_status": "tracking — see adcp#3666"
+ },
+ "live_event_sponsorship": {
+ "description": "Sponsorship attached to a specific live broadcast (sports, concert, breaking-news window). Concurrent-impression and stream-state tracking — the impression is bounded by the event's start/end times and may include during-event creative variants.",
+ "typical_use": "Super Bowl pregame sponsorship, live concert streaming sponsorships, breaking-news sponsorship windows",
+ "tracking_model_hint": "Event-window-bounded impressions; stream-state correlation (live vs replay vs ended)",
+ "promotion_status": "tracking — see adcp#3666"
+ }
+ }
+}
diff --git a/static/schemas/source/core/format.json b/static/schemas/source/core/format.json
index 6836672086..d86c63f0b0 100644
--- a/static/schemas/source/core/format.json
+++ b/static/schemas/source/core/format.json
@@ -29,6 +29,10 @@
"type": "array",
"description": "Publisher-controlled elements rendered on top of buyer content at this asset's position (e.g., video player controls, publisher logos). Creative agents should avoid placing critical content (CTAs, logos, key copy) within overlay bounds.",
"items": { "$ref": "/schemas/core/overlay.json" }
+ },
+ "asset_group_id": {
+ "type": "string",
+ "description": "Optional canonical asset_group_id this slot fills, drawn from /schemas/core/asset-group-vocabulary.json. Lets buyers and migration tools resolve v1 author-invented slot names (e.g., `click_url`) to canonical names (e.g., `landing_page_url`). Validators MAY soft-warn when a v1 slot's asset_id is a known alias but no asset_group_id is declared."
}
},
"required": ["item_type", "asset_id", "asset_type", "required"]
@@ -44,6 +48,10 @@
"type": "string",
"description": "Descriptive label for this asset's purpose. For documentation and UI display only — manifests key assets by asset_id, not asset_role."
},
+ "asset_group_id": {
+ "type": "string",
+ "description": "Optional canonical asset_group_id this slot fills, drawn from /schemas/core/asset-group-vocabulary.json. Same semantics as on baseIndividualAsset — lets buyers and migration tools resolve v1 author-invented slot names to canonical names."
+ },
"required": {
"type": "boolean",
"description": "Whether this asset is required within each repetition of the group",
@@ -278,6 +286,15 @@
"requirements": { "$ref": "/schemas/core/requirements/javascript-asset-requirements.json" }
}
},
+ {
+ "title": "IndividualZipAsset",
+ "description": "Zip-bundled asset (HTML5 banner bundles, etc.)",
+ "allOf": [{ "$ref": "#/$defs/baseIndividualAsset" }],
+ "properties": {
+ "item_type": { "const": "individual" },
+ "asset_type": { "const": "zip" }
+ }
+ },
{
"title": "IndividualVastAsset",
"description": "VAST asset",
@@ -451,6 +468,14 @@
"requirements": { "$ref": "/schemas/core/requirements/javascript-asset-requirements.json" }
}
},
+ {
+ "title": "GroupZipAsset",
+ "description": "Zip-bundled asset in group (HTML5 banner bundles, etc.)",
+ "allOf": [{ "$ref": "#/$defs/baseGroupAsset" }],
+ "properties": {
+ "asset_type": { "const": "zip" }
+ }
+ },
{
"title": "GroupVastAsset",
"description": "VAST asset in group",
diff --git a/static/schemas/source/core/offering-asset-group.json b/static/schemas/source/core/offering-asset-group.json
index b7b50e4620..c3893668e8 100644
--- a/static/schemas/source/core/offering-asset-group.json
+++ b/static/schemas/source/core/offering-asset-group.json
@@ -29,6 +29,7 @@
{ "$ref": "/schemas/core/assets/daast-asset.json" },
{ "$ref": "/schemas/core/assets/css-asset.json" },
{ "$ref": "/schemas/core/assets/javascript-asset.json" },
+ { "$ref": "/schemas/core/assets/zip-asset.json" },
{ "$ref": "/schemas/core/assets/webhook-asset.json" }
],
"discriminator": {
diff --git a/static/schemas/source/core/platform-extension-ref.json b/static/schemas/source/core/platform-extension-ref.json
new file mode 100644
index 0000000000..8d9f5eb591
--- /dev/null
+++ b/static/schemas/source/core/platform-extension-ref.json
@@ -0,0 +1,27 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "/schemas/core/platform-extension-ref.json",
+ "title": "Platform Extension Reference",
+ "description": "Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` — divergent digests in one response indicate producer-side error (e.g., concurrent extension revision mid-render). Buyers encountering divergent digests for the same URI MUST fail closed: treat all references to that URI as unresolved and surface a validation error rather than picking one branch silently. **Across responses**, digest divergence is normal — extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.",
+ "type": "object",
+ "required": ["uri", "digest"],
+ "properties": {
+ "uri": {
+ "type": "string",
+ "format": "uri",
+ "description": "URL identifying the extension. The URI base is the owning agent's URL; the path identifies the extension within that agent. Example: 'https://meta.adcp/extensions/meta_pixel'."
+ },
+ "digest": {
+ "type": "string",
+ "pattern": "^sha256:[a-f0-9]{64}$",
+ "description": "SHA-256 content digest of the extension definition (sha256:). Used to detect drift — if the agent revises the extension, the digest changes and cached definitions become invalid."
+ }
+ },
+ "additionalProperties": true,
+ "examples": [
+ {
+ "uri": "https://meta.adcp/extensions/meta_pixel",
+ "digest": "sha256:a3f5b7c9d8e2f1a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2"
+ }
+ ]
+}
diff --git a/static/schemas/source/core/product-format-declaration.json b/static/schemas/source/core/product-format-declaration.json
new file mode 100644
index 0000000000..bd8b505ab5
--- /dev/null
+++ b/static/schemas/source/core/product-format-declaration.json
@@ -0,0 +1,239 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "/schemas/core/product-format-declaration.json",
+ "title": "Product Format Declaration",
+ "description": "Inline format declaration on a product. The `format_kind` discriminator names which canonical format the product narrows; `params` carries the canonical's parameter schema (slots, dimensions, durations, codecs, character limits, platform_extensions, tracking_extensions, etc.). Optional `capability_id` (stable identifier for routing when a product's `format_options` contains multiple declarations sharing the same `format_kind`) and `applies_to_channels` (subset of the product's declared channels this declaration applies to — lets a multi-channel product carry distinct format_options per channel). Discriminated-union shape generates clean tagged unions in TypeScript and Pydantic codegen. Replaces v1's named-format pattern (where products referenced a separately-defined format file via compound `format_id`). v1 named formats remain supported through the deprecation cycle; v2 product-bound declarations are opt-in.\n\n**Custom format_kind** (`format_kind: \"custom\"`): for adopter-defined shapes that don't fit the 11 canonicals (multi-placement takeover, roadblock, branded content, cross-screen sponsorship, sponsorship lockup, newsletter sponsorship, AR lens, playable, live event sponsorship). When `format_kind` is `custom`, the declaration MUST carry `format_shape` (recognized global pattern from the [format-shape vocabulary registry](/schemas/core/format-shape-vocabulary.json)) AND `format_schema` (URI+digest reference to a fetchable schema describing the actual `params` and `slots`). Buyer agents fetch the schema, validate manifests structurally, and reason about manifests without per-seller integration code. See [adcp#3666](https://github.com/adcontextprotocol/adcp/issues/3666) for the canonical promotion queue.",
+ "type": "object",
+ "required": ["format_kind", "params"],
+ "discriminator": { "propertyName": "format_kind" },
+ "properties": {
+ "capability_id": {
+ "type": "string",
+ "description": "Optional stable identifier for this format declaration. REQUIRED when the parent product's `format_options` contains multiple declarations sharing the same `format_kind` (so buyers can disambiguate which option a manifest targets via `manifest.capability_id`). Recommended for any declaration that may be referenced by capability_id over time. Format-internal (not a URI). Examples: 'flashtalking_image_300x250', 'pmax_responsive_search'."
+ },
+ "applies_to_channels": {
+ "type": "array",
+ "items": { "$ref": "/schemas/enums/channels.json" },
+ "uniqueItems": true,
+ "description": "Optional subset of the parent product's `channels` to which this declaration applies. When omitted, the declaration applies to ALL channels declared on the product. Lets a multi-channel product (e.g., `channels: ['display', 'video']`) carry distinct format_options per channel — `format_options: [{format_kind: 'image', applies_to_channels: ['display']}, {format_kind: 'video_hosted', applies_to_channels: ['video']}]`. Buyers ship channel-appropriate manifests per `applies_to_channels`."
+ },
+ "runtime_status": {
+ "type": "string",
+ "enum": ["stable", "preview", "declared_only"],
+ "default": "stable",
+ "description": "Adopter-runtime readiness for this product-format declaration. **Distinct from the canonical's `status` field** (which describes whether the v2 working group has stabilized the format definition itself). `runtime_status` describes whether THIS seller's runtime actually honors what they declared on THIS product.\n\n- `stable` (default) — adopter's runtime fully honors the declared format + production source. Buyers can rely on the declaration as a serving contract.\n- `preview` — runtime supports the basic path; some axes (e.g., per-item fan-out under `item_production_model`, brief-driven overrides, advanced `platform_extensions`) may be partial. Buyers SHOULD validate via `validate_input` or sandbox before committing budget.\n- `declared_only` — catalog declaration is forward-looking; runtime does NOT yet implement this path. Buyers MUST treat as informational and confirm via `validate_input` or a sandbox storyboard before purchase. Compliance storyboards SHOULD skip-gate `declared_only` entries gracefully rather than failing.\n\nThe two axes vary independently: a `stable` canonical can have `declared_only` adopters (canonical is settled in spec but adopter hasn't wired runtime yet), and a `preview` canonical can have `stable` adopters (adopter built against the preview shape and their runtime fully honors it). Producers SHOULD set this when their product declaration is aspirational; absence is interpreted as `stable`. Sellers MUST upgrade the value as the runtime catches up; buyers cache it like any other capability field."
+ },
+ "format_shape": {
+ "type": "string",
+ "description": "REQUIRED when `format_kind: \"custom\"`; otherwise MUST be absent. Recognized global pattern this custom shape is an instance of, drawn from the [format-shape vocabulary registry](/schemas/core/format-shape-vocabulary.json) (`multi_placement_takeover`, `roadblock`, `branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values valid (validators MAY soft-warn) — adopters CAN ship a shape that isn't yet in the registry. Adding entries is a vocabulary PR. Once a `format_shape` entry sees 2+ adopters with substantively similar `format_schema` content for 90+ days, the working group promotes it to a first-class canonical."
+ },
+ "format_schema": {
+ "$ref": "/schemas/core/platform-extension-ref.json",
+ "description": "REQUIRED when `format_kind: \"custom\"`; otherwise MUST be absent. URI+digest reference to a fetchable schema describing this custom shape's actual `params` and `slots`. Same hosting model as `platform_extensions`: open-ecosystem publishers host the artifact at the canonical URI on their subdomain; closed-platform / walled-garden shapes resolve through the AAO mirror at `https://mirror.adcontextprotocol.org/translated/...`. Buyer agents fetch by `uri@digest` (immutable per digest, aggressive caching, `Cache-Control: public, max-age=31536000, immutable`), validate `params` and `slots` against the fetched schema, and reason about manifests structurally — same mechanic as platform_extensions but at the format-structure level. Without `format_schema`, custom shapes would be opaque to buyer agents and the protocol would regress to per-seller integration code; that's why the schema is required, not optional."
+ }
+ },
+ "allOf": [
+ {
+ "if": {
+ "properties": { "format_kind": { "const": "custom" } },
+ "required": ["format_kind"]
+ },
+ "then": {
+ "required": ["format_shape", "format_schema"]
+ },
+ "else": {
+ "not": { "anyOf": [
+ { "required": ["format_shape"] },
+ { "required": ["format_schema"] }
+ ] }
+ }
+ }
+ ],
+ "oneOf": [
+ {
+ "title": "Image Format Declaration",
+ "properties": {
+ "format_kind": { "type": "string", "const": "image" },
+ "params": { "$ref": "/schemas/formats/canonical/image.json" }
+ },
+ "required": ["format_kind", "params"]
+ },
+ {
+ "title": "HTML5 Format Declaration",
+ "properties": {
+ "format_kind": { "type": "string", "const": "html5" },
+ "params": { "$ref": "/schemas/formats/canonical/html5.json" }
+ },
+ "required": ["format_kind", "params"]
+ },
+ {
+ "title": "Display Tag Format Declaration",
+ "properties": {
+ "format_kind": { "type": "string", "const": "display_tag" },
+ "params": { "$ref": "/schemas/formats/canonical/display_tag.json" }
+ },
+ "required": ["format_kind", "params"]
+ },
+ {
+ "title": "Image Carousel Format Declaration",
+ "properties": {
+ "format_kind": { "type": "string", "const": "image_carousel" },
+ "params": { "$ref": "/schemas/formats/canonical/image_carousel.json" }
+ },
+ "required": ["format_kind", "params"]
+ },
+ {
+ "title": "Hosted Video Format Declaration",
+ "properties": {
+ "format_kind": { "type": "string", "const": "video_hosted" },
+ "params": { "$ref": "/schemas/formats/canonical/video_hosted.json" }
+ },
+ "required": ["format_kind", "params"]
+ },
+ {
+ "title": "VAST Video Format Declaration",
+ "properties": {
+ "format_kind": { "type": "string", "const": "video_vast" },
+ "params": { "$ref": "/schemas/formats/canonical/video_vast.json" }
+ },
+ "required": ["format_kind", "params"]
+ },
+ {
+ "title": "Hosted Audio Format Declaration",
+ "properties": {
+ "format_kind": { "type": "string", "const": "audio_hosted" },
+ "params": { "$ref": "/schemas/formats/canonical/audio_hosted.json" }
+ },
+ "required": ["format_kind", "params"]
+ },
+ {
+ "title": "DAAST Audio Format Declaration",
+ "properties": {
+ "format_kind": { "type": "string", "const": "audio_daast" },
+ "params": { "$ref": "/schemas/formats/canonical/audio_daast.json" }
+ },
+ "required": ["format_kind", "params"]
+ },
+ {
+ "title": "Sponsored Placement Format Declaration",
+ "properties": {
+ "format_kind": { "type": "string", "const": "sponsored_placement" },
+ "params": { "$ref": "/schemas/formats/canonical/sponsored_placement.json" }
+ },
+ "required": ["format_kind", "params"]
+ },
+ {
+ "title": "Responsive Creative Format Declaration",
+ "properties": {
+ "format_kind": { "type": "string", "const": "responsive_creative" },
+ "params": { "$ref": "/schemas/formats/canonical/responsive_creative.json" }
+ },
+ "required": ["format_kind", "params"]
+ },
+ {
+ "title": "Agent Placement Format Declaration",
+ "properties": {
+ "format_kind": { "type": "string", "const": "agent_placement" },
+ "params": { "$ref": "/schemas/formats/canonical/agent_placement.json" }
+ },
+ "required": ["format_kind", "params"]
+ },
+ {
+ "title": "Custom Format Declaration",
+ "description": "Adopter-defined shape that doesn't fit the 11 canonicals. Requires `format_shape` (vocabulary-registered global pattern) and `format_schema` (URI+digest reference to a fetchable schema describing the actual params/slots). `params` shape is governed by the fetched schema rather than baked into AdCP — kept as `type: object` here with `additionalProperties: true` because the canonical schema validates dynamically post-fetch.",
+ "properties": {
+ "format_kind": { "type": "string", "const": "custom" },
+ "params": {
+ "type": "object",
+ "additionalProperties": true,
+ "description": "Custom shape's params. Validated against the schema fetched from `format_schema.uri` at the cached `format_schema.digest`."
+ }
+ },
+ "required": ["format_kind", "params"]
+ }
+ ],
+ "examples": [
+ {
+ "description": "Meta Reels — narrows video_hosted (vertical orientation)",
+ "data": {
+ "format_kind": "video_hosted",
+ "params": {
+ "orientation": "vertical",
+ "aspect_ratio": "9:16",
+ "duration_ms_range": [3000, 90000],
+ "min_width": 1080,
+ "min_height": 1920,
+ "max_file_size_mb": 200,
+ "video_codecs": ["h264"],
+ "audio_codecs": ["aac"],
+ "headline_max_chars": 25,
+ "primary_text_max_chars": 72,
+ "captions": "recommended",
+ "cta_values": ["LEARN_MORE", "SHOP_NOW", "DOWNLOAD", "SIGN_UP"],
+ "composition_model": "deterministic",
+ "platform_extensions": [
+ { "uri": "https://meta.adcp/extensions/meta_pixel", "digest": "sha256:a3f5b7c9d8e2f1a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2" }
+ ]
+ }
+ }
+ },
+ {
+ "description": "IAB Medium Rectangle (300x250) — narrows image",
+ "data": {
+ "format_kind": "image",
+ "params": {
+ "width": 300,
+ "height": 250,
+ "max_file_size_kb": 200,
+ "image_formats": ["jpg", "png", "gif"],
+ "ssl_required": true,
+ "composition_model": "deterministic",
+ "cta_values": ["LEARN_MORE", "SHOP_NOW", "GET_OFFER"]
+ }
+ }
+ },
+ {
+ "description": "Podcast 30s host-read — narrows audio_hosted with a `script` slot the seller's host reads verbatim. No separate `inputs` map; the script lives in the manifest's `assets` like any other text asset.",
+ "data": {
+ "format_kind": "audio_hosted",
+ "params": {
+ "duration_ms_exact": 30000,
+ "audio_codecs": ["mp3", "aac"],
+ "audio_sample_rates": [44100, 48000],
+ "audio_channels": ["stereo"],
+ "loudness_lufs": -16,
+ "audio_source": "publisher_host_recorded",
+ "buyer_audio_acceptance": "rejected",
+ "composition_model": "deterministic",
+ "slots": [
+ { "asset_group_id": "script", "required": true, "asset_type": "text", "max_chars": 800 },
+ { "asset_group_id": "offering_ref", "required": false, "asset_type": "text" }
+ ],
+ "production_window_business_days": 7
+ }
+ }
+ },
+ {
+ "description": "NYTimes Homepage Takeover — custom format_kind, classified against the multi_placement_takeover format_shape, with format_schema pointing at NYTimes's hosted schema. Buyer agents fetch the schema by uri@digest (cached, immutable) and validate the manifest structurally.",
+ "data": {
+ "format_kind": "custom",
+ "format_shape": "multi_placement_takeover",
+ "format_schema": {
+ "uri": "https://nytimes.adcp/schemas/formats/homepage_takeover_v3",
+ "digest": "sha256:e1d4f6a9c2b5e8d1f4a7c0b3e6d9f2a5c8b1e4d7f0a3c6b9e2d5f8a1c4b7e0a3"
+ },
+ "capability_id": "nytimes_homepage_takeover_premium",
+ "applies_to_channels": ["display", "olv"],
+ "params": {
+ "components": [
+ { "placement_type": "homepage_skin", "required": true },
+ { "placement_type": "preroll_video", "required": true },
+ { "placement_type": "sponsorship_lockup", "required": true }
+ ],
+ "exclusivity_window_hours": 24,
+ "ssl_required": true
+ }
+ }
+ }
+ ]
+}
diff --git a/static/schemas/source/core/product.json b/static/schemas/source/core/product.json
index 29bc6aff5b..30b13fe2d7 100644
--- a/static/schemas/source/core/product.json
+++ b/static/schemas/source/core/product.json
@@ -36,11 +36,19 @@
},
"format_ids": {
"type": "array",
- "description": "Array of supported creative format IDs - structured format_id objects with agent_url and id",
+ "description": "v1 path: array of supported creative format IDs (structured format_id objects with agent_url and id). Products MAY use either `format_ids` (v1) or `format_options` (v2 inline declarations) — not both. v1 named formats remain supported through the deprecation cycle.",
"items": {
"$ref": "/schemas/core/format-id.json"
}
},
+ "format_options": {
+ "type": "array",
+ "minItems": 1,
+ "description": "v2 path: one or more inline format declarations the product accepts. Each element narrows a canonical format with parameters, slots, and platform_extensions. The 90% case is a single-element array (one canonical narrowed for the product). Multi-element use cases: a product that accepts EITHER a third-party-hosted creative (e.g., Flashtalking-served `html5`) OR an internal `display_tag`; a video product that accepts a hosted `video_hosted` upload OR a `video_vast` tag. Buyers pick which option they're shipping at `sync_creatives` time by aligning their manifest to the matching declaration's `format_kind` and slots. Mutually exclusive with `format_ids`.",
+ "items": {
+ "$ref": "/schemas/core/product-format-declaration.json"
+ }
+ },
"placements": {
"type": "array",
"description": "Optional array of specific placements within this product. When provided, buyers can target specific placements when assigning creatives.",
@@ -265,42 +273,78 @@
},
"product_card": {
"type": "object",
- "description": "Optional standard visual card (300x400px) for displaying this product in user interfaces. Can be rendered via preview_creative or pre-generated.",
+ "description": "Optional standard visual card for displaying this product in user interfaces (catalog browsers, dashboards, agent UIs). Distinct from `format` — product_card describes the UI rendering of the product itself, not the ad creative the product accepts. Typed inline; no format_id indirection. Receivers render the card directly from these fields.",
"properties": {
- "format_id": {
- "$ref": "/schemas/core/format-id.json",
- "description": "Creative format defining the card layout (typically product_card_standard)"
+ "image": {
+ "$ref": "/schemas/core/assets/image-asset.json",
+ "description": "Hero image for the card. Recommended ~300x400 (4:3 portrait) for the standard card layout; receivers may scale."
+ },
+ "title": {
+ "type": "string",
+ "description": "Card title (typically the product name).",
+ "maxLength": 60
+ },
+ "description": {
+ "type": "string",
+ "description": "Short descriptive blurb shown below the title.",
+ "maxLength": 200
},
- "manifest": {
- "type": "object",
- "description": "Asset manifest for rendering the card, structure defined by the format",
- "additionalProperties": true
+ "price_label": {
+ "type": "string",
+ "description": "Formatted price or pricing summary (e.g., 'From $5 CPM', 'Auction floor $0.50 CPC'). Free-text — receivers render verbatim.",
+ "maxLength": 30
+ },
+ "cta_label": {
+ "type": "string",
+ "description": "Call-to-action button label (e.g., 'View details', 'Get proposal').",
+ "maxLength": 25
}
},
- "required": [
- "format_id",
- "manifest"
- ],
"additionalProperties": true
},
"product_card_detailed": {
"type": "object",
- "description": "Optional detailed card with carousel and full specifications. Provides rich product presentation similar to media kit pages.",
+ "description": "Optional detailed card with hero + carousel + structured specifications, for rich product presentation (media-kit-style pages, full product detail views). Distinct from `format` — describes the UI rendering of the product itself, not the ad creative the product accepts. Typed inline; no format_id indirection.",
"properties": {
- "format_id": {
- "$ref": "/schemas/core/format-id.json",
- "description": "Creative format defining the detailed card layout (typically product_card_detailed)"
+ "hero_image": {
+ "$ref": "/schemas/core/assets/image-asset.json",
+ "description": "Primary hero image at the top of the detailed view."
+ },
+ "carousel_images": {
+ "type": "array",
+ "description": "Additional images for a swipeable carousel below the hero.",
+ "items": { "$ref": "/schemas/core/assets/image-asset.json" }
+ },
+ "title": {
+ "type": "string",
+ "description": "Page title (typically the product name)."
},
- "manifest": {
- "type": "object",
- "description": "Asset manifest for rendering the detailed card, structure defined by the format",
- "additionalProperties": true
+ "description": {
+ "type": "string",
+ "description": "Full descriptive copy. Markdown allowed in client renderers that support it; otherwise treat as plain text."
+ },
+ "specifications": {
+ "type": "array",
+ "description": "Structured key/value specifications (e.g., 'Aspect ratio: 9:16', 'Duration: 30s'). Each item is a labeled fact about the product.",
+ "items": {
+ "type": "object",
+ "required": ["label", "value"],
+ "properties": {
+ "label": { "type": "string", "maxLength": 60 },
+ "value": { "type": "string", "maxLength": 200 }
+ },
+ "additionalProperties": true
+ }
+ },
+ "price_label": {
+ "type": "string",
+ "description": "Formatted price or pricing summary."
+ },
+ "cta_label": {
+ "type": "string",
+ "description": "Call-to-action button label."
}
},
- "required": [
- "format_id",
- "manifest"
- ],
"additionalProperties": true
},
"collections": {
@@ -449,10 +493,23 @@
"name",
"description",
"publisher_properties",
- "format_ids",
"delivery_type",
"pricing_options",
"reporting_capabilities"
],
+ "oneOf": [
+ {
+ "title": "v1 Product (named-format reference)",
+ "description": "Product references one or more named formats by structured format_id ({ agent_url, id }). The v1 path; remains supported through 4.x.",
+ "required": ["format_ids"],
+ "not": { "required": ["format_options"] }
+ },
+ {
+ "title": "v2 Product (inline format declarations)",
+ "description": "Product carries one or more inline ProductFormatDeclarations, each narrowing a canonical format. The v2 path introduced by RFC #3305. A single-element `format_options` array is the 90% case; multi-element arrays declare that the product accepts any of the listed format options.",
+ "required": ["format_options"],
+ "not": { "required": ["format_ids"] }
+ }
+ ],
"additionalProperties": true
}
diff --git a/static/schemas/source/creative/asset-types/index.json b/static/schemas/source/creative/asset-types/index.json
index 3f0abcb2a5..d4cd88bd3c 100644
--- a/static/schemas/source/creative/asset-types/index.json
+++ b/static/schemas/source/creative/asset-types/index.json
@@ -66,6 +66,11 @@
"schema": "/schemas/core/assets/javascript-asset.json",
"typical_use": "Third-party tags, custom interaction logic, analytics"
},
+ "zip": {
+ "description": "Bundled creative archive (zip) containing index.html and supporting assets",
+ "schema": "/schemas/core/assets/zip-asset.json",
+ "typical_use": "HTML5 banner bundles with index.html + CSS + JS + images, MRAID-compatible interactive ads"
+ },
"brief": {
"description": "Campaign-level creative context (creative brief)",
"schema": "/schemas/core/assets/brief-asset.json",
diff --git a/static/schemas/source/creative/scenes.json b/static/schemas/source/creative/scenes.json
new file mode 100644
index 0000000000..5fc2570c1b
--- /dev/null
+++ b/static/schemas/source/creative/scenes.json
@@ -0,0 +1,71 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "/schemas/creative/scenes.json",
+ "title": "Scenes",
+ "description": "Typed scene-by-scene structure used as input to build_creative for generative video platforms. Renamed from 'storyboard' to avoid collision with the testing-harness storyboard concept used elsewhere in AdCP. Distinct from `reference-asset.json` `purpose: 'storyboard'`, which describes a reference asset (image, video, document) serving as visual direction — Scenes is the structured plan; a storyboard reference asset is the visual inspiration that may inform such a plan.",
+ "type": "object",
+ "required": ["scenes"],
+ "properties": {
+ "scenes": {
+ "type": "array",
+ "minItems": 1,
+ "description": "Ordered list of scenes that compose the generated video. Sum of `duration_ms` across scenes should match the target video duration.",
+ "items": {
+ "type": "object",
+ "required": ["order", "duration_ms", "description"],
+ "properties": {
+ "order": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "1-indexed sequence position of this scene in the final video."
+ },
+ "duration_ms": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "Duration of this scene in milliseconds."
+ },
+ "description": {
+ "type": "string",
+ "description": "Visual description of the scene for synthesis (what should appear, action, mood, framing)."
+ },
+ "vo": {
+ "type": "string",
+ "description": "Voiceover script for this scene (optional)."
+ },
+ "caption": {
+ "type": "string",
+ "description": "On-screen caption text for this scene (optional)."
+ }
+ },
+ "additionalProperties": true
+ }
+ }
+ },
+ "additionalProperties": true,
+ "examples": [
+ {
+ "scenes": [
+ {
+ "order": 1,
+ "duration_ms": 3000,
+ "description": "Open on a sunset beach with the brand logo overlay",
+ "vo": "Welcome to summer with Acme",
+ "caption": "Summer Sale Now"
+ },
+ {
+ "order": 2,
+ "duration_ms": 4000,
+ "description": "Product close-up — limited-edition sneakers, slow zoom",
+ "vo": "Limited edition. Hand-crafted.",
+ "caption": "Up to 50% Off"
+ },
+ {
+ "order": 3,
+ "duration_ms": 3000,
+ "description": "Logo end-card with CTA button overlay",
+ "caption": "Shop Now"
+ }
+ ]
+ }
+ ]
+}
diff --git a/static/schemas/source/creative/validate-input-request.json b/static/schemas/source/creative/validate-input-request.json
new file mode 100644
index 0000000000..35bb22da80
--- /dev/null
+++ b/static/schemas/source/creative/validate-input-request.json
@@ -0,0 +1,47 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "/schemas/creative/validate-input-request.json",
+ "title": "Validate Input Request",
+ "description": "Request payload for the validate_input task. Lets buyers dry-run a creative manifest against canonical formats and/or specific products before committing to a render. Cheaper than preview_creative (no synthesis cost). Used by build_creative internally to validate inputs before producing output. For genuinely nondeterministic generative platforms (Veo/Sora/Runway-class) where predictive validation is impossible, the platform's own post-synthesis QA loop applies — validate_input is the predictable-case primitive.",
+ "type": "object",
+ "required": ["manifest"],
+ "properties": {
+ "manifest": {
+ "$ref": "/schemas/core/creative-manifest.json",
+ "description": "Creative manifest to validate."
+ },
+ "format_ids": {
+ "type": "array",
+ "items": { "type": "string" },
+ "description": "Canonical format names to validate against (e.g., ['video_hosted', 'video_vast']). Each is a canonical format identifier or a third-party URI form. Multi-format support enables universal-creative scenarios where one manifest targets multiple sellers' format declarations."
+ },
+ "product_ids": {
+ "type": "array",
+ "items": { "type": "string" },
+ "description": "Specific product IDs to validate against. The seller validates the manifest against each product's inline ProductFormatDeclaration narrowing of the canonical."
+ }
+ },
+ "additionalProperties": true,
+ "examples": [
+ {
+ "description": "Dry-run a video manifest against canonical video_vertical and a specific Meta product",
+ "data": {
+ "manifest": {
+ "format_id": { "agent_url": "https://creative.adcontextprotocol.org", "id": "video_30s" },
+ "assets": {
+ "video_main": {
+ "asset_type": "video",
+ "url": "https://cdn.acme.example/spring-30s.mp4",
+ "duration_ms": 30000,
+ "width": 1080,
+ "height": 1920
+ }
+ },
+ "brand": { "domain": "acme.example" }
+ },
+ "format_ids": ["video_hosted"],
+ "product_ids": ["meta_reels_us"]
+ }
+ }
+ ]
+}
diff --git a/static/schemas/source/creative/validate-input-response.json b/static/schemas/source/creative/validate-input-response.json
new file mode 100644
index 0000000000..f88cce43d4
--- /dev/null
+++ b/static/schemas/source/creative/validate-input-response.json
@@ -0,0 +1,16 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "/schemas/creative/validate-input-response.json",
+ "title": "Validate Input Response",
+ "description": "Response payload for the validate_input task. Returns per-target validation results — one entry per format_id or product_id requested. `predicted` carries the platform's pre-flight estimate (e.g., predicted audio duration from text-length analysis), NOT the actual output — there is no protocol state for orphaned out-of-spec artifacts. For nondeterministic generative platforms, the QA-loop obligation means out-of-spec output never reaches this surface; instead, build_creative returns task_failed with synthesis_failed reason.\n\nThe `ValidateInputResult` type is split into its own schema (`/schemas/creative/validate-input-result.json`) rather than inlined here because the same per-target shape is intended for reuse by adjacent async-validation surfaces (planned: per-batch result envelopes on `build_creative` async paths, and asynchronous canonical-against-product validation in `sync_creatives`). Producers that only need the synchronous batch shape today MAY treat the split as YAGNI, but the schema reuse anchors the violation/retry shape so downstream surfaces don't drift.",
+ "type": "object",
+ "required": ["results"],
+ "properties": {
+ "results": {
+ "type": "array",
+ "items": { "$ref": "/schemas/creative/validate-input-result.json" },
+ "description": "Per-target validation results."
+ }
+ },
+ "additionalProperties": true
+}
diff --git a/static/schemas/source/creative/validate-input-result.json b/static/schemas/source/creative/validate-input-result.json
new file mode 100644
index 0000000000..17f144a5bb
--- /dev/null
+++ b/static/schemas/source/creative/validate-input-result.json
@@ -0,0 +1,78 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "/schemas/creative/validate-input-result.json",
+ "title": "Validate Input Result",
+ "description": "Per-target result of a validate_input call.",
+ "type": "object",
+ "required": ["target", "ok"],
+ "properties": {
+ "target": {
+ "type": "object",
+ "required": ["kind", "id"],
+ "properties": {
+ "kind": {
+ "type": "string",
+ "enum": ["canonical", "product", "third_party_format"]
+ },
+ "id": {
+ "type": "string",
+ "description": "Canonical format name (e.g., 'image'), product_id, or URI-form format_id."
+ }
+ },
+ "additionalProperties": true
+ },
+ "ok": {
+ "type": "boolean",
+ "description": "True when the manifest validates against the target."
+ },
+ "violations": {
+ "type": "array",
+ "description": "When ok is false, the specific constraints the manifest fails to meet.",
+ "items": {
+ "type": "object",
+ "required": ["rule", "field"],
+ "properties": {
+ "rule": {
+ "type": "string",
+ "description": "Rule name (e.g., 'duration_ms_range', 'aspect_ratio', 'max_file_size_kb')."
+ },
+ "expected": {
+ "description": "Expected value or range (e.g., '28000-32000', '9:16', 200)."
+ },
+ "predicted": {
+ "description": "Platform's pre-flight estimate for this field (NOT the actual output — there is no protocol state for orphaned out-of-spec artifacts). For TTS, this might be the predicted audio duration from text-length analysis. Helps the buyer fix the input before committing to a build."
+ },
+ "field": {
+ "type": "string",
+ "description": "Path to the violating field (e.g., 'assets.video_main.duration_ms')."
+ },
+ "retry_with": {
+ "type": "object",
+ "description": "Optional advisory adjustment hint. Platforms MAY suggest a corrected input shape; buyers MUST treat this as advisory, not authoritative.",
+ "additionalProperties": true
+ }
+ },
+ "additionalProperties": true
+ }
+ }
+ },
+ "additionalProperties": true,
+ "examples": [
+ {
+ "description": "Manifest fails canonical video_vertical because its duration is too long",
+ "data": {
+ "target": { "kind": "canonical", "id": "video_vertical" },
+ "ok": false,
+ "violations": [
+ {
+ "rule": "duration_ms_range",
+ "expected": "3000-90000",
+ "predicted": 91500,
+ "field": "assets.video_main.duration_ms",
+ "retry_with": { "trim_ms_from_end": 1500 }
+ }
+ ]
+ }
+ }
+ ]
+}
diff --git a/static/schemas/source/formats/canonical/_base.json b/static/schemas/source/formats/canonical/_base.json
new file mode 100644
index 0000000000..359e69471d
--- /dev/null
+++ b/static/schemas/source/formats/canonical/_base.json
@@ -0,0 +1,103 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "/schemas/formats/canonical/_base.json",
+ "title": "Canonical Format Base",
+ "description": "Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).",
+ "type": "object",
+ "properties": {
+ "status": {
+ "type": "string",
+ "enum": ["stable", "preview", "deprecated"],
+ "default": "stable",
+ "description": "Stability tier for this canonical format. `stable` (default): the schema and tracking model are committed and any breaking changes go through normal major-version deprecation. `preview`: shipped for early adoption but the parameter shape and tracking model MAY break in a subsequent minor release once 2-3 adopters have built against it; buyers should treat preview canonicals as experimental and plan for migration. **Stabilization rubric**: a preview canonical is promoted to `stable` once (a) at least 2 production adopters have built against it AND (b) 90 consecutive days have passed without a breaking change to its parameter shape. Each preview canonical also carries a `migration_target_version` indicating the version by which the working group expects to either stabilize it or surface a breaking revision. `deprecated`: replacement is available; existing adopters supported through the deprecation cycle but new adoption is discouraged. Producers SHOULD include this field on `preview` and `deprecated` canonicals; absence is interpreted as `stable`."
+ },
+ "since_version": {
+ "type": "string",
+ "description": "AdCP release-precision version that introduced this canonical (e.g., '3.1', '3.2.0'). Lets adopters reason about minimum protocol version requirements when consuming a format declaration."
+ },
+ "migration_target_version": {
+ "type": "string",
+ "description": "For `preview` canonicals: the AdCP release-precision version by which the working group expects to either promote the canonical to `stable` or surface a breaking revision. Lets adopters time their migration. Unset for `stable` canonicals (they migrate via the normal major-version deprecation cycle). For `deprecated` canonicals, indicates the release in which the canonical will be removed."
+ },
+ "composition_model": {
+ "type": "string",
+ "enum": ["deterministic", "algorithmic"],
+ "description": "Whether the surface composes deterministically (buyer can predict per-slot rendering — sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing — responsive_creative, agent_placement)."
+ },
+ "provenance_required": {
+ "type": "boolean",
+ "description": "When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent."
+ },
+ "platform_extensions": {
+ "type": "array",
+ "description": "Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.",
+ "items": { "$ref": "/schemas/core/platform-extension-ref.json" }
+ },
+ "tracking_extensions": {
+ "type": "array",
+ "description": "Subset of platform_extensions specifically scoped to tracking concerns (pixel IDs, conversion event taxonomies, viewability vendors, OM-SDK partners). Functionally equivalent to listing tracking-related extensions under platform_extensions; the separate field surfaces \"what's tracking-related\" without forcing buyers to fetch each extension definition to find out. When present, every entry MUST also be present in (or implied by) platform_extensions. Producers MAY omit tracking_extensions and put everything under platform_extensions; the split is for buyer convenience, not schema enforcement.",
+ "items": { "$ref": "/schemas/core/platform-extension-ref.json" }
+ },
+ "synthesis_nondeterministic": {
+ "type": "boolean",
+ "description": "When true, the format's production pipeline is genuinely nondeterministic — the platform cannot guarantee that synthesis from a given input set produces in-spec output. Veo / Sora / Runway-class generative video, and other AI-synthesis flows where output dimensions, duration, or quality vary per run. Implies a different validation contract: predictive `validate_input` is impossible; the platform's own post-synthesis QA loop applies; if the QA loop exhausts without producing a valid artifact, `build_creative` returns task_failed with a synthesis_failed reason. Distinct from `composition_model` (which describes how the surface composes per-slot rendering, not whether synthesis is deterministic). When false or absent, the format's production is predictable enough that `validate_input` can predict output properties from input properties.\n\n**Compatibility with production-source enums** (`audio_source` / `image_source` / `video_source` / `item_production_model`): `synthesis_nondeterministic: true` MAY pair with any of `seller_pre_rendered_from_brief`, `seller_human_designed`, or `agent_synthesized` (the QA loop is concept-level, not source-specific — \"seller renders from brief but each retry differs\" is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.",
+ "default": false
+ },
+ "slots": {
+ "type": "array",
+ "description": "Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.",
+ "items": {
+ "type": "object",
+ "required": ["asset_group_id", "asset_type"],
+ "properties": {
+ "asset_group_id": {
+ "type": "string",
+ "description": "Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings."
+ },
+ "asset_type": {
+ "type": "string",
+ "enum": ["image", "video", "audio", "text", "markdown", "url", "html", "css", "javascript", "vast", "daast", "webhook", "brief", "catalog", "zip", "object"],
+ "description": "Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `object` is a fallback for structured non-asset inputs that don't fit a primitive asset_type (rare; prefer specific types when possible)."
+ },
+ "required": {
+ "type": "boolean",
+ "description": "Whether this slot is required for a valid manifest.",
+ "default": false
+ },
+ "min": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "Minimum count for repeatable / pool slots."
+ },
+ "max": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "Maximum count for repeatable / pool slots."
+ },
+ "max_chars": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "Per-slot character limit for text / markdown / brief assets."
+ },
+ "max_size_kb": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "Per-slot file size limit in kilobytes for image / video / audio / zip assets."
+ },
+ "description": {
+ "type": "string",
+ "description": "Human-readable description of what the slot expects from the buyer."
+ }
+ },
+ "additionalProperties": true
+ }
+ },
+ "production_window_business_days": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative)."
+ }
+ },
+ "additionalProperties": true,
+ "x-canonical-policy-required-params-not-enforced": "Canonical format schemas intentionally do NOT mark any of their parameter fields as `required` and use `additionalProperties: true`. The canonical is the loose contract — describing what parameters MAY narrow it. Products narrow the canonical with concrete values (and may also leave most parameters absent — buyers infer behavior from the canonical's prose description for any unspecified parameter). This is intentional, not an oversight: enforcing required params on the canonical would force every product to declare every parameter, which is the wrong direction (we want products to declare only the values that differ from sensible canonical defaults). Adopters writing strict validators should validate against the canonical's structural shape (what fields ARE valid) and apply business rules separately for product-level narrowing minima."
+}
diff --git a/static/schemas/source/formats/canonical/agent_placement.json b/static/schemas/source/formats/canonical/agent_placement.json
new file mode 100644
index 0000000000..4d3ada6fcf
--- /dev/null
+++ b/static/schemas/source/formats/canonical/agent_placement.json
@@ -0,0 +1,52 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "/schemas/formats/canonical/agent_placement.json",
+ "title": "Canonical Format: Agent Placement (AI-surface sponsored placement)",
+ "description": "Sponsored placement integrated into an AI-surface's response to a user. Buyer supplies a `BrandRef` (resolving brand.json for context), an optional `offering_ref` to focus the mention on a specific offering, and an optional `landing_page_url` the surface MAY attach as a citation. The surface (LLM, voice assistant, sponsored-search ranker) composes a natural-language mention, sponsored card, or audio snippet within its response to a user query. **Composition is algorithmic** — the agent chooses phrasing and presentation. Output asset_type varies by surface: `text` for chat UIs and sponsored search snippets; `audio` (synthesized) for voice assistants; `card` for structured AI-surface result cards. Tracking model: mention-level impression + attribution events; per-mention id keys back to brand and offering. Distinct from `si_chat` (which is the user-converses-with-brand's-agent pattern — brand owns the conversational surface) and from `sponsored_placement` (retail-media catalog-driven). Parallels `sponsored_placement` structurally: both are surface-composed placements; agent_placement is for AI/agentic surfaces, sponsored_placement is for retail media.\n\n**Stability:** preview. The parameter shape and tracking model are still settling as ChatGPT, Perplexity, Gemini, and voice-assistant surfaces ship the first agent_placement integrations. Expect schema breakage in 3.2 once 2-3 adopters have built against this canonical and the surface composition model converges.",
+ "allOf": [{ "$ref": "/schemas/formats/canonical/_base.json" }],
+ "properties": {
+ "status": {
+ "default": "preview"
+ },
+ "slots": {
+ "default": [
+ { "asset_group_id": "offering_ref", "asset_type": "text", "required": false },
+ { "asset_group_id": "landing_page_url", "asset_type": "url", "required": false }
+ ],
+ "description": "agent_placement has minimal buyer-shipped slots — the surface composes the rendered output from brand context (resolved via the manifest's top-level `brand` BrandRef) plus optional offering_ref and landing_page_url assets. None of these assets are rendered verbatim by the buyer; the agent chooses how to use them."
+ },
+ "output_modality": {
+ "type": "string",
+ "enum": ["text", "audio", "card"],
+ "description": "How the surface presents the mention. `text` = inline text (chat, search snippet). `audio` = TTS-synthesized voice. `card` = structured card with optional image + text."
+ },
+ "max_mention_length_chars": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "For text output: maximum length of the surface-composed mention text."
+ },
+ "max_mention_duration_ms": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "For audio output: maximum duration of the spoken mention in milliseconds."
+ },
+ "supports_offering_reference": {
+ "type": "boolean",
+ "description": "Whether the product accepts an offering reference (specific product/service to promote within the mention) in addition to brand context."
+ },
+ "supports_landing_page_url": {
+ "type": "boolean",
+ "description": "Whether the surface attaches a landing page URL to the mention (citation, learn-more link)."
+ },
+ "tone_constraints": {
+ "type": "array",
+ "items": { "type": "string" },
+ "description": "**Advisory only.** Buyer-declared brand-voice preferences the surface SHOULD honor (e.g., ['formal', 'no_superlatives']). LLM/agentic surfaces have no protocol-level mechanism to verify enforcement — adopters that need hard guarantees should rely on brand.json voice declarations and post-mention review rather than this field. Future revisions may tie this to a structured tone vocabulary; for now treat as free-text guidance."
+ },
+ "disclosure_required": {
+ "type": "boolean",
+ "description": "Whether the surface must include an explicit sponsorship disclosure label."
+ }
+ },
+ "additionalProperties": true
+}
diff --git a/static/schemas/source/formats/canonical/audio_daast.json b/static/schemas/source/formats/canonical/audio_daast.json
new file mode 100644
index 0000000000..f0a50b7d28
--- /dev/null
+++ b/static/schemas/source/formats/canonical/audio_daast.json
@@ -0,0 +1,44 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "/schemas/formats/canonical/audio_daast.json",
+ "title": "Canonical Format: DAAST Audio",
+ "description": "DAAST-tag-delivered audio creative (audio analog of VAST). Slot: `daast_tag` (daast asset, URL or inline XML). Tracking model: DAAST events inherent to the spec — `impression`, `firstQuartile`, `midpoint`, `thirdQuartile`, `complete`, `start`, `pause`, `resume`, `mute`, `unmute`, `clickTracking`, `error`. Distinct from `audio_hosted` (direct file with external tracking).",
+ "allOf": [{ "$ref": "/schemas/formats/canonical/_base.json" }],
+ "properties": {
+ "slots": {
+ "default": [
+ { "asset_group_id": "daast_tag", "asset_type": "daast", "required": true },
+ { "asset_group_id": "landing_page_url", "asset_type": "url", "required": false }
+ ],
+ "description": "Default slots for audio_daast canonical. Buyer ships a DAAST tag (URL or inline XML, 1.0 or 1.1) plus an optional clickthrough URL. Tracking events are inherent to DAAST and don't require explicit slots."
+ },
+ "daast_version": {
+ "type": "string",
+ "enum": ["1.0", "1.1"]
+ },
+ "duration_ms_range": {
+ "type": "array",
+ "items": { "type": "integer", "minimum": 0 },
+ "minItems": 2,
+ "maxItems": 2
+ },
+ "duration_ms_exact": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "linear_required": {
+ "type": "boolean"
+ },
+ "max_wrapper_depth": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "ssl_required": {
+ "type": "boolean"
+ },
+ "companion_image_required": {
+ "type": "boolean"
+ }
+ },
+ "additionalProperties": true
+}
diff --git a/static/schemas/source/formats/canonical/audio_hosted.json b/static/schemas/source/formats/canonical/audio_hosted.json
new file mode 100644
index 0000000000..0169668a1b
--- /dev/null
+++ b/static/schemas/source/formats/canonical/audio_hosted.json
@@ -0,0 +1,79 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "/schemas/formats/canonical/audio_hosted.json",
+ "title": "Canonical Format: Hosted Audio",
+ "description": "Direct audio creative — buyer ships an `audio` asset (mp3/aac/wav) for asset-driven products, or ships a `script` / `creative_brief` text asset for products where the seller produces audio internally (podcast host-reads, TTS synthesis). Optional companion slots: `companion_image`, `brand_name`, `landing_page_url`. Tracking model: standard impression + completion + companion-image-click pixels via universal_macros. Distinct from `audio_daast` (DAAST tag, inherent DAAST event tracking). For host-reads and synthesized audio, the format declares `audio_source: 'publisher_host_recorded'` or `'agent_synthesized'` plus `buyer_audio_acceptance: 'rejected'`; the format's `slots` declaration enumerates which assets the buyer ships (e.g., `script` text asset for host-reads). The seller decides how to consume each asset (render verbatim vs produce audio from text) — there is no separate manifest 'inputs' map; everything the buyer ships goes in `assets`.",
+ "allOf": [{ "$ref": "/schemas/formats/canonical/_base.json" }],
+ "properties": {
+ "slots": {
+ "default": [
+ { "asset_group_id": "audio_main", "asset_type": "audio", "required": true },
+ { "asset_group_id": "companion_image", "asset_type": "image", "required": false },
+ { "asset_group_id": "brand_name", "asset_type": "text", "required": false },
+ { "asset_group_id": "landing_page_url", "asset_type": "url", "required": false }
+ ],
+ "description": "Default slots for buyer-uploaded audio. Host-read products override with a `script` (asset_type: text) or `creative_brief` (asset_type: brief) slot in place of `audio_main`, plus `audio_source: 'publisher_host_recorded'` and `buyer_audio_acceptance: 'rejected'`. TTS-from-script products override similarly with `audio_source: 'seller_pre_rendered_from_brief'`."
+ },
+ "duration_ms_range": {
+ "type": "array",
+ "items": { "type": "integer", "minimum": 0 },
+ "minItems": 2,
+ "maxItems": 2
+ },
+ "duration_ms_exact": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "audio_codecs": {
+ "type": "array",
+ "items": { "type": "string", "enum": ["mp3", "aac", "wav", "opus", "flac"] }
+ },
+ "audio_sample_rates": {
+ "type": "array",
+ "items": { "type": "integer", "minimum": 1 }
+ },
+ "audio_channels": {
+ "type": "array",
+ "items": { "type": "string", "enum": ["mono", "stereo"] }
+ },
+ "min_bitrate_kbps": { "type": "integer", "minimum": 1 },
+ "max_bitrate_kbps": { "type": "integer", "minimum": 1 },
+ "loudness_lufs": {
+ "type": "number",
+ "description": "Required integrated loudness in LUFS (typical: -16 for streaming/podcast, -23 for broadcast). Negative values."
+ },
+ "loudness_tolerance_db": {
+ "type": "number",
+ "minimum": 0,
+ "description": "Permitted deviation from loudness_lufs in dB."
+ },
+ "true_peak_dbfs": {
+ "type": "number",
+ "description": "Maximum true-peak level in dBFS (typical: -2)."
+ },
+ "audio_source": {
+ "type": "string",
+ "enum": ["buyer_uploaded", "publisher_host_recorded", "seller_pre_rendered_from_brief", "seller_human_designed", "agent_synthesized"],
+ "default": "buyer_uploaded",
+ "description": "Where the rendered audio comes from. Parallels `image_source` on `image` and `video_source` on `video_hosted`. `buyer_uploaded` (default): buyer ships a pre-rendered audio asset. `publisher_host_recorded`: the publisher's host records the audio (podcast host-read pattern); buyer must use the publisher's build_creative capability. `seller_pre_rendered_from_brief`: buyer ships a brief plus structured copy; seller renders ONE audio file from those inputs at sync_creatives or build_creative time (TTS-from-brief, AI-narration-from-script). `seller_human_designed`: seller's studio team produces the audio manually. `agent_synthesized`: AI synthesis pipeline (TTS or generative audio); pair with `synthesis_nondeterministic: true` when the platform cannot guarantee in-spec output. The `slots` declaration is the binding contract for what the buyer ships; `audio_source` is informational."
+ },
+ "buyer_audio_acceptance": {
+ "type": "string",
+ "enum": ["accepted", "rejected"],
+ "default": "accepted",
+ "description": "Whether the product accepts buyer-uploaded audio. When `rejected`, the buyer cannot ship an audio asset directly — they must use build_creative (or sync_creatives with brief inputs) so the seller produces the audio. Combined with `audio_source`, lets a product declare 'I produce audio from briefs and refuse buyer uploads' (audio_source=`seller_pre_rendered_from_brief`, buyer_audio_acceptance=`rejected`)."
+ },
+ "companion_image_required": {
+ "type": "boolean"
+ },
+ "companion_image_aspect_ratio": {
+ "type": "string"
+ },
+ "companion_image_max_file_size_kb": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "brand_name_max_chars": { "type": "integer", "minimum": 1 }
+ },
+ "additionalProperties": true
+}
diff --git a/static/schemas/source/formats/canonical/display_tag.json b/static/schemas/source/formats/canonical/display_tag.json
new file mode 100644
index 0000000000..3899a81cd9
--- /dev/null
+++ b/static/schemas/source/formats/canonical/display_tag.json
@@ -0,0 +1,61 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "/schemas/formats/canonical/display_tag.json",
+ "title": "Canonical Format: Display Tag",
+ "description": "Third-party-served display tag (JS, iframe, or 1×1 redirect). The buyer's adserver hosts the creative; the seller calls the tag URL at impression time. Slot: `tag_url` (url asset with appropriate `url_type`). Tracking model: opaque to seller — third party serves and measures. Click tracking via redirect URL substitution using universal_macros. Distinct from `image` (static asset hosted by seller) and `html5` (zip bundle hosted by seller).",
+ "allOf": [{ "$ref": "/schemas/formats/canonical/_base.json" }],
+ "properties": {
+ "slots": {
+ "default": [
+ { "asset_group_id": "tag_url", "asset_type": "url", "required": true },
+ { "asset_group_id": "backup_image", "asset_type": "image", "required": false }
+ ],
+ "description": "Default slots for display_tag canonical. Buyer ships a URL pointing at the third-party-served creative (JS, iframe, or 1×1 redirect) plus an optional backup image. Click and impression macros are substituted into the tag URL by the seller using `universal_macros`."
+ },
+ "width": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "Required tag rendering width in pixels."
+ },
+ "height": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "Required tag rendering height in pixels."
+ },
+ "supported_tag_types": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": ["iframe", "javascript", "1x1_redirect"]
+ },
+ "description": "Tag delivery mechanisms accepted."
+ },
+ "ssl_required": {
+ "type": "boolean",
+ "description": "Whether the tag URL must be HTTPS."
+ },
+ "max_redirect_depth": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "Maximum redirect chain depth permitted."
+ },
+ "max_response_time_ms": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "Maximum tag-server response time in milliseconds."
+ },
+ "backup_image_required": {
+ "type": "boolean",
+ "description": "Whether a backup image must accompany the tag for environments that cannot render the third-party tag."
+ },
+ "backup_image_max_size_kb": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "om_sdk_required": {
+ "type": "boolean",
+ "description": "Whether the buyer's tag must integrate IAB Open Measurement SDK for viewability."
+ }
+ },
+ "additionalProperties": true
+}
diff --git a/static/schemas/source/formats/canonical/html5.json b/static/schemas/source/formats/canonical/html5.json
new file mode 100644
index 0000000000..944c875b9c
--- /dev/null
+++ b/static/schemas/source/formats/canonical/html5.json
@@ -0,0 +1,83 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "/schemas/formats/canonical/html5.json",
+ "title": "Canonical Format: HTML5 Banner",
+ "description": "Interactive HTML5 banner delivered as a zip archive. Slot: `html5_bundle` (zip asset). Tracking model: MRAID + IAB Open Measurement (OM-SDK) + click-tag macro substitution + backup image fallback. Receivers unpack the zip, validate internal structure, and serve from CDN. Distinct from `image` (static, non-interactive) and `display_tag` (third-party served). The zip's entry point is typically `index.html`; click handling uses `clickTag` (or `clickTAG`) macro substitution.",
+ "allOf": [{ "$ref": "/schemas/formats/canonical/_base.json" }],
+ "properties": {
+ "slots": {
+ "default": [
+ { "asset_group_id": "html5_bundle", "asset_type": "zip", "required": true },
+ { "asset_group_id": "backup_image", "asset_type": "image", "required": false },
+ { "asset_group_id": "landing_page_url", "asset_type": "url", "required": false }
+ ],
+ "description": "Default slots for html5 canonical. Buyer ships a zip bundle plus optional backup image (required when `backup_image_required: true`) and clickthrough URL. The zip's entry point is typically `index.html`; click handling uses the `clickTag` (or `clickTAG`) macro substituted by the seller at serve time."
+ },
+ "width": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "Required banner width in pixels."
+ },
+ "height": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "Required banner height in pixels."
+ },
+ "max_initial_load_kb": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "Maximum initial-load file size (zip + above-the-fold assets) in kilobytes. IAB display standards: 200 KB for fixed sizes, 100 KB for mobile."
+ },
+ "max_polite_load_kb": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "Maximum polite-load file size after host-initiated subload, in kilobytes. IAB display standards: 500 KB for fixed sizes."
+ },
+ "host_initiated_subload": {
+ "type": "boolean",
+ "description": "Whether the host page must initiate the polite-load phase. IAB-compliant banners require true."
+ },
+ "max_animation_duration_ms": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "Maximum total animation duration in milliseconds. IAB standard: 30000 (30 seconds)."
+ },
+ "max_cpu_load_percent": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 100,
+ "description": "Maximum CPU load percentage during render."
+ },
+ "mraid_required": {
+ "type": "boolean",
+ "description": "Whether MRAID compatibility is required (mobile in-app)."
+ },
+ "mraid_version": {
+ "type": "string",
+ "enum": ["2.0", "3.0"],
+ "description": "Required MRAID version when mraid_required is true."
+ },
+ "om_sdk_required": {
+ "type": "boolean",
+ "description": "Whether IAB Open Measurement SDK integration is required."
+ },
+ "clicktag_macro": {
+ "type": "string",
+ "enum": ["clickTag", "clickTAG"],
+ "description": "Name of the click-tag macro the bundle must use."
+ },
+ "backup_image_required": {
+ "type": "boolean",
+ "description": "Whether a backup image must accompany the zip for non-HTML5 environments."
+ },
+ "backup_image_max_size_kb": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "Maximum backup image file size in kilobytes."
+ },
+ "ssl_required": {
+ "type": "boolean"
+ }
+ },
+ "additionalProperties": true
+}
diff --git a/static/schemas/source/formats/canonical/image.json b/static/schemas/source/formats/canonical/image.json
new file mode 100644
index 0000000000..12ebf29ac5
--- /dev/null
+++ b/static/schemas/source/formats/canonical/image.json
@@ -0,0 +1,73 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "/schemas/formats/canonical/image.json",
+ "title": "Canonical Format: Image",
+ "description": "Static image creative format. Slots: `image_main` (image asset, file or hosted URL), optional `headline` (text), `body_text` (text), `cta` (text/enum), `landing_page_url` (url). Tracking model: impression pixel + click URL via universal_macros, with optional viewability pixel. Distinct from `html5` (interactive bundles) and `display_tag` (third-party served). AR/dimensions narrow to specific sizes via product parameters — covers IAB display sizes (300x250, 728x90, 970x250, etc.) without a separate iab_size enum.",
+ "allOf": [{ "$ref": "/schemas/formats/canonical/_base.json" }],
+ "properties": {
+ "slots": {
+ "default": [
+ { "asset_group_id": "image_main", "asset_type": "image", "required": true },
+ { "asset_group_id": "headline", "asset_type": "text", "required": false },
+ { "asset_group_id": "body_text", "asset_type": "text", "required": false },
+ { "asset_group_id": "landing_page_url", "asset_type": "url", "required": false }
+ ],
+ "description": "Default slots for image canonical. Buyer ships an image asset (file or hosted URL) plus optional headline, body text, and clickthrough URL. Products MAY override the default with narrowing (e.g., make `headline` required, or add a `cta` slot)."
+ },
+ "width": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "Required image width in pixels."
+ },
+ "height": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "Required image height in pixels."
+ },
+ "aspect_ratio": {
+ "type": "string",
+ "pattern": "^[0-9]+(\\.[0-9]+)?:[0-9]+(\\.[0-9]+)?$",
+ "description": "Optional aspect ratio constraint (e.g., '1.91:1', '1:1'). When provided alongside width/height, must agree."
+ },
+ "max_file_size_kb": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "Maximum file size in kilobytes."
+ },
+ "image_formats": {
+ "type": "array",
+ "items": { "type": "string", "enum": ["jpg", "jpeg", "png", "gif", "webp", "svg"] },
+ "description": "Permitted image file formats."
+ },
+ "ssl_required": {
+ "type": "boolean",
+ "description": "Whether the image and its trackers must be served over HTTPS."
+ },
+ "headline_max_chars": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "body_text_max_chars": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "cta_values": {
+ "type": "array",
+ "items": { "type": "string" },
+ "description": "Permitted CTA values for this product (e.g., ['LEARN_MORE', 'SHOP_NOW'])."
+ },
+ "image_source": {
+ "type": "string",
+ "enum": ["buyer_uploaded", "seller_pre_rendered_from_brief", "seller_human_designed", "agent_synthesized"],
+ "default": "buyer_uploaded",
+ "description": "Where the rendered image comes from. Parallels `audio_source` on `audio_hosted`. `buyer_uploaded` (default): buyer ships a pre-rendered image asset; product narrows file format / dimensions / size. `seller_pre_rendered_from_brief`: buyer ships a brief plus structured copy (headline, landing_page_url); seller renders ONE image at sync_creatives or build_creative time and serves it like any deterministic creative (generative-DSP pattern: universalads, Pencil, AdCreative.ai). `seller_human_designed`: seller's design team renders manually from a brief (human-in-the-loop services). `agent_synthesized`: AI synthesis pipeline; pair with `synthesis_nondeterministic: true` when the platform cannot guarantee in-spec output (Veo/Sora/Imagen-class). The `slots` declaration is the binding contract for what the buyer ships; `image_source` is informational and lets buyers understand the production model when picking products."
+ },
+ "buyer_image_acceptance": {
+ "type": "string",
+ "enum": ["accepted", "rejected"],
+ "default": "accepted",
+ "description": "Whether the product accepts buyer-uploaded images. When `rejected`, the buyer cannot ship an image asset directly — they must use build_creative (or sync_creatives with brief inputs) so the seller produces the image. Combined with `image_source`, lets a product declare 'I produce images from briefs and refuse buyer uploads' (image_source=`seller_pre_rendered_from_brief`, buyer_image_acceptance=`rejected`)."
+ }
+ },
+ "additionalProperties": true
+}
diff --git a/static/schemas/source/formats/canonical/image_carousel.json b/static/schemas/source/formats/canonical/image_carousel.json
new file mode 100644
index 0000000000..15e142faab
--- /dev/null
+++ b/static/schemas/source/formats/canonical/image_carousel.json
@@ -0,0 +1,76 @@
+{
+ "$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-shaped objects (a single key with an array value — NOT one key per card, NOT dotted/bracketed paths). Each card carries: `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 per card: `image` and `video` (Meta-style mixed-media); platforms can narrow to image-only or video-only via `allowed_card_asset_types`.\n\nThe manifest's `assets.cards` value is an array of card objects matching `card_shape` below. Example: `\"cards\": [{\"media\": {\"asset_type\": \"image\", \"url\": \"...\"}, \"headline\": \"Buy now\", \"landing_page_url\": {\"asset_type\": \"url\", \"url\": \"...\"}}, ...]`. This is the normative shape — adopters MUST NOT invent per-card key conventions (`card_0_headline`, `cards.0.headline`, etc.).",
+ "allOf": [{ "$ref": "/schemas/formats/canonical/_base.json" }],
+ "properties": {
+ "slots": {
+ "default": [
+ { "asset_group_id": "cards", "asset_type": "object", "required": true, "min": 2, "max": 10 },
+ { "asset_group_id": "primary_text", "asset_type": "text", "required": false },
+ { "asset_group_id": "landing_page_url", "asset_type": "url", "required": false }
+ ],
+ "description": "Default slots for image_carousel. The `cards` slot's value in the manifest is an array of card objects (see `card_shape`); `min` / `max` constrain card count."
+ },
+ "card_shape": {
+ "type": "object",
+ "description": "Normative structure of each card object in the manifest's `assets.cards` array. Adopters MUST honor this shape; product narrowing MAY add per-card platform_extension fields but MUST NOT rename or restructure the listed fields. Constraints on per-card values (max chars, max file size, etc.) live on the format declaration's per-card parameters (`card_image_max_file_size_kb`, `card_headline_max_chars`, etc.).",
+ "properties": {
+ "media": {
+ "description": "The card's primary visual asset. Either an `image` or `video` asset matching `allowed_card_asset_types`."
+ },
+ "headline": {
+ "type": "string",
+ "description": "Optional per-card headline. Length governed by `card_headline_max_chars` on the format declaration."
+ },
+ "landing_page_url": {
+ "description": "Optional per-card click-through URL. `url` asset with `url_type: \"clickthrough\"`."
+ }
+ }
+ },
+ "card_aspect_ratio": {
+ "type": "string",
+ "pattern": "^[0-9]+(\\.[0-9]+)?:[0-9]+(\\.[0-9]+)?$",
+ "description": "Aspect ratio shared across all cards (e.g., '1:1', '1.91:1', '4:5')."
+ },
+ "min_cards": {
+ "type": "integer",
+ "minimum": 2,
+ "description": "Minimum card count (typical: 2 or 3)."
+ },
+ "max_cards": {
+ "type": "integer",
+ "description": "Maximum card count (typical: 6, 10, or 35 depending on platform)."
+ },
+ "allowed_card_asset_types": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": ["image", "video"]
+ },
+ "description": "Asset types each card may carry. Default: ['image']. Polymorphic carousels (Meta) allow ['image', 'video']."
+ },
+ "card_image_max_file_size_kb": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "card_video_max_duration_ms": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "primary_text_max_chars": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "Maximum length of the carousel-level primary text."
+ },
+ "card_headline_max_chars": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "ssl_required": {
+ "type": "boolean"
+ }
+ },
+ "additionalProperties": true
+}
diff --git a/static/schemas/source/formats/canonical/responsive_creative.json b/static/schemas/source/formats/canonical/responsive_creative.json
new file mode 100644
index 0000000000..8c1f29a2ea
--- /dev/null
+++ b/static/schemas/source/formats/canonical/responsive_creative.json
@@ -0,0 +1,58 @@
+{
+ "$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.).\n\n**Stability:** preview. Slot vocabulary and per-slot count limits track Google PMax and Meta Advantage+ surfaces, both of which still ship slot/count/policy changes regularly. Expect schema breakage in 3.2 once 2-3 adopters have built against this canonical and the slot vocabulary stabilizes across surfaces.",
+ "allOf": [{ "$ref": "/schemas/formats/canonical/_base.json" }],
+ "properties": {
+ "status": {
+ "default": "preview"
+ },
+ "slots": {
+ "default": [
+ { "asset_group_id": "headlines", "asset_type": "text", "required": true, "min": 3, "max": 15 },
+ { "asset_group_id": "long_headlines", "asset_type": "text", "required": false, "min": 1, "max": 5 },
+ { "asset_group_id": "descriptions", "asset_type": "text", "required": true, "min": 2, "max": 5 },
+ { "asset_group_id": "images_landscape", "asset_type": "image", "required": false, "min": 1, "max": 20 },
+ { "asset_group_id": "images_square", "asset_type": "image", "required": false, "min": 1, "max": 20 },
+ { "asset_group_id": "images_vertical", "asset_type": "image", "required": false, "min": 1, "max": 20 },
+ { "asset_group_id": "video", "asset_type": "video", "required": false, "min": 0, "max": 5 },
+ { "asset_group_id": "logo", "asset_type": "image", "required": true, "min": 1, "max": 5 },
+ { "asset_group_id": "landing_page_url", "asset_type": "url", "required": true, "min": 1, "max": 1 }
+ ]
+ },
+ "headlines_min": { "type": "integer", "minimum": 0 },
+ "headlines_max": { "type": "integer", "minimum": 0 },
+ "headline_max_chars": { "type": "integer", "minimum": 1 },
+ "long_headlines_min": { "type": "integer", "minimum": 0 },
+ "long_headlines_max": { "type": "integer", "minimum": 0 },
+ "long_headline_max_chars": { "type": "integer", "minimum": 1 },
+ "descriptions_min": { "type": "integer", "minimum": 0 },
+ "descriptions_max": { "type": "integer", "minimum": 0 },
+ "description_max_chars": { "type": "integer", "minimum": 1 },
+ "images_landscape_min": { "type": "integer", "minimum": 0 },
+ "images_landscape_max": { "type": "integer", "minimum": 0 },
+ "images_landscape_aspect_ratio": { "type": "string" },
+ "images_square_min": { "type": "integer", "minimum": 0 },
+ "images_square_max": { "type": "integer", "minimum": 0 },
+ "images_vertical_min": { "type": "integer", "minimum": 0 },
+ "images_vertical_max": { "type": "integer", "minimum": 0 },
+ "videos_min": { "type": "integer", "minimum": 0 },
+ "videos_max": { "type": "integer", "minimum": 0 },
+ "video_min_duration_ms": { "type": "integer", "minimum": 1 },
+ "video_max_duration_ms": { "type": "integer", "minimum": 1 },
+ "logo_min": { "type": "integer", "minimum": 0 },
+ "logo_max": { "type": "integer", "minimum": 0 },
+ "logo_aspect_ratios": {
+ "type": "array",
+ "items": { "type": "string" }
+ },
+ "business_name_max_chars": { "type": "integer", "minimum": 1 },
+ "asset_image_max_file_size_kb": { "type": "integer", "minimum": 1 },
+ "supports_catalog_input": {
+ "type": "boolean",
+ "description": "Whether the product can additionally consume a catalog reference (e.g., PMax with product feed)."
+ }
+ },
+ "additionalProperties": true
+}
diff --git a/static/schemas/source/formats/canonical/sponsored_placement.json b/static/schemas/source/formats/canonical/sponsored_placement.json
new file mode 100644
index 0000000000..961f2b421a
--- /dev/null
+++ b/static/schemas/source/formats/canonical/sponsored_placement.json
@@ -0,0 +1,59 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "/schemas/formats/canonical/sponsored_placement.json",
+ "title": "Canonical Format: Sponsored Placement (retail-media catalog-driven)",
+ "description": "Catalog-driven retail-media format. Slot: `source_catalog` (catalog asset — product/SKU/ASIN/GTIN catalog reference), optional `landing_page_url`. Buyer supplies the catalog reference; surface composes per-item or multi-item rendering using its native placement template. **Composition is deterministic** — buyer can predict per-slot rendering from the catalog item structure. Tracking model: per-item impression + click + conversion (catalog-keyed via offering_id/sku/gtin macros). Covers Amazon Sponsored Products, Criteo Sponsored Products, CitrusAd Sponsored Products. Distinct from `responsive_creative` (algorithmic combinator from buyer pool) and `agent_placement` (text/audio AI-surface composition).",
+ "allOf": [{ "$ref": "/schemas/formats/canonical/_base.json" }],
+ "properties": {
+ "slots": {
+ "default": [
+ { "asset_group_id": "source_catalog", "required": true, "asset_type": "catalog" },
+ { "asset_group_id": "hero_asset", "required": false, "asset_type": "image" },
+ { "asset_group_id": "landing_page_url", "required": false, "asset_type": "url" }
+ ]
+ },
+ "supported_catalog_types": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": ["product", "store", "offering", "hotel", "flight", "vehicle", "real_estate", "education", "destination", "app", "job", "inventory"]
+ },
+ "description": "Catalog types this product accepts."
+ },
+ "min_items": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "Minimum catalog item count buyer must supply."
+ },
+ "max_items": {
+ "type": "integer",
+ "description": "Maximum items considered for placement."
+ },
+ "fanout_mode": {
+ "type": "string",
+ "enum": ["per_item", "multi_item_in_creative", "single_item"],
+ "description": "How items map to delivery: per_item = one ad per catalog item; multi_item_in_creative = composed multi-item ad (Pinterest Collection, Snap Collection); single_item = one ad showing one item."
+ },
+ "required_catalog_fields": {
+ "type": "array",
+ "items": { "type": "string" },
+ "description": "Catalog item fields the seller requires (e.g., ['title', 'image_url', 'price'])."
+ },
+ "supported_id_types": {
+ "type": "array",
+ "items": { "type": "string", "enum": ["asin", "sku", "gtin", "offering_id", "store_id", "hotel_id", "flight_id", "vehicle_id", "listing_id", "program_id", "destination_id", "app_id", "job_id"] },
+ "description": "Catalog identifier types the placement renders against."
+ },
+ "hero_asset_supported": {
+ "type": "boolean",
+ "description": "Whether the buyer can supply a hero/banner asset alongside the catalog (Pinterest Collection pattern)."
+ },
+ "item_production_model": {
+ "type": "string",
+ "enum": ["buyer_uploaded", "seller_pre_rendered_from_brief", "seller_human_designed", "agent_synthesized"],
+ "default": "buyer_uploaded",
+ "description": "How each per-item creative is produced. Aligned with the buyer-side default name (`buyer_uploaded`) used by `image_source` / `video_source` / `audio_source` so SDK codegen emits a single shared 4-value enum across all production-source fields. `buyer_uploaded` (default, current Amazon/Criteo/CitrusAd pattern): the buyer's catalog already contains rendered assets per item; the seller composes the placement using those assets. (\"Uploaded\" reads slightly off for catalog-keyed items where the buyer didn't actively upload bytes — the catalog ingestion already supplied them — but the semantic is the same: rendered bytes are buyer-supplied, not seller-produced.) `seller_pre_rendered_from_brief`: the buyer ships a brief plus the catalog reference; the seller renders one creative per catalog item from the brief at sync_creatives time. `seller_human_designed`: seller's design team produces per-item renders manually. `agent_synthesized`: AI synthesis pipeline produces per-item renders; pair with `synthesis_nondeterministic: true` for Veo/Sora-class generative video applied per item. Captures the multi-output generative pattern (1 brief × N catalog items → N rendered creatives) under the existing canonical without requiring a separate canonical. Distinct from `fanout_mode`, which describes how items map to delivery slots after rendering."
+ }
+ },
+ "additionalProperties": true
+}
diff --git a/static/schemas/source/formats/canonical/video_hosted.json b/static/schemas/source/formats/canonical/video_hosted.json
new file mode 100644
index 0000000000..0eb76971ef
--- /dev/null
+++ b/static/schemas/source/formats/canonical/video_hosted.json
@@ -0,0 +1,100 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "/schemas/formats/canonical/video_hosted.json",
+ "title": "Canonical Format: Hosted Video",
+ "description": "Direct video file (mp4/webm/mov) hosted by the buyer. Slot: `video_main` (video asset, file or hosted URL), optional `headline`, `brand_name`, `cta`, `companion_banner`, `landing_page_url`. Tracking model: IAB Open Measurement SDK + external impression/click/quartile pixels via universal_macros. Orientation is a parameter (vertical 9:16 / horizontal 16:9 / square 1:1); slot shape includes optional `brand_name` (typical for vertical short-form) and optional `companion_banner` (typical for horizontal instream). Distinct from `video_vast` (VAST tag, inherent VAST event tracking) — receivers fire impression and click pixels at delivery time.",
+ "allOf": [{ "$ref": "/schemas/formats/canonical/_base.json" }],
+ "properties": {
+ "slots": {
+ "default": [
+ { "asset_group_id": "video_main", "asset_type": "video", "required": true },
+ { "asset_group_id": "headline", "asset_type": "text", "required": false },
+ { "asset_group_id": "brand_name", "asset_type": "text", "required": false },
+ { "asset_group_id": "companion_banner", "asset_type": "image", "required": false },
+ { "asset_group_id": "landing_page_url", "asset_type": "url", "required": false }
+ ],
+ "description": "Default slots for video_hosted canonical. Buyer ships a video asset (file or hosted URL); optional headline, brand_name (typical for vertical short-form), companion_banner (typical for horizontal instream), and clickthrough URL. Products MAY override or extend the default."
+ },
+ "orientation": {
+ "type": "string",
+ "enum": ["vertical", "horizontal", "square"],
+ "description": "Video orientation. Vertical = 9:16 (Reels, Stories, Shorts). Horizontal = 16:9 (instream, CTV). Square = 1:1 (in-feed)."
+ },
+ "aspect_ratio": {
+ "type": "string",
+ "pattern": "^[0-9]+(\\.[0-9]+)?:[0-9]+(\\.[0-9]+)?$",
+ "description": "Aspect ratio. Inferred from orientation if omitted."
+ },
+ "min_width": { "type": "integer", "minimum": 1 },
+ "min_height": { "type": "integer", "minimum": 1 },
+ "max_width": { "type": "integer", "minimum": 1 },
+ "max_height": { "type": "integer", "minimum": 1 },
+ "duration_ms_range": {
+ "type": "array",
+ "items": { "type": "integer", "minimum": 0 },
+ "minItems": 2,
+ "maxItems": 2,
+ "description": "[min, max] duration in milliseconds."
+ },
+ "duration_ms_exact": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "When set, duration must equal exactly this value."
+ },
+ "video_codecs": {
+ "type": "array",
+ "items": { "type": "string", "enum": ["h264", "h265", "vp8", "vp9", "av1", "prores"] }
+ },
+ "audio_codecs": {
+ "type": "array",
+ "items": { "type": "string", "enum": ["aac", "mp3", "opus", "pcm"] }
+ },
+ "containers": {
+ "type": "array",
+ "items": { "type": "string", "enum": ["mp4", "webm", "mov"] }
+ },
+ "min_bitrate_kbps": { "type": "integer", "minimum": 1 },
+ "max_bitrate_kbps": { "type": "integer", "minimum": 1 },
+ "max_file_size_mb": { "type": "integer", "minimum": 1 },
+ "frame_rates": {
+ "type": "array",
+ "items": { "type": "number" }
+ },
+ "captions": {
+ "type": "string",
+ "enum": ["required", "recommended", "not_required"]
+ },
+ "om_sdk_required": {
+ "type": "boolean"
+ },
+ "headline_max_chars": { "type": "integer", "minimum": 1 },
+ "primary_text_max_chars": { "type": "integer", "minimum": 1 },
+ "brand_name_max_chars": { "type": "integer", "minimum": 1 },
+ "cta_values": {
+ "type": "array",
+ "items": { "type": "string" }
+ },
+ "companion_banner_widths": {
+ "type": "array",
+ "items": { "type": "integer", "minimum": 1 },
+ "description": "Permitted companion banner widths (instream video)."
+ },
+ "companion_banner_heights": {
+ "type": "array",
+ "items": { "type": "integer", "minimum": 1 }
+ },
+ "video_source": {
+ "type": "string",
+ "enum": ["buyer_uploaded", "seller_pre_rendered_from_brief", "seller_human_designed", "agent_synthesized"],
+ "default": "buyer_uploaded",
+ "description": "Where the rendered video comes from. Parallels `audio_source` on `audio_hosted`. `buyer_uploaded` (default): buyer ships a pre-rendered video asset; product narrows codec / dimensions / duration. `seller_pre_rendered_from_brief`: buyer ships a brief plus structured copy; seller renders ONE video at sync_creatives or build_creative time and serves it like any deterministic creative (generative-DSP pattern). `seller_human_designed`: seller's editorial / design team renders manually from a brief. `agent_synthesized`: AI synthesis pipeline; pair with `synthesis_nondeterministic: true` for Veo / Sora / Runway-class flows where output dimensions or duration vary per run. The `slots` declaration is the binding contract for what the buyer ships; `video_source` is informational and lets buyers understand the production model when picking products."
+ },
+ "buyer_video_acceptance": {
+ "type": "string",
+ "enum": ["accepted", "rejected"],
+ "default": "accepted",
+ "description": "Whether the product accepts buyer-uploaded video. When `rejected`, the buyer cannot ship a video asset directly — they must use build_creative (or sync_creatives with brief inputs) so the seller produces the video."
+ }
+ },
+ "additionalProperties": true
+}
diff --git a/static/schemas/source/formats/canonical/video_vast.json b/static/schemas/source/formats/canonical/video_vast.json
new file mode 100644
index 0000000000..2e803d8118
--- /dev/null
+++ b/static/schemas/source/formats/canonical/video_vast.json
@@ -0,0 +1,73 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "/schemas/formats/canonical/video_vast.json",
+ "title": "Canonical Format: VAST Video",
+ "description": "VAST-tag-delivered video creative. Slot: `vast_tag` (vast asset, URL or inline XML, VAST 2.x-4.x). Tracking model: VAST events inherent to the spec — `impression`, `firstQuartile`, `midpoint`, `thirdQuartile`, `complete`, `start`, `pause`, `resume`, `mute`, `unmute`, `expand`, `collapse`, `fullscreen`, `creativeView`, `clickTracking`, `error`. VPAID interactivity via `vpaid_enabled: true` flag. SIMID extensions for interactive video supported as VAST extensions. Orientation is a parameter (vertical / horizontal / square). Distinct from `video_hosted` (direct file with external tracking).",
+ "allOf": [{ "$ref": "/schemas/formats/canonical/_base.json" }],
+ "properties": {
+ "slots": {
+ "default": [
+ { "asset_group_id": "vast_tag", "asset_type": "vast", "required": true },
+ { "asset_group_id": "landing_page_url", "asset_type": "url", "required": false }
+ ],
+ "description": "Default slots for video_vast canonical. Buyer ships a VAST tag (URL or inline XML, VAST 2.x-4.x) plus an optional clickthrough URL (which falls back to the VAST `ClickThrough` element when omitted). Tracking events are inherent to VAST and don't require explicit slots."
+ },
+ "orientation": {
+ "type": "string",
+ "enum": ["vertical", "horizontal", "square"]
+ },
+ "aspect_ratio": {
+ "type": "string",
+ "pattern": "^[0-9]+(\\.[0-9]+)?:[0-9]+(\\.[0-9]+)?$"
+ },
+ "vast_version": {
+ "type": "string",
+ "enum": ["2.0", "3.0", "4.0", "4.1", "4.2"],
+ "description": "Required VAST version."
+ },
+ "vpaid_enabled": {
+ "type": "boolean",
+ "description": "Whether VPAID interactivity is supported. When true, the VAST tag may carry VPAID JS/Flash payloads."
+ },
+ "vpaid_version": {
+ "type": "string",
+ "enum": ["1.0", "2.0"]
+ },
+ "simid_supported": {
+ "type": "boolean",
+ "description": "Whether IAB SIMID interactive video extensions are supported."
+ },
+ "duration_ms_range": {
+ "type": "array",
+ "items": { "type": "integer", "minimum": 0 },
+ "minItems": 2,
+ "maxItems": 2
+ },
+ "duration_ms_exact": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "min_width": { "type": "integer", "minimum": 1 },
+ "max_width": { "type": "integer", "minimum": 1 },
+ "min_height": { "type": "integer", "minimum": 1 },
+ "max_height": { "type": "integer", "minimum": 1 },
+ "linear_required": {
+ "type": "boolean",
+ "description": "Whether the VAST creative must be linear (non-skippable in-stream)."
+ },
+ "skippable_after_ms": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "When skippable, the buyer-side skip threshold in milliseconds (e.g., 5000 for 5-second skippable pre-roll)."
+ },
+ "max_wrapper_depth": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "Maximum VAST wrapper redirect depth permitted."
+ },
+ "ssl_required": {
+ "type": "boolean"
+ }
+ },
+ "additionalProperties": true
+}
diff --git a/static/schemas/source/index.json b/static/schemas/source/index.json
index 4206377261..aba5c9799f 100644
--- a/static/schemas/source/index.json
+++ b/static/schemas/source/index.json
@@ -195,6 +195,18 @@
"$ref": "/schemas/core/offering-asset-group.json",
"description": "A structured group of creative assets within an offering, identified by group ID and asset type"
},
+ "asset-group-vocabulary": {
+ "$ref": "/schemas/core/asset-group-vocabulary.json",
+ "description": "Canonical registry of asset_group_id values with descriptions and v1 alias mapping (e.g., landing_page_url replaces 6 v1 alias names)"
+ },
+ "product-format-declaration": {
+ "$ref": "/schemas/core/product-format-declaration.json",
+ "description": "v2 inline format declaration on products. Keyed by canonical format name; product narrows exactly one canonical with platform-specific parameters."
+ },
+ "platform-extension-ref": {
+ "$ref": "/schemas/core/platform-extension-ref.json",
+ "description": "Reference to a platform extension definition (URI + content digest)."
+ },
"store-item": {
"$ref": "/schemas/core/store-item.json",
"description": "A physical store or location with coordinates, address, and catchment areas for proximity targeting"
@@ -1106,11 +1118,27 @@
"$ref": "/schemas/creative/sync-creatives-response.json",
"description": "Response payload for sync_creatives task"
}
+ },
+ "validate-input": {
+ "request": {
+ "$ref": "/schemas/creative/validate-input-request.json",
+ "description": "Request parameters for validating a creative manifest against canonical formats and/or specific products without committing to a render"
+ },
+ "response": {
+ "$ref": "/schemas/creative/validate-input-response.json",
+ "description": "Response payload for validate_input task with per-target validation results"
+ }
}
},
"asset_types": {
"$ref": "/schemas/creative/asset-types/index.json",
"description": "Asset type definitions for creative manifests"
+ },
+ "build_inputs": {
+ "scenes": {
+ "$ref": "/schemas/creative/scenes.json",
+ "description": "Typed scene-by-scene structure for build_creative input on generative video platforms"
+ }
}
},
"signals": {
diff --git a/static/schemas/source/media-buy/get-products-response.json b/static/schemas/source/media-buy/get-products-response.json
index 8ef545b080..4551875c47 100644
--- a/static/schemas/source/media-buy/get-products-response.json
+++ b/static/schemas/source/media-buy/get-products-response.json
@@ -17,6 +17,35 @@
"$ref": "/schemas/core/product.json"
}
},
+ "extensions": {
+ "type": "object",
+ "description": "Bundled platform-extension definitions referenced by any product in `products`. Keyed by `@` (e.g., `https://meta.adcp/extensions/meta_pixel@sha256:abc...`). When present, lets buyers resolve `platform_extensions` references on product format declarations without a separate fetch. Buyer SDKs cache by URI@digest; subsequent get_products responses MAY omit definitions the buyer already has cached and rely on the digest match. Each value is an extension definition with `extends` (the canonical concept it extends, e.g., `tracking`), `fields` (the schema for additional fields the extension contributes), `version`, and optional `description`.",
+ "patternProperties": {
+ "^https?://[^@]+@sha256:[a-f0-9]{64}$": {
+ "type": "object",
+ "required": ["extends", "fields"],
+ "properties": {
+ "extends": {
+ "type": "string",
+ "description": "Canonical concept this extension extends (e.g., `tracking`, `cta_vocabulary`, `destinations`, `placement`)."
+ },
+ "fields": {
+ "type": "object",
+ "description": "JSON Schema fragment declaring the additional fields this extension contributes."
+ },
+ "version": {
+ "type": "string",
+ "description": "Semantic version of the extension definition. Distinct from the digest — version is human-readable; digest is the integrity check."
+ },
+ "description": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": true
+ }
+ },
+ "additionalProperties": true
+ },
"proposals": {
"type": "array",
"description": "Optional array of proposed media plans with budget allocations across products. Publishers include proposals when they can provide strategic guidance based on the brief. Proposals are actionable - buyers can refine them via follow-up get_products calls within the same session, or execute them directly via create_media_buy.",
diff --git a/static/schemas/source/protocol/get-adcp-capabilities-response.json b/static/schemas/source/protocol/get-adcp-capabilities-response.json
index d92eea00aa..3f553c4749 100644
--- a/static/schemas/source/protocol/get-adcp-capabilities-response.json
+++ b/static/schemas/source/protocol/get-adcp-capabilities-response.json
@@ -922,6 +922,25 @@
"type": "boolean",
"description": "When true, this agent can transform or resize existing manifests via build_creative. The buyer provides a creative_manifest and a target_format_id, and the agent adapts the creative to the new format.",
"default": false
+ },
+ "supported_formats": {
+ "type": "array",
+ "description": "v2 path: format declarations describing which canonical formats this creative agent can produce via `build_creative`. Each entry uses the same `ProductFormatDeclaration` shape as a product's inline `format_options[i]` — `format_kind` discriminator + `params` (canonical's parameter schema including `slots`, dimensions, durations, codecs, character limits, platform_extensions, tracking_extensions). Replaces the v1 `list_creative_formats` discovery surface for creative agents.",
+ "items": {
+ "type": "object",
+ "properties": {
+ "capability_id": {
+ "type": "string",
+ "description": "Stable identifier for this format declaration within the agent (e.g., 'audiostack_audio_30s'). Optional but recommended for declarations that may be referenced over time."
+ },
+ "format": {
+ "$ref": "/schemas/core/product-format-declaration.json",
+ "description": "Format declaration this agent can produce. Same shape as a product's inline `format_options[i]`: `format_kind` + `params` + `slots`."
+ }
+ },
+ "required": ["format"],
+ "additionalProperties": true
+ }
}
},
"additionalProperties": true
diff --git a/tests/schema-validation.test.cjs b/tests/schema-validation.test.cjs
index fd80c951d7..dc3e01a635 100644
--- a/tests/schema-validation.test.cjs
+++ b/tests/schema-validation.test.cjs
@@ -279,10 +279,12 @@ async function runTests() {
await test('Core schemas have appropriate required fields', () => {
const coreSchemas = schemas.filter(([path]) => path.includes('/core/'));
const requiredFieldChecks = {
- 'product.json': ['product_id', 'name', 'description', 'format_ids', 'delivery_type'],
+ // product.json: format_ids OR format_options is required (v1 OR v2 path) — checked separately below
+ // creative-asset.json: format_id OR format_kind is required (v1 OR v2 path) — checked separately below
+ 'product.json': ['product_id', 'name', 'description', 'delivery_type'],
'media-buy.json': ['media_buy_id', 'status', 'total_budget', 'packages'],
'package.json': ['package_id'],
- 'creative-asset.json': ['creative_id', 'name', 'format_id', 'assets'],
+ 'creative-asset.json': ['creative_id', 'name', 'assets'],
'error.json': ['code', 'message']
};
@@ -299,6 +301,31 @@ async function runTests() {
}
}
}
+
+ // product.json: assert v1 (format_ids) OR v2 (format_options) is required via oneOf
+ const productEntry = coreSchemas.find(([p]) => path.basename(p) === 'product.json');
+ if (productEntry) {
+ const [, productSchema] = productEntry;
+ const oneOf = productSchema.oneOf || [];
+ const hasV1Branch = oneOf.some((branch) => (branch.required || []).includes('format_ids'));
+ const hasV2Branch = oneOf.some((branch) => (branch.required || []).includes('format_options'));
+ if (!hasV1Branch || !hasV2Branch) {
+ return `product.json: must have a oneOf with v1 branch (required: ["format_ids"]) and v2 branch (required: ["format_options"]); found v1=${hasV1Branch}, v2=${hasV2Branch}`;
+ }
+ }
+
+ // creative-asset.json: assert v1 (format_id) OR v2 (format_kind) is required via oneOf
+ const creativeAssetEntry = coreSchemas.find(([p]) => path.basename(p) === 'creative-asset.json');
+ if (creativeAssetEntry) {
+ const [, creativeAssetSchema] = creativeAssetEntry;
+ const oneOf = creativeAssetSchema.oneOf || [];
+ const hasV1Branch = oneOf.some((branch) => (branch.required || []).includes('format_id'));
+ const hasV2Branch = oneOf.some((branch) => (branch.required || []).includes('format_kind'));
+ if (!hasV1Branch || !hasV2Branch) {
+ return `creative-asset.json: must have a oneOf with v1 branch (required: ["format_id"]) and v2 branch (required: ["format_kind"]); found v1=${hasV1Branch}, v2=${hasV2Branch}`;
+ }
+ }
+
return true;
});
diff --git a/tests/v2-fixture-validation.test.cjs b/tests/v2-fixture-validation.test.cjs
new file mode 100644
index 0000000000..30b5b23e7d
--- /dev/null
+++ b/tests/v2-fixture-validation.test.cjs
@@ -0,0 +1,146 @@
+#!/usr/bin/env node
+/**
+ * v2 Reference Fixture Validation Test
+ *
+ * Validates the reference Product fixtures at static/examples/products/v2/*.json
+ * against /schemas/core/product.json. These fixtures are the "does it really
+ * work?" check for the v2 RFC (#3305) — concrete fully-valid Product objects
+ * that adopters and tooling can validate against.
+ *
+ * Run: npm run test:v2-fixtures
+ */
+
+const Ajv = require('ajv').default;
+const addFormats = require('ajv-formats').default;
+const fs = require('fs');
+const path = require('path');
+
+const SCHEMAS_DIR = path.resolve(__dirname, '../static/schemas/source');
+const FIXTURES_DIR = path.resolve(__dirname, '../static/examples/products/v2');
+const RESPONSE_FIXTURES_DIR = path.resolve(__dirname, '../static/examples/get_products_responses/v2');
+
+const RED = '\x1b[31m';
+const GREEN = '\x1b[32m';
+const RESET = '\x1b[0m';
+
+function loadAllSchemas(ajv) {
+ function walk(dir) {
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
+ const full = path.join(dir, entry.name);
+ if (entry.isDirectory()) {
+ walk(full);
+ } else if (entry.name.endsWith('.json')) {
+ try {
+ const schema = JSON.parse(fs.readFileSync(full, 'utf8'));
+ if (schema.$id) {
+ try {
+ ajv.addSchema(schema, schema.$id);
+ } catch (e) {
+ // already added
+ }
+ }
+ } catch (e) {
+ // skip non-schema or malformed JSON
+ }
+ }
+ }
+ }
+ walk(SCHEMAS_DIR);
+}
+
+function main() {
+ const ajv = new Ajv({
+ allErrors: true,
+ strict: false,
+ discriminator: true,
+ });
+ addFormats(ajv);
+ loadAllSchemas(ajv);
+
+ const validate = ajv.getSchema('/schemas/core/product.json');
+ if (!validate) {
+ console.error(`${RED}ERROR:${RESET} could not load /schemas/core/product.json from ${SCHEMAS_DIR}`);
+ process.exit(2);
+ }
+
+ if (!fs.existsSync(FIXTURES_DIR)) {
+ console.error(`${RED}ERROR:${RESET} fixtures directory not found: ${FIXTURES_DIR}`);
+ process.exit(2);
+ }
+
+ const fixtures = fs
+ .readdirSync(FIXTURES_DIR)
+ .filter((f) => f.endsWith('.json'))
+ .sort();
+
+ if (fixtures.length === 0) {
+ console.error(`${RED}ERROR:${RESET} no fixtures found in ${FIXTURES_DIR}`);
+ process.exit(2);
+ }
+
+ console.log('v2 Reference Fixture Validation');
+ console.log('================================');
+ console.log(`Schema: /schemas/core/product.json`);
+ console.log(`Fixtures: ${FIXTURES_DIR}`);
+ console.log('');
+
+ let pass = 0;
+ let fail = 0;
+
+ for (const f of fixtures) {
+ const full = path.join(FIXTURES_DIR, f);
+ const fixture = JSON.parse(fs.readFileSync(full, 'utf8'));
+ const valid = validate(fixture);
+ if (valid) {
+ console.log(` ${GREEN}✓${RESET} ${f}`);
+ pass++;
+ } else {
+ console.log(` ${RED}✗${RESET} ${f}`);
+ for (const err of (validate.errors || []).slice(0, 10)) {
+ console.log(` ${err.instancePath || '(root)'}: ${err.message}`);
+ }
+ fail++;
+ }
+ }
+
+ // Validate get_products response fixtures (with bundled extensions) if present
+ if (fs.existsSync(RESPONSE_FIXTURES_DIR)) {
+ const responseValidate = ajv.getSchema('/schemas/media-buy/get-products-response.json');
+ if (responseValidate) {
+ const responseFixtures = fs
+ .readdirSync(RESPONSE_FIXTURES_DIR)
+ .filter((f) => f.endsWith('.json'))
+ .sort();
+ if (responseFixtures.length > 0) {
+ console.log('');
+ console.log('get_products response fixtures:');
+ for (const f of responseFixtures) {
+ const full = path.join(RESPONSE_FIXTURES_DIR, f);
+ const fixture = JSON.parse(fs.readFileSync(full, 'utf8'));
+ const valid = responseValidate(fixture);
+ if (valid) {
+ console.log(` ${GREEN}✓${RESET} ${f}`);
+ pass++;
+ } else {
+ console.log(` ${RED}✗${RESET} ${f}`);
+ for (const err of (responseValidate.errors || []).slice(0, 10)) {
+ console.log(` ${err.instancePath || '(root)'}: ${err.message}`);
+ }
+ fail++;
+ }
+ }
+ }
+ }
+ }
+
+ console.log('');
+ if (fail === 0) {
+ console.log(`${GREEN}✅ All ${pass} v2 reference fixtures validate.${RESET}`);
+ process.exit(0);
+ } else {
+ console.log(`${RED}❌ ${fail} fixture(s) failed validation; ${pass} passed.${RESET}`);
+ process.exit(1);
+ }
+}
+
+main();