Documentation Index
Fetch the complete documentation index at: https://docs.adcontextprotocol.org/llms.txt
Use this file to discover all available pages before exploring further.
Canonical Formats (preview)
TL;DR for adopters reading cold:
- 7 of 12 canonical formats ship
stableat 3.1 GA (image,display_tag,video_hosted,video_vast,audio_hosted,audio_daast,native_in_feed— all re-encodings of IAB / VAST / DAAST / IAB OpenRTB Native 1.2). 5 staypreviewpast GA (html5,image_carousel,sponsored_placement,responsive_creative,agent_placement).- v1 named formats stay first-class through 4.x with a 5.0 sunset floor. Dual-emission is the migration mode; SDKs translate either direction. Realistic v2-only buyer-agent timing is 4.x at the earliest.
- 15 v1→v2 mapping registry entries at GA, 71+ of audited v1 formats are v1-only out of the gate (need seller
canonicalfield or registry PR to project to v2). v2-aware buyers see meaningfully thinner inventory than v1-aware ones through 3.x. Dual-read codepath realistic through 3.3.- Phase 4 (SDK codegen) is the gating dependency for adopter consumption. Schemas are shippable today; the typed-tagged-union ergonomics this design earns land fully only with codegen. The runtime Ajv validator is the load-bearing gate — generated TS/Pydantic types lose
if/thennarrowing onformat_kind: "custom"andresult_kind.
Status: Preview track. The canonical-formats surface is being designed in flight against RFC #3305 and the #3307 implementation branch. Naming note: This work was originally drafted as “creative formats v2” — the v1↔v2 contrast describes the two format-authoring models (legacy named-format registry vs new canonical formats on products). To avoid collision with AdCP-the-protocol’s own version numbering (currently 3.x), file paths, identifiers, and the body of this doc use canonical formats terminology. The v1↔v2 contrast is reserved as schema-description shorthand where it disambiguates the two authoring paths onCanonical formats collapse today’s separate format registry into product-bound declarations. AdCP defines a small set of canonical formats (universal building blocks); sellers’ products carry inlineProduct.format_idsvsProduct.format_options.
ProductFormatDeclarations 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 — canonical formats don’t create a new vocabulary layer for those.
Glossary
| Term | One-line definition |
|---|---|
| Canonical format | One of 12 AdCP-defined format archetypes that products narrow (e.g., image, video_vast, audio_hosted, native_in_feed). 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 ProductFormatDeclarations on a v2 product. The 90% case is single-element; multi-element declares “accepts any of.” |
ProductFormatDeclaration | Inline format declaration: format_kind + params + capability_id (REQUIRED when entries share format_kind; SHOULD be set on every entry per 3.1 so V2 buyers can author against the product via PackageRequest.capability_ids[]) + optional applies_to_channels + optional experimental + optional v1_format_ref[] (always array — see below). |
v1_format_ref | Array of {agent_url, id} references linking a v2 declaration to one or more v1 named formats. Always array; single-ref is [{...}]. Multi-size declarations carry one ref per size (see “Multi-size fan-out” below). |
canonical: annotation | The v1 catalog entry’s projection annotation. Always object form — { kind: "image" } minimum, { kind, asset_source, slots_override } rich form for entries whose v1 shape doesn’t follow the canonical’s defaults (generative, brief-driven). Never bare-string. |
| Sibling refinement principle | Before reaching for a new canonical, check whether asset_source + slots_override + applies_to_channels + event_log surface covers the case. Applied to: generative (image + asset_source: agent_synthesized), broadcast TV (video_hosted + applies_to_channels: [“tv”]), DOOH (image + applies_to_channels: [“dooh”]), native (image + slots_override). No new canonical for any of them. |
capability_id | Stable identifier for a format declaration. REQUIRED when format_options carries multiple declarations sharing the same format_kind (to disambiguate). SHOULD be set on every format_options[] entry — not just when forced by collision — so V2-mental-model buyers can author against the product via PackageRequest.capability_ids[] and creative-manifest.capability_id. Without it, products are still 3.1-conformant but the V2 authoring path is unreachable and buyers fall back to v1 format_ids[]. The 4.0 cutover will tighten this from SHOULD to MUST. |
applies_to_channels | Subset of the product’s declared channels this format declaration applies to. Lets multi-channel products carry per-channel format options. |
experimental | Boolean on canonical (_base.json) and on ProductFormatDeclaration. true = may not work as declared; have a v1 fallback ready. Replaces the earlier status + runtime_status enums (single binary flag rather than two stability axes). |
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). |
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). |
asset_source | Production-source declaration: who renders the asset bytes. Single shared 5-value enum across image / video_hosted / audio_hosted. item_production_model on sponsored_placement covers the same axis with a 4-value subset (drops publisher_host_recorded, which is audio-specific). |
status on canonicals | Spec-maturity axis (stable / preview / deprecated). All canonicals are preview while v2 itself is in preview; per-canonical promotion to stable happens at 3.1 GA based on adopter evidence. |
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, video_brief, 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 | Inline override on BrandRef for per-call brand-kit tweaks (logo, colors, voice, tagline) where brand.json is missing, stale, or inappropriate. Same pattern as industries and data_subject_contestation on BrandRef. |
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 12 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. 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 — domain plus optional brand_id for house-of-brands) resolving brand.json; explicit override via brand_kit_override inline on the BrandRef itself |
The 12 canonical formats
Each canonical lives at/schemas/formats/canonical/<name>.json. Tracking model is format-specific (split by tracking model is why we have 12 instead of, say, 5).
| Canonical | What it is | Tracking |
|---|---|---|
image | Static image, file or hosted URL redirect | Impression pixel + click URL via universal_macros |
html5 | Interactive HTML5 banner (zip asset) | MRAID + OM-SDK + click-tag macro + backup image |
display_tag | Third-party JS/iframe tag URL | Opaque to seller |
image_carousel | Multi-card swipe (polymorphic image/video items) | Per-card pixels + carousel engagement |
video_hosted | Direct video file, orientation parameter | OM-SDK + external impression/click/quartile trackers |
video_vast | VAST tag (URL or inline XML), VAST 2-4.x | Inherent VAST events |
audio_hosted | Direct audio file (or host-read produced via build_creative) | Standard audio impression/completion |
audio_daast | DAAST tag | Inherent DAAST events |
sponsored_placement | Retail-media catalog-driven (Amazon SP, Criteo SP, CitrusAd SP) — REQUIRES source_catalog slot. Not for IAB in-feed native, content-recommendation, or PMax-style algorithmic surfaces. | Per-item catalog-keyed events |
native_in_feed | IAB-shaped in-feed native and content-recommendation widgets (Taboola, Outbrain, Yahoo Native, AdMob Native, in-feed sponsored cards). Slots map 1:1 to IAB OpenRTB Native 1.2. | Renderer-fired pixel_tracker (impression / viewability / click) |
responsive_creative | Buyer asset pool, surface composes combinations (Google Responsive Display/Search Ads, Performance Max, Demand Gen; Meta Advantage+ creative) | Per-asset performance breakdown |
agent_placement | 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 |
experimental — one field, both axes
A canonical (or a seller’s specific product declaration) carries a single experimental: boolean flag. Same semantics as experimental on protocols: ‘this is shipping but may break, evolve, or fail.’ Buyers reading experimental: true SHOULD have a v1 fallback ready and SHOULD validate via validate_input or a sandbox before routing production budget. This replaces the earlier two-axis design (status + runtime_status enums) — collapsed because what buyers actually care about is binary: do I treat this as production-stable, or as use-at-your-own-risk.
experimental: true is set at 3.1 GA on four canonicals:
| Canonical | Why experimental | Promotion gated on |
|---|---|---|
sponsored_placement | Four meaningfully different retail-media adapter contracts (Amazon SP, Criteo SP / CitrusAd SP, Pinterest Collection, generative-per-SKU) under one canonical — see Sponsored Placement adapter contracts | Two adopters with format_schema evidence |
responsive_creative | Algorithmic composition (surface picks combinations); no clean v1-translatable equivalent | Adopter evidence + per-surface conformance |
agent_placement | Tracking macro vocabulary / postback shape / cross-surface dedup intentionally underspecified for 3.1 | 3.2 tracking-contract spec |
custom | Inherently experimental — adopter-defined shape until promoted to a first-class format_kind | Per-shape promotion via the #3666 queue |
image, display_tag, video_hosted, video_vast, audio_hosted, audio_daast, native_in_feed) plus html5 and image_carousel ship non-experimental — they’re settled industry standards being re-encoded in canonical-formats vocabulary.
Sellers MAY set experimental: true at the product-declaration level (on a specific ProductFormatDeclaration) even when the underlying canonical is non-experimental — useful for beta runtime paths or forward-looking catalog declarations the seller hasn’t wired yet. Buyer SDKs SHOULD filter products with experimental: true from default views and offer an opt-in to surface them.
When a seller marks a product experimental: true, the buyer’s path of least resistance is the v1 fallback: if the v2 declaration carries v1_format_ref linking to a v1 named format, ship against v1 until the seller drops the experimental flag. v1 is the safe path; v2 is the surface the seller is still testing.
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_creativefor Google PMax / Meta Advantage+;agent_placementfor AI-surface composition.)
asset_source describes who renders the rendered asset, and when:
asset_sourceonimage,video_hosted,audio_hosted— single shared 5-value enum:buyer_uploaded | publisher_host_recorded | seller_pre_rendered_from_brief | seller_human_designed | agent_synthesized.publisher_host_recordedis audio-specific (podcast host-read pattern) and meaningful only onaudio_hosted.item_production_modelonsponsored_placement— same axis, 4-value subset (dropspublisher_host_recorded), applied per catalog item (the multi-output generative case: 1 brief × N catalog items → N rendered creatives)
composition_model: deterministic (the surface serves what it received) + asset_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 asset_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
Whenasset_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 asset_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 optionallanding_page_urland the buyer’s measurement-vendor pixels declared viaplatform_extensionson the format, filtered byextensions[uri].extends === "tracking") 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_creativesresponse SHOULD include atracker_blockfield 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 <TrackingEvents> block at production time. When the buyer ships a complete vast or daast tag, the trackers travel inside the tag.
What format_kind is NOT for
format_kind names the creative ASSET shape — what the buyer ships, what the surface accepts. It’s not for delivery medium, measurement model, or targeting context. Conflating these is the most common architectural mistake a 3.2 contributor will be tempted to make. Three concrete examples:
| Tempting (wrong) | Right answer | Why |
|---|---|---|
format_kind: "broadcast_video" | format_kind: "video_hosted" + applies_to_channels: ["tv"] | The video asset is the same shape. Broadcast vs streaming is a delivery / measurement difference, not a creative-shape difference. |
format_kind: "dooh_image" | format_kind: "image" + applies_to_channels: ["dooh"] | The image asset is the same shape. DOOH’s location-keyed measurement and no-click model live on sync_event_sources / event_log, not on the format. |
format_kind: "image_generative" | format_kind: "image" + asset_source: "agent_synthesized" + slots_override: [{ generation_prompt: text }] | Same canonical TYPE; different production SOURCE. The two-axis model already separates them. |
format_kind, check whether the difference is:
- Creative type (image vs video vs audio vs html5 vs 3p-tag) →
format_kind, the only knob it controls. - Production model (who renders, when) →
asset_sourceon the format declaration. - Slot shape (what assets the buyer ships) →
slots_overrideon the projection ref (catalog side) or on the v2 product’sformat_options[]declaration. - Delivery medium / channel (TV vs streaming vs DOOH vs social) →
applies_to_channelson the v2 product. - Measurement / tracking / event model. Splits two ways:
- Renderer-fired trackers (the renderer hits a URL when serving / viewing / clicking) →
pixel_trackerasset (orvast_tracker/daast_trackeron those formats). Lives on the creative manifest as a typed slot. Buyer’s measurement vendor URLs the seller’s renderer fires at serve time. Seedocs/creative/asset-types.mdx#pixel-tracker-asset. - Conversion pixels (fire on the advertiser’s site post-click — Meta Pixel, GA4 server-side, custom postbacks) →
sync_event_sources/event_log. Campaign-scoped, NOT creative-asset-scoped. The same pixel fires for every ad in the campaign.
- Renderer-fired trackers (the renderer hits a URL when serving / viewing / clicking) →
- Targeting context (audience vs geo vs daypart) → media-buy targeting overlay, not the format.
format_kind only when the CREATIVE ASSET itself is structurally different (e.g., DAI’s ad-stitched continuous audio stream is structurally different from audio_hosted’s file-per-impression). All 50 ad formats in the v1 catalog at GA project to canonicals via this rule; broadcast TV, DOOH, and generative all stay on existing canonicals (sibling refinement via applies_to_channels / asset_source / slots_override). The one exception is native_in_feed: IAB OpenRTB Native 1.2 in-feed and content-recommendation units have an asset-bundle composition shape (title + image + body + CTA assembled by the renderer) that isn’t expressible as sibling refinement on image or responsive_creative and isn’t catalog-keyed like sponsored_placement — a buyer agent needs the format_kind discriminator to route to the right assembly logic. The 12-canonical line is held because native_in_feed cleared this bar; it’s not a precedent for every channel asking for its own canonical.
When to use slots_override (and when to leave it off)
slots_override (on the catalog’s canonical: projection ref OR on a v2 product’s format_options[] declaration) replaces the canonical’s default slot set with a custom list. Use it sparingly — most formats inherit defaults cleanly.
Decision rule. Would a buyer composing a creative manifest list different assets in this case vs the canonical’s default? If yes, slots_override. If no, leave it off.
| Case | Default vs Override |
|---|---|
| 300×250 IAB MREC | Default. Canonical image defaults already carry image_main: image, required + headline/body/cta/landing — the buyer’s manifest assets are the canonical’s defaults. |
| Native standard (title + description + image + icon + sponsored_by) | Override. Adds brand_name (sponsored_by alias) as required, makes headline required, narrows cta to enum values. Manifest assets diverge from canonical defaults. |
| DOOH billboard (image only, no click) | Default. Canonical image defaults cover it (image_main required, no other required slots). No-click model lives on event_log, not on slots. |
| Broadcast TV spot (video file + captions URL) | Default. Canonical video_hosted defaults cover it (video_main required, captions optional). Broadcast trafficking lives on applies_to_channels: ["tv"]. |
| Generative image (text prompt instead of image) | Override. Replaces image_main: image, required with generation_prompt: text, required. Manifest assets are structurally different from canonical defaults. |
| Podcast host-read (script text instead of audio) | Override. Replaces audio_main: audio, required with script: text, required + asset_source: publisher_host_recorded. |
slots_override only to declare measurement pixels, delivery-medium flags, or targeting context, you’re using it wrong — those don’t belong in slots at all (see “What format_kind is NOT for” above).
Custom formats — shapes the 12 canonicals don’t cover
The 12 canonicals cover atomic creative shapes (one image, one video, one display tag, one carousel, one native in-feed unit, 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 12). v2 handles them via a structured custom mechanism that buyer agents can reason about, NOT via free-formext.
The mechanism
test=false
format_kind: "custom":
format_shape— recognized global pattern from the format-shape vocabulary registry. 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.format_schema— URI+digest reference to a fetchable schema describing the shape’s actualparamsandslots. Same hosting model asplatform_extensions: open-ecosystem publishers host the artifact at the canonical URI on their subdomain; closed-platform / walled-garden shapes resolve through the AAO mirror athttps://creative.adcontextprotocol.org/translated/.... Buyer agents fetch byuri@digest(immutable per digest, aggressive caching), validateparamsandslotsagainst the fetched schema, and reason about manifests structurally.params— the actual structure, governed by the schema fetched fromformat_schema.uri. AdCP doesn’t bake the params shape; the seller’s schema does.
format_schema fetch contract (normative)
format_schema gates validation — without the schema, a buyer cannot reason about the custom shape. The transport rules below apply identically to BOTH format_schema and platform_extensions (any SDK fetching a platform-extension-ref.json URI applies the same rules — a shared fetch path that drops to the weakest bar undermines format_schema’s hardening). The consumption distinction (format_schema is load-bearing, platform_extensions is informational) is about what the body means, not about how it’s fetched.
- Transport:
https://only.http://,file://,data:, and other schemes MUST be rejected. - SSRF protection: resolved hostname MUST NOT land on RFC 1918 (10/8, 172.16/12, 192.168/16), loopback (127/8, ::1), link-local (169.254/16, fe80::/10), CGNAT (100.64/10), or RFC 6761 special-use names (
.local,.localhost,.internal,.test,.example,.invalid). Cloud metadata endpoints (169.254.169.254,metadata.google.internal,kubernetes.default.svc) are explicitly forbidden — these are credential-leak primitives. Connection MUST be pinned to the resolved IP (or re-resolved and re-validated per request) to defeat DNS rebinding. - No redirects. HTTP redirects MUST be disabled on these fetches. Open redirects on same-origin paths are otherwise a free SSRF primitive.
- 1 MiB response cap. Enforce during streaming. Over-cap = hard fail.
- Digest mismatch is a hard fail. SHA-256 of the body MUST equal
format_schema.digest(sha256:+ 64 lowercase hex). On mismatch, the buyer MUST treat the declaration as unresolvable. No fallback to the unverified body. Sustained mismatch (vs network flap) MUST be distinguishable in telemetry — it’s a substitution-attack signal. - Timeout ≤5s recommended. Timeout treated as a 5xx (transient — retry or skip).
$refsandboxing: fetched schemas MAY use$ref, but only to (a) same-origin URIs after RFC 3986 §6 normalization (lowercase scheme + host, strip default port, no userinfo), (b) the AAO catalog domain (https://creative.adcontextprotocol.org/...), or (c) intra-document JSON Pointer refs bounded to the parent document. Cross-origin$refto arbitrary URIs MUST be rejected.$ref: file://...MUST be rejected. Transitive$refdepth ≤8 AND total$refcount ≤256 across the resolved tree (depth alone is not enough — depth 8 × breadth 100 = 10^16 nodes).- Schema-compile bounds (DoS protection): validators MUST bound CPU/memory. Recommended: compiled-schema keyword count ≤10 000,
patternregexes evaluated withre2OR under a per-pattern timeout, per-manifest validation budget ≤250 ms (exceeded → invalid + telemetry signal). Without these, a “valid” schema with catastrophic regex backtracking pins a CPU forever. - Cache by
uri@digest, immutable. On 404 / partition / persistent failure: skip the declaration for this session, surface viaerrors[], do NOT fail the wholeget_productsresponse. - Schema validity: fetched body must be a valid JSON Schema (Draft 07 or 2020-12). Invalid schema → same as digest mismatch (unresolvable, surface via
errors[], skip). - AAO catalog domain:
https://creative.adcontextprotocol.org/*is a single trust anchor in the allowlist; compromise of the catalog domain or its CA compromises every buyer agent. Catalog-served bodies are digest-pinned identically to origin fetches. Signed-body + transparency-log hardening is tracked as a 3.2 follow-up.
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
Aformat_shape entry is promoted to a first-class format_kind when:
- At least 2 production adopters ship it via custom + format_schema
- 90 consecutive days without a breaking change to the shape adopters converged on
- The shape has a defined tracking model (which signals fire, which trackers attach, what the impression contract is)
- The working group opens a per-canonical promotion issue, drafts a canonical schema (
/schemas/formats/canonical/<name>.json), lands a fixture, and ships in the next minor release
format_kind == "custom" silently stops matching publishers that ship a promoted shape — the seller’s product now arrives as format_kind: "<promoted_name>". The normative migration contract (in format-shape-vocabulary.json’s description):
- Transition window (≥90 days): sellers MAY emit both shapes simultaneously —
format_options[]carrying oneformat_kind: "custom"+format_shape: "<name>"declaration AND oneformat_kind: "<promoted_name>"declaration. - Consumer-SDK deprecation warning: SDKs SHOULD emit a structured deprecation warning via their lint channel (same surface as
FORMAT_PROJECTION_FAILED) when they seeformat_kind: "custom"with aformat_shapethat’s been promoted. Payload:{ format_shape, promoted_to, promotion_release, transition_end }. promotion_statuslifecycle: the registry entry’spromotion_statusupdates fromtracking — see adcp#3666topromoted to <format_kind> in <version>; transition ends <date>when the working group schedules promotion. SDKs MAY read this at codegen / runtime.- Post-transition: sellers SHOULD drop the legacy
format_kind: "custom"declaration. Buyers MAY then assumeformat_kind == "custom"is a long-tail / non-promoted shape.
Asset group vocabulary
Formatslots reference canonical asset_group_id values from the vocabulary registry. 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 |
video_brief | object | scenes, storyboard, shot_brief |
voice_id / offering_ref | 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) |
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 is a useful test of canonical-formats coverage: a platform-specific format from a vendor that hasn’t adopted AdCP, with rendering details (CTA enum, primary text, headline limits, brand name overlay) on top of a vertical video. Each Reels feature lands somewhere — canonicalparams, an inherited or overridden slot, the brand layer, the campaign layer — and the canonical doesn’t need to grow.
Where each Reels feature lives
| Reels feature | Where it lives | Notes |
|---|---|---|
| Vertical 9:16, 3-90s, 1080×1920, h264/aac, mp4 | params on video_hosted | Standard canonical parameters. |
| Headline (40ch), Primary text (125ch) | params.headline_max_chars, params.primary_text_max_chars + inherited headline / primary_text slots | Buyer ships text content via the slots; the param narrows the constraint. |
| CTA from a fixed enum | params.cta_values[] + inherited cta slot | Buyer ships assets.cta.text; validator checks against the enum. |
| Landing page URL | Inherited landing_page_url slot on video_hosted | No special handling. |
| Brand name overlay | NOT in the format. Auto-applied by Meta from the linked Page (auth context outside AdCP). | The inherited brand_name slot exists in the canonical; products that need explicit content (publisher-direct video, CTV) populate it. Meta doesn’t consume it for Reels. |
| Logo overlay | NOT in the format. Auto-applied by Meta from the linked Page. | BrandRef.brand_kit_override.logo exists for formats whose seller-side renderer overlays a logo (host-read podcasts, CTV bumpers, publisher direct). Meta doesn’t use it. |
| Music overlay (Reels music library) | NOT in this declaration. Would be a platform_extension on the format if added; not present in 3.1 — too platform-specific to be worth a schema seat without 2+ adopters. | Sellers can layer it via ext on the manifest in the meantime. |
| Pixel / conversion tracking | NOT on the format. Conversion tracking is sync_event_sources / event_log territory — campaign-scoped, not creative-scoped. | The same pixel fires for every ad in the campaign regardless of creative; one declaration in event_log covers the campaign. |
| Placement selection (Feed vs Reels vs Stories) | NOT on the format. Placement is selected at media-buy time (Placement.format_options[].capability_id) — pick meta_reels to buy Reels, meta_stories_video to buy Stories. | They’re separate format declarations on the publisher catalog; not a runtime extension parameter. |
Where to declare a format
Three places. Pick by who owns the assertion.| Surface | Authority | When to use |
|---|---|---|
adagents.json top-level formats[] (publisher catalog) | Publisher (or AAO community mirror standing in) | Shared across all sellers of a publisher’s inventory — declares the publisher-authoritative shape once. Use applies_to_property_ids / applies_to_property_tags to scope to a subset of the file’s properties[]. |
Product.format_options[] (inline on a product) | Seller, on a specific product | Seller-specific narrowing, custom format, or one-off pricing variant. SHOULD reference a publisher catalog entry by capability_id when the publisher has one. |
Placement.format_options[] (capability_id reference OR inline) | Publisher (or seller publishing placements) | Ties a placement to one or more accepted formats. Reference by capability_id (recommended; resolves against the file’s top-level formats[]) or inline for placement-local narrowing. Same-file scope — cross-file capability_id lookup is not supported. |
applies_to_property_ids and Placement.format_options[] answer different questions: the first scopes a FORMAT to a subset of properties (“Reels applies to Instagram + Facebook but not WhatsApp”); the second binds a PLACEMENT to one or more formats (“Instagram Reels accepts the meta_reels capability”). Property-level format support → applies_to_property_ids. Placement-level binding → placements[].format_options[].
Format discovery (resolution order)
Buyer agents answer “what formats does this publisher accept?” vialist_creative_formats(publisher_domain="<domain>", property_id?="<id>"). Three-tier resolution:
- Publisher-hosted: fetch
https://<publisher_domain>/.well-known/adagents.json. If present and carriesformats[], return it. Responsesource: "publisher". - AAO community mirror: on 404 or absence-of-formats[], fall back to
https://creative.adcontextprotocol.org/translated/<platform>/adagents.json. Return itsformats[]. Responsesource: "aao_mirror". - Agent-derived: if neither tier returns a catalog, the agent synthesizes from the union of its own
Product.format_options[]over products selling the publisher’s inventory. Responsesource: "agent_derived". Lowest authority — the agent’s view of what it sells, not the publisher’s catalog. Long-tail IAB publishers without a structured catalog will live here.
format_schema — https-only, SSRF guards (RFC 1918 / loopback / link-local / metadata-endpoint denylist; resolve hostname and pin connection to defeat DNS rebinding), ≤5s timeout, 1 MiB cap, no redirects. See static/schemas/source/core/product-format-declaration.json#format_schema for the full normative contract. Adagents.json files carry authorization claims + signing keys; SSRF leakage is higher-value to an attacker than format_schema leakage.
Community-mirror governance (3.1 status). The AAO publishes adagents.json files for unadopted platforms at creative.adcontextprotocol.org/translated/<platform>/. Maintenance is unowned today — entries are best-effort, derived from publicly documented platform specs. The community-mirror namespace inherits the same single-trust-anchor concern as the format_schema mirror: compromise of creative.adcontextprotocol.org or its CA compromises every buyer agent reading the mirror. Signed-body + transparency-log hardening is tracked as a 3.2 follow-up. Until then, buyer SDKs SHOULD treat mirror-served content as advisory (label via source: "aao_mirror"), prefer publisher-hosted tier 1 when available, and apply freshness checks (per-platform OWNERS + staleness threshold are tracked separately — when a mirror entry hasn’t been refreshed within the threshold, the SDK MAY demote its authority to agent_derived).
Identity-confusion note (normative). A mirror URL in v1_format_ref[].agent_url declares format-shape provenance, NOT seller identity. Buyer allowlists matching on v1_format_ref[].agent_url are matching shape namespace; inventory authorization always flows from authorized_agents[] + publisher signing keys. A seller pointing v1_format_ref at creative.adcontextprotocol.org/translated/meta is asserting “this format follows the AAO-mirrored Meta Reels shape,” not “I am Meta.”
Platform-adoption cutover. When a platform adopts AdCP and publishes its own adagents.json, the AAO mirror file SHOULD set superseded_by: "<platform-domain>/.well-known/adagents.json". Buyer SDKs encountering superseded_by MUST short-circuit and re-fetch from the named URL rather than serving stale mirror content. The mirror SHOULD continue serving for ≥1 minor release with superseded_by set so caches keyed on the mirror URL get an explicit migration signal rather than a silent break. Sellers also update v1_format_ref[].agent_url to the platform’s adopted agent_url in the same minor release.
End-to-end fetch flow — buyer’s perspective
A buyer agent seeing aProduct with publisher_properties[].publisher_domain = "instagram.com" and needing to know “what formats does this publisher accept, scoped to this property?” walks the following resolution. The pieces are documented separately above; this section walks them in order so adopters don’t have to assemble the journey from fragments.
https://instagram.com/.well-known/adagents.json. Apply the format_schema transport contract (https-only, SSRF guards, ≤5s timeout, 1 MiB cap, no redirects — see product-format-declaration.json#format_schema). Instagram hasn’t adopted AdCP today — fetch returns 404.
Step 2 — Fall back to AAO community mirror. On Step 1’s 404 (or 200-with-no-formats[]), buyer fetches https://creative.adcontextprotocol.org/translated/meta/adagents.json. Same transport contract. Response carries formats[] with publisher-authoritative declarations. The buyer SDK labels the result source: "aao_mirror" for telemetry.
Step 3 — Check for supersession. If the response carries superseded_by, short-circuit: re-fetch from the named URL (typically the platform’s adopted adagents.json) and use that response instead. Today the mirror’s superseded_by is unset; in the future when Meta adopts, it would point at https://meta.com/.well-known/adagents.json.
Step 4 — Scope by property_id. From the file’s formats[], filter to entries whose applies_to_property_ids includes "instagram" (the property ID; not the same as publisher_domain). Property IDs are declared in the file’s top-level properties[] block. A formats[] entry with no applies_to_property_ids / applies_to_property_tags scoping applies to ALL properties in the file. For Meta:
meta_reels→ applies_to_property_ids: [“instagram”, “facebook”] → matchesmeta_feed_image→ applies_to_property_ids: [“instagram”, “facebook”] → matchesmeta_stories_video→ applies_to_property_ids: [“instagram”, “facebook”] → matchesmeta_feed_carousel→ applies_to_property_ids: [“instagram”, “facebook”] → matches
format_options: [{ capability_id: "meta_reels" }]. The buyer agent now knows the FULL declaration: format_kind: video_hosted, vertical 9:16, 3-90s duration, the full Meta CTA enum, plus the v1_format_ref[] array for v1-wire dual emission.
Step 5 — Resolve placement references (if any). If the publisher catalog includes placements[] and a placement carries format_options: [{ capability_id: "meta_reels" }], the buyer resolves the capability_id against the SAME file’s top-level formats[] (same-file scope; cross-file lookup is not supported by design). When the reference is broken — capability_id not present in formats[] — the SDK MUST surface FORMAT_CAPABILITY_UNRESOLVED on the response errors[] and fail closed for that placement.
Step 6 — Multi-tier discovery cache. Buyer SDK caches the file by <resolved URL>@<sha256> for the duration the publisher honors (typically Cache-Control: max-age, capped at 24h). Subsequent products from the same publisher reuse the cached file without re-fetching.
Concrete payload sequence (Meta Reels, scoped to Instagram):
source field reports tier: "publisher" if Step 1 returned formats[], "aao_mirror" if Step 2 did, "agent_derived" if neither and the SDK synthesized from products’ own format_options[]. Two SDKs hitting the same agent for the same publisher get consistent labeling regardless of which tier produced the list.
Community-registry hosting
Meta hasn’t adopted AdCP, so itsadagents.json lives at the AAO community-registry mirror, https://creative.adcontextprotocol.org/translated/meta/adagents.json. The mirror file declares formats[] at the publisher catalog level — one declaration of Meta Reels, scoped to Instagram + Facebook (not WhatsApp), reused across every seller’s products. When Meta later publishes its own adagents.json at meta.com/.well-known/adagents.json, the platform-hosted file takes precedence and the mirror entry deprecates (via the superseded_by signal documented above).
test=false
list_creative_formats(publisher_domain="meta.com") by fetching this file (or meta.com/.well-known/adagents.json first; mirror is the fallback) and returning formats[]. The whole question “what formats does Meta support” resolves in one round-trip without per-product traversal.
Product reuses the catalog declaration
A seller’smeta_reels_us product references the publisher-catalog declaration by capability_id, narrowing only the parts specific to that product (geography, pricing). The format shape itself is inherited from creative.adcontextprotocol.org/translated/meta:
test=false
capability_id: "meta_reels" tag lets buyer agents recognize this as the same Meta Reels they read from the publisher catalog — the seller didn’t reinvent the format, they’re selling inventory against the catalog declaration. The 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.
What lives where (and why)
- Canonical params — fields the canonical already defines (dimensions, durations, codecs, CTA enum, char limits). Sellers narrow values; SDKs validate. Tight, codegen-clean.
- Canonical slots — content the manifest carries.
video_hostedinheritsvideo_main,headline,primary_text,cta,brand_name,companion_banner,landing_page_url. Products can override (remove slots the surface doesn’t use; mark required; narrow values). platform_extensions— net-new fields the canonical doesn’t recognize, scoped to one platform’s renderer (e.g., a hypothetical Reels music overlay carrying track_id + licensing flags). Bundled by URI+digest inget_productsso buyers fetch once and cache.BrandRef+brand_kit_override— brand context (logo, colors, voice, tagline) consumed by formats whose seller-side renderer overlays brand. Host-read podcasts, CTV bumpers, publisher-direct display do consume it. Meta auto-overlays from the linked Page (auth context outside AdCP), so brand_kit_override has no effect for Meta Reels — it’s still the right schema location for the cases that DO consume it.- Campaign / event-log surfaces — conversion tracking (Meta Pixel, GA4, server-side events). These belong on
sync_event_sources/event_log(campaign-scoped, fires per impression regardless of creative). Format declarations carry creative shape; event-log declarations carry tracking configuration. Don’t putpixel_idinplatform_extensionson a creative format. - Media-buy surfaces — placement selection (Feed vs Reels vs Stories). Pick the right format on the publisher catalog (
meta_reelsvsmeta_stories_videovsmeta_feed_image); not a per-creative extension knob.
tests/canonical-format-conventions.test.cjs enforces the v1_format_ref.agent_url AAO-hosted convention and the slot/param consistency rule.
Worked example — IAB display (flexible multi-format, multi-size)
A real IAB display placement isn’t a single 300×250 image slot — it’s a flexible slot that accepts multiple creative types (image, HTML5, third-party tag, sometimes native or video-in-banner) at multiple sizes (300×250 MREC, 728×90 leaderboard, 970×250 billboard, responsive). The canonical-formats vocabulary models this with two orthogonal mechanisms:format_kindis the creative TYPE — one ofimage,html5,display_tag,native,video_hosted— and never carries dimensional identity.- Size lives in
paramsas one of three modes (mutually exclusive):- Fixed:
width+heightintegers — single accepted size (e.g., a 300×250-only legacy slot). - Multi-size:
sizes: [{width, height}, ...]— list of accepted sizes for a flexible slot. Mirrors OpenRTBbanner.format[]. - Responsive:
min_width/max_width+min_height/max_height— accepted dimensional ranges for slots that adapt to viewport.
- Fixed:
test=false
format_options entries (one per creative type), each with sizes[] carrying the three accepted IAB sizes. Buyer agents read this as “the slot accepts image OR html5 OR display_tag at any of 300×250 / 728×90 / 970×250.” The buyer ships ONE creative — they pick which type and which size — and validation checks the manifest’s slot width/height against the appropriate sizes[] list for the chosen format_kind.
Responsive variant. A responsive slot replaces sizes[] with min/max ranges — min_width: 300, max_width: 970, min_height: 50, max_height: 250 — and accepts any dimensions within the box. Same multi-format pattern; different size declaration. Exactly one size mode (fixed width+height / multi-size sizes[] / responsive ranges) per format_options entry, enforced at the schema layer.
Seller preference. When a multi-format product has several format_options at the same price, sellers MAY set seller_preference: "preferred" | "accepted" | "discouraged" on each entry to hint which the seller would prefer the buyer ship (often because of viewability / measurement / render-quality differences). Soft routing signal — buyer agents respect it when their own constraints don’t override.
Worked example — Podcast 30s host-read
Host-reads are the host-recorded-from-buyer-script pattern. The product declaresaudio_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):
test=false
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’sbuild_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.
- Buyer reads The Daily’s product format → sees
slots: [{ asset_group_id: "script", asset_type: "text", required: true }]declared - Buyer calls
build_creative({ format: <The Daily's audio_hosted narrowing>, 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 itscreative.supported_formatsonget_adcp_capabilities - Receives a rendered manifest with audio asset
- Submits the rendered manifest via
sync_creativesto 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.- Buyer reads the same product format
- Buyer submits via
sync_creativeswith the assets in the manifest (e.g., ascripttext asset under that slot in theassetsmap) - Seller produces internally; how is invisible to the buyer
- Returns async status; buyer polls or waits for completion
asset_source: "publisher_host_recorded" + buyer_asset_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 viacreative.supported_formatson its OWNget_adcp_capabilities
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 callsget_products on NYTimes. The MREC product narrows canonical image:
test=false
2. Buyer calls Flashtalking’s build_creative
test=false
test=false
3. Buyer ships to NYTimes
Buyer callssync_creatives on NYTimes with the manifest from Flashtalking. NYTimes:
- Validates the manifest against canonical
image(300×250, ≤200KB, SSL). - Validates against the product’s narrowing (matches — same params).
- Does NOT validate against Flashtalking’s narrowing — that’s the creative agent’s contract with the buyer, not the seller’s contract.
- If valid → creative registered. If not → returns canonical violations (
widthmismatch,max_file_size_kbexceeded).
Worked example — generative DSP (universalads-class, asset_source: seller_pre_rendered_from_brief)
A generative DSP (universalads, Pencil, AdCreative.ai-shaped tools) is a sales agent that ALSO renders creatives inline atsync_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.
test=false
sync_creatives produces the rendered MREC PNG and registers it. Two axes: composition_model: deterministic (the surface serves what it received), asset_source: seller_pre_rendered_from_brief (the seller renders from inputs at sync time). buyer_asset_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’sformat_kind (and capability_id if needed) to the matching declaration:
test=false
test=false
format_options (normative):
format_kindselects the canonical and its slot vocabulary.capability_idis REQUIRED on the manifest when the target product’sformat_optionscontains two or more declarations sharing the sameformat_kind— without it, the seller can’t disambiguate which option the buyer is shipping against.capability_idis OPTIONAL when eachformat_kindin the product’sformat_optionsis unique (the example above: one html5 entry, one display_tag entry) —format_kindalone routes the manifest. Buyers MAY still sendcapability_idas a clarity hint.
format_kind, so capability_id is optional. Including it (as shown) is a recommended habit — it makes the manifest unambiguous to logs, replays, and downstream tooling, and it keeps the buyer-side codepath identical regardless of whether the seller’s product has one or many options sharing a kind.
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:test=false
item_production_model: seller_pre_rendered_from_brief says: for each catalog item, the seller renders ONE creative using the brief plus the catalog item’s structured fields (title, image, price). fanout_mode: per_item says each item gets its own ad in delivery. Together they capture the multi-output generative pattern (1 brief × N items → N ads) under the existing sponsored_placement canonical.
Worked example — Pinterest: which canonical?
Pinterest is the canonical disambiguation example because a single platform sells inventory under two structurally different shapes. Buyer agents reading these products must route to the matching canonical or the manifest won’t render.| Pinterest product | Canonical | Why |
|---|---|---|
| Promoted Pin (sponsored single Pin in the home/search feed) | native_in_feed | Asset-bundle composition — buyer ships title + image + body + landing URL; Pinterest’s renderer assembles the Pin to match feed look-and-feel. No catalog feed; the buyer-supplied bundle IS the creative. |
| Pinterest Collection (single hero image + 3 product thumbnails sourced from a catalog) | sponsored_placement | Catalog-keyed — buyer ships a source_catalog reference (and an optional hero_asset); Pinterest composes the per-item thumbnails by reading product catalog rows. fanout_mode: multi_item_in_creative distinguishes Collection from per-item retail-media SP. |
| Pinterest Idea Pin (multi-page swipeable native unit; pages may be image or video) | image_carousel | Multi-card swipe shape — the carousel canonical’s per-card slot is polymorphic image-or-video, which matches Idea Pin pages that mix the two within one Pin. |
| Pinterest Shopping Pin (single product Pin keyed to a catalog row) | sponsored_placement with fanout_mode: single_item | Single rendered creative pulled from one catalog item — still catalog-keyed composition, just one item per ad. |
applies_to_channels: ["social"]) vs TikTok Collection (sponsored_placement), and so on. Buyer agents route on the composition shape; the publisher’s brand of the surface is incidental.
A buyer agent reading a format_kind: native_in_feed product knows to assemble the title/image/body/CTA bundle from its own creative pool. Reading format_kind: sponsored_placement, it knows to attach a catalog feed and let the seller compose per-item. The discriminator carries the decision; no per-platform branching needed.
Validation flow — validate_input
Buyers can dry-run a manifest against canonicals and/or specific products without committing to a render. The buyer’s manifest below is a v2 manifest (format_kind: "video_hosted"); the slot key is the canonical’s asset_group_id (video_main); the asset value carries its asset_type discriminator. The buyer asks validate_input to check both the canonical contract AND the seller’s specific product narrowing in a single round-trip:
test=false
video_hosted doesn’t constrain duration — products narrow); the Meta Reels product narrows duration to [3000, 90000] ms, so 95000 is out of range and the product target fails:
test=false
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_creativecall. 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_inputonce with all 10 product_ids; gets back per-target results. Cheaper than 10 separatesync_creativesround-trips. - Debugging a rejected manifest. When
sync_creativesreturns violations, callingvalidate_inputagainst 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: algorithmicorsynthesis_nondeterministic: true). The platform’s preview surface is a richer follow-on;validate_inputis the cheap pre-flight that gates whether previewing is even worth attempting.
validate_input:
- For a manifest you intend to submit anyway.
sync_creativesreturns the same violations and registers on success —validate_inputadds a round-trip without reducing total work. - For products where the seller’s narrowing is unknowable client-side without fetching extensions.
validate_inputpulls extensions same assync_creativesdoes — there’s no discovery shortcut. - For high-volume per-impression decisions.
validate_inputis per-target, not per-impression. Operational scale (hundreds of products × N format_options) belongs to client-side filtering against the cachedget_productsresponse.
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, video_brief, 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. |
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 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 viavalidate_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_productsresponse. Buyers who know their manifest’sformat_kindand 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), callvalidate_inputonce with all candidate product_ids intargets[]. 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).
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’sslots 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.
Brand identity via brand.json (with override)
v2 formats no longer redeclarebrand_logo, brand_colors, brand_voice, brand_tagline as explicit slots. When a manifest carries a BrandRef 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 BrandRef itself carries an inline brand_kit_override:
test=false
brand.json for the call carrying this BrandRef. The pattern matches BrandRef’s existing inline overrides (industries, data_subject_contestation) — brand.json is canonical; inline overrides are per-call. Adopters needing to override brand-kit fields outside this subset (voice_attributes, prohibited_terms) MUST publish a different brand.json and reference it via a different domain.
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: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://creative.adcontextprotocol.org/translated/<platform>/<artifact>@<digest>). Worked-example fixtures in this repo that reference https://creative.adcontextprotocol.org/translated/meta/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:
test=false
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.
Dual emission and v2↔v1 projection (normative)
Products MAY carry bothformat_ids (v1) and format_options (v2) during the migration window. When both ship, the two MUST refer to the same underlying format declaration — divergent shapes are a contract violation.
Producer rules
- SDKs that derive both shapes from a single source guarantee the invariant. Hand-authored products MUST be reviewed for agreement.
- A producer that cannot guarantee agreement MUST emit one shape only.
- For
format_kind: "custom"declarations, producers MUST setcanonical_formats_only: trueand MUST NOT synthesize a v1format_id. The protocol does NOT mint synthetic format_ids (anaao-synth/*namespace was considered and rejected — adopters would index on identifiers with no stable identity). - For
format_optionsdeclarations whose canonical/parameter shape has no clean v1 named-format equivalent (e.g., a structural shape not inv1-canonical-mapping.jsonand not declared on any v1 file), producers SHOULD setcanonical_formats_only: truerather than emit only one of the two shapes silently.
Consumer rules (v1→v2)
When reading a product on the v1 path, SDKs projectformat_ids to format_options using the resolution order from v1-canonical-mapping.json:
- Authoritative v2 → v1 link: if any v2
ProductFormatDeclarationon the same product carriesv1_format_refpointing at this v1format_id, use that v2 declaration directly. Highest priority — seller asserts the link. - Seller-asserted on the v1 file: explicit
canonicalfield on the v1 format declaration. - Registry glob:
format_id_globmatch. - Structural match: registry structural-shape match.
- Fail closed: SDK MUST NOT synthesize a
format_optionsentry. SDKs MUST augment the response’serrors[]array with an entry carryingsource: "sdk",sdk_id,code: FORMAT_PROJECTION_FAILED, and the field+details (see error-code.json). Single mandated surface — lint-output channels are NOT acceptable; the multi-hop agent network needs warnings to propagate across SDK boundaries via the wire response. The advisory is non-fatal: the response stays 200/success, the product is still valid on the v1 path, only the v2format_optionsprojection is absent.
Consumer rules (divergence detection)
When a product carries BOTHformat_ids and format_options and the two disagree (different canonical, different dimensions, different orientation, etc.):
- SDKs MUST treat this as a producer contract violation.
- SDKs MUST prefer
format_options(canonical formats are the richer surface) and MUST surface the divergent product viaerrors[]augmentation withsource: "sdk",sdk_id, andcode: FORMAT_DECLARATION_DIVERGENT. Single mandated surface — lint-output channels are not acceptable. Hard-failing the entireget_productsresponse is discouraged — it punishes downstream buyers for a producer bug. - SDKs MUST NOT silently pick one shape and discard the other without surfacing the divergence to the calling agent.
format_ids[i] must equal format_options[j]”). Consumer-side detection is the only line of defense; SDK conformance suites SHOULD include divergence fixtures.
”Narrows” — formal definition (normative)
When the spec says a v2format_options entry MUST refer to the same underlying declaration as a v1 format_ids entry on the same product (dual-emission invariant), or when an SDK compares an authored v2 declaration against the registry-projected one to detect divergence, the comparison MUST follow this definition:
v2.params narrows the v1-projected baseline when every parameter present on v2.params is structurally a subset of the equivalent v1 requirement after registry expansion. Specifically:
- Scalar constraints: a v2 scalar value
narrowsa v1 range when the v2 value is contained within the v1 range.v2.width: 300narrowsv1.width_range: [200, 400];v2.duration_ms_exact: 30000narrowsv1.min_duration_ms: 3000. - Enum constraints: v2.enum_value is a
narrowingif it appears in v1.allowed_values (or v1.allowed_values is absent — open enum).v2.image_formats: ["jpg", "png"]narrowsv1.image_formats: ["jpg", "png", "gif", "webp"]. - Range constraints: v2.range narrows v1.range when v2’s lower bound ≥ v1’s lower bound AND v2’s upper bound ≤ v1’s upper bound.
v2.duration_ms_range: [5000, 30000]narrowsv1.duration_ms_range: [3000, 90000]. - Absent v2 parameter: when v2.params omits a parameter that v1 specified, v2 inherits v1’s value (no narrowing constraint added). Producers SHOULD omit rather than restate v1 defaults.
- Asymmetric narrowing: when v1 says nothing about a parameter but v2 specifies one (e.g., v1 has no
image_formatsconstraint, v2 declaresimage_formats: ["jpg"]), v2 isnarrowingagainst the implicit “any value” v1 baseline. This is the expected v2-tightens-v1 pattern. - Conflict: any v2 parameter value that falls outside the corresponding v1 constraint is a conflict, not narrowing. SDKs MUST treat conflict between dual-emitted shapes as divergence and surface via
FORMAT_DECLARATION_DIVERGENT.
v1 → v2 projection via canonical: annotation (object shape)
The v1 catalog’s canonical: annotation is an OBJECT, not a string. The minimal form carries just the canonical kind; the rich form adds asset_source and slots_override for v1 entries whose shape doesn’t follow the canonical’s defaults.
Why an object. A bare-string annotation canonical: "image" implicitly carries the canonical’s default slot set (image_main: image, required) and default asset_source (buyer_uploaded). For v1 entries that follow those defaults — a 300×250 image upload — that’s correct. For v1 entries that DON’T (generative, brief-driven, host-recorded), the bare annotation is lossy: an SDK projecting display_300x250_generative with bare canonical: "image" would produce a v2 declaration claiming buyer-uploaded image bytes, when the v1 entry actually wants a generation_prompt: text input. v2-aware buyers reading the projection would mis-route. The object form fixes this.
Two cases. Default-slot case (most v1 entries):
canonical-projection-ref.json):
kind→format_kindon the projected v2 ProductFormatDeclaration.asset_source(if set) →params.asset_source. If absent, projection uses the canonical’s default (typicallybuyer_uploaded).slots_override(if set) REPLACES the canonical’s defaultslots[]on the projected declaration. If absent, projection inherits the canonical’s defaults.- v1 entry’s
requirements(dimensions, durations, codecs) →paramsfields per the canonical’s parameter schema. - v1 entry’s
assets[*]MUST be consistent with the resultingslots[]—asset_id↔asset_group_id(asset-group-vocabulary resolves aliases).
display_*_generative entries carry the rich form with asset_source: agent_synthesized and a generation_prompt: text slot override. v2-aware buyers projecting these get a v2 declaration that correctly says “this is a 300×250 image format, produced by agent synthesis, buyer ships a text prompt as input.” A buyer with image bytes can’t satisfy that contract; a buyer with a generation prompt can. The same canonical kind (image) supports both because asset_source + slots_override discriminates on the production model.
“How does a seller say ‘I don’t do generative’?” They already have — by declaring format_kind: image with default slots (no asset_source override). The canonical’s required image_main: image slot excludes generative buyers automatically. To OPT INTO generative, the seller declares asset_source: agent_synthesized and overrides slots[] on their product’s format_options entry. Default behavior is the conservative one.
v2 → v1 linking via v1_format_ref
When a seller has both a published v1 named format AND a v2 declaration for the same underlying product/inventory, the v2 declaration carries v1_format_ref: [{ agent_url, id }] (always an array) linking back to one or more v1 identifiers. The v2 declaration is the source of truth for shape; the v1 format file stays a pure v1 shape — no mirrored declaration.
Multi-size fan-out (normative)
A multi-size v2 declaration withparams.sizes: [{w,h}, ...] of N entries SHOULD carry one v1_format_ref[] entry per size — N v1 named formats covering the N sizes. v1-only buyers then see the product on all sizes via the dual-emitted format_ids[].
When the seller asserts fewer refs than sizes (v1_format_ref[].length < sizes[].length), two cases:
- SDK does NOT fan out (default normative behavior). Emit
format_ids[]carrying only the seller-asserted refs (size loss is real but bounded). MUST also emitFORMAT_DECLARATION_V1_LOSSY_MULTI_SIZEon the responseerrors[]witherror.details: { product_id, declared_sizes, covered_sizes, dropped_sizes }so v1-aware downstream agents see what coverage was lost. The lossy emit is the conservative wire shape — exactly what the seller asserted, no synthesis. - SDK DOES fan out (MAY-do, non-normative). For each entry in
sizes[]lacking a correspondingv1_format_ref, the SDK MAY consult the AAO catalog and look up the per-size v1 named format (e.g., for{width: 728, height: 90}→display_728x90_image). When the lookup succeeds, the SDK MAY emit the catalog-resolved ref underformat_ids[]alongside the seller-asserted refs. SDKs that fan out MUST still emitFORMAT_DECLARATION_V1_LOSSY_MULTI_SIZEas a transparency advisory so downstream consumers know whichformat_ids[]entries were seller-asserted vs catalog-resolved. The advisory’serror.detailsSHOULD includesynthesized_refs: [<list of catalog-resolved ids>].
synthesized_refs.
Inter-SDK convergence rule. Two SDKs processing the same input MAY produce different format_ids[] (one fans out, one doesn’t), but both MUST emit FORMAT_DECLARATION_V1_LOSSY_MULTI_SIZE with consistent declared_sizes / covered_sizes / dropped_sizes. Buyer agents reading the response stream can reconcile divergent format_ids[] against the advisory.
test=false
canonical_parameters field on v1 format.json files (which is deprecated in 3.1 and removed at 4.0). The directional link from v2 → v1 captures the same fact without the parallel-shape drift surface — v1 files no longer mirror the v2 shape.
v1_format_ref is mutually exclusive with canonical_formats_only: true — a declaration either has a v1 home (linked via v1_format_ref) or doesn’t (asserted via canonical_formats_only: true). For format_kind: "custom" declarations, exactly one of the two MUST be set.
v2-only declarations on the wire
A buyer reading a product on the v1 wire path seesformat_options entries with canonical_formats_only: true absent from format_ids. This is intentional and not a producer error. v2-aware buyers reading format_options see them. v1-only buyers see fewer options on format_ids than a v2-aware buyer sees on format_options for the same product — the v1 surface is a strict subset on these products until v1 sunset (5.0).
v2-native sellers emitting to v1 buyers
For v2-native sellers whose products do NOT carry preexisting v1 named formats (e.g., a seller that came up after canonical-formats stabilized and authored onlyformat_options), the question of what to put in format_ids for v1-only buyers has two acceptable answers:
- Default — set
canonical_formats_only: true, omit fromformat_ids. v1 buyers see noformat_idsentry for these declarations. The product is functionally invisible to v1-only buyers, but the v1 surface stays clean (no synthetic identifiers polluting buyer-side allowlists). - Synthesize seller-scoped IDs. When a seller wants v1-only buyer reach, they MAY mint
format_idslike<seller_domain>/canonical_<format_kind>_<param_summary>(e.g.,acme.example/canonical_image_300x250). When synthesizing:- The IDs MUST be seller-scoped (under the seller’s own
agent_url), never underaao-synth/*or any cross-seller namespace — the AAO-mirror-style synthetic namespace was considered and rejected because adopters would index on identifiers with no stable identity. - The IDs MUST be declared in the seller’s published format catalog (the static format file referenced by their adcp-resource manifest, the same place
list_creative_formatsreads from) so v1 buyers see consistent identifiers betweenProduct.format_idsand the seller’s format directory. - The
format_kind_<param_summary>convention is a recommendation, not a normative requirement — sellers MAY use any naming convention scoped to their ownagent_urlnamespace. Buyers MUST NOT pattern-match on the convention for routing (the seller’s catalog is authoritative). - The synthetic
format_idsentry and the correspondingformat_optionsentry MUST satisfy the same dual-emission narrowing contract as any other dual-emitted product (see the projection rules above).
- The IDs MUST be seller-scoped (under the seller’s own
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.jsonand 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. Canonical formats reference them by name. destination_kindsas a new schema —url-asset.jsonalready hasurl_typecovering URL kind disambiguation. Platform-specific destinations (Metamessenger_thread, etc.) are platform extensions.cta_vocabularyas a canonical pattern — CTAs vary meaningfully across surfaces; we let products declarecta_valuesarrays inline until cross-platform demand emerges.list_build_capabilitiesas a separate tool — folded intoget_adcp_capabilitiesundercreative.supported_formats.build_capability,build_capability_ref, and a separateinputsmap — collapsed into the canonicalslotsmodel on the format declaration. The format declares slots (canonicalasset_group_id+asset_type+ constraints); the manifest has a singleassetsmap 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 covered by sibling refinement (no new canonical)
The 12 canonicals cover display, video, audio, native in-feed, retail-media, AI-surface, and responsive-creative archetypes. Several channels look like they want their own canonical but don’t — they’re covered by sibling refinement: the same canonical’sasset_source, slots_override, or applies_to_channels axis handles the difference.
- Linear / addressable TV —
video_hosted+applies_to_channels: ["tv"]. Asset is still a video file with size/codec/duration constraints. The GRP/spot transaction model and addressable household targeting are media-buy + measurement concerns, not creative-format concerns. - OOH / DOOH —
image(orvideo_hosted) +applies_to_channels: ["dooh"]. Asset is still a still image (or short video) with size constraints. Location-keyed measurement (Geopath, COMMB) belongs onsync_event_sources/event_log, not on the format. - Generative-from-prompt — same
format_kindas the buyer-uploaded equivalent (image, video_hosted, audio_hosted) +asset_source: agent_synthesized+slots_overridedeclaring the input shape (generation_prompt: text,creative_brief: brief,video_brief: object). v2-aware buyers see “this format wants a text prompt or structured brief, not image bytes.” - Video native —
video_hosted+applies_to_channels: ["native"]. The asset is still a hosted video file; the difference is renderer placement (in-feed) and is captured by the channel axis. (For non-video in-feed native units — title + image + body assembled by the renderer — use the dedicatednative_in_feedcanonical, which has a meaningfully different assembly shape.)
- Audio dynamic ad insertion (DAI) — ad-stitched audio with mid-stream insertion has a different tracking shape than
audio_hostedoraudio_daast. Likely a specialized canonical oraudio_daastextension 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_vastcanonical handles VAST-tag-driven live insertion today; richer live patterns deferred.
asset_source, (b) slot shape — covered by slots_override, (c) channel — covered by applies_to_channels, or (d) measurement / tracking — covered by sync_event_sources / event_log. New canonical only when the CREATIVE ASSET itself is structurally different (e.g., DAI’s ad-stitched continuous audio stream is structurally different from audio_hosted’s file-per-impression). 50/50 ad formats in the v1 catalog now project to canonicals via this pattern; zero new canonicals were added for broadcast / DOOH / native / generative.
Generative-DSP and multi-output patterns are forward-looking
Theasset_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 viabuild_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.
Codegen vs runtime: the validator is the gate
product-format-declaration.json carries an allOf/if/then/else that conditionally requires format_shape, format_schema, and canonical_formats_only only when format_kind === "custom". The same pattern applies to validate-input-result.json’s result_kind discriminator with conditional violations. JSON Schema captures these conditionals cleanly, but most codegen pipelines (json-schema-to-typescript, datamodel-codegen) strip if/then/else before emitting types because conditional narrowing doesn’t map to TypeScript’s structural type system or to Pydantic’s class model. The generated types are therefore strictly more permissive than the schema:
- Generated TS / Python types accept a
format_kind: "custom"declaration that omitsformat_shapeorformat_schema— the type system has no way to narrow on the discriminator and require the conditional fields. - The Ajv (or equivalent) runtime validator IS the gate. SDKs MUST run the JSON Schema validator before trusting a
ProductFormatDeclarationparsed from the wire; the codegenned type is a convenience layer, not a contract. - Buyer-agent authors writing v2 in TypeScript SHOULD treat the generated types as a starting point and add their own runtime validation step — same pattern adopters already use for any JSON-Schema-validated API. Adopters who skip runtime validation will get type-system success on declarations that the schema rejects, and discover the gap only when their declarations hit a strict downstream validator.
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 |
list_creative_formats shape from v2 product format declarations through 4.0. v2 is the new path, not the only path.
Realistic 3.1 coverage
v1-canonical-mapping.json ships with ~15 unambiguous entries at 3.1 (IAB display sizes, VAST 4.x, DAAST 1.x). The full v1 audit catalogued 86 formats across 12 platforms; ~76% of those (≈65 formats) fit existing canonicals structurally but only project automatically when either (a) the seller adds an explicit canonical field to their v1 format file, or (b) someone files a registry PR adding the format_id_glob or structural match. Out of the gate at 3.1, 71+ of the audited formats are v1-only — they don’t lose support, but a v2-only buyer agent doesn’t see them on format_options until a seller or AAO contributor closes the gap. Through 3.x, expect most product traffic to remain v1 wire shape with opt-in format_options from early-adopter sellers (Meta, NYTimes, AudioStack, generative-DSP, retail-media). Buyer agents planning v2-only consumption in 3.x will see meaningfully thinner inventory than v1-aware agents; planning the codepath as dual-read through at least 3.3 is realistic. The 5.0 sunset for v1 is the floor on dual-emission, not the expected switchover date — anyone planning v2-only should pencil that in for 4.x at the earliest.
Phase status
| Phase | Status | What’s in it |
|---|---|---|
| Phase 1 | ✅ in #3307 | asset_group_id vocabulary registry (canonical entries + audit-grounded aliases), video_brief schema (renamed from earlier scenes), zip asset type, video/audio doc fixes |
| Phase 2 | ✅ in #3307 | 12 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 inline on BrandRef, platform-extension-ref, typed inline product_card / product_card_detailed, format_ids + format_options anyOf on Product (dual emission legal during migration per #3765) |
| Phase 3 | ✅ in #3307 | v1↔canonical-formats migration guide, 12 fully-validated reference Product fixtures + 1 get_products response fixture with bundled extensions, fixture-validation test suite (npm run test:canonical-fixtures) |
| Phase 4 | ⚠️ MUST ship alongside 3.1.0 beta | Reference SDK codegen (TypeScript first, then Python), server-side flatten wrapper reference implementation, platform_extensions URI+digest fetch+cache helper, first-class typed accessors for production_window_business_days and other slot-level scheduling hints (otherwise adopters dig them out of the format blob and lose the v1↔v2 ergonomic win). Without Phase 4, adopters cannot consume canonical-formats cleanly — the typed-tagged-union ergonomics this PR’s design earns require codegen to deliver. Without the flatten wrapper, walled gardens stay v1-only and list_creative_formats reality skews; with it, format_ids is auto-derived from format_options and migration is invisible to v1 consumers. The schemas are shippable today; the SDK work is the gating dependency for adopter consumption and MUST land alongside the 3.1.0 beta, not after. |
Empirical projection coverage
The AAO catalog atcreative.adcontextprotocol.org (the published v1 format library in server/src/creative-agent/reference-formats.json) has 33 of 57 entries (58%) annotated with canonical: <format_kind> — direct v1→v2 projection, no SDK guesswork. The remaining 24 fall into deliberate gaps the spec is explicit about rather than coverage shortfalls:
Final 3.1 coverage: 50/50 ad formats in the v1 catalog are annotated, via the projection-ref object form (canonical: { kind, asset_source?, slots_override? }). Plus 7 UI-scaffolding card entries (product_card_*, format_card_*, proposal_card_*, native_product_card) split into server/src/creative-agent/ui-element-formats.json since they’re not ad formats — they’re agent-interface display widgets that never project to ad canonicals.
How each previously-unannotated group landed:
- 8 generative entries (
display_generative,display_300x250_generative,display_728x90_generative,display_320x50_generative,display_160x600_generative,display_336x280_generative,display_300x600_generative,display_970x250_generative) — annotated as{ kind: "image", asset_source: "agent_synthesized", slots_override: [{ generation_prompt: text, required }] }. v2-aware buyers projecting see “this format wants a text prompt, not image bytes.” Sibling refinement onasset_source+slots_override; noimage_generativecanonical needed. - 3 broadcast (
broadcast_spot_15s/30s/60s) —{ kind: "video_hosted" }. Sellers narrow viaapplies_to_channels: ["tv"]on the v2 product. Same asset shape (video file + duration + codec). - 4 DOOH (
dooh_billboard_*,dooh_transit_screen) —{ kind: "image" }. Sellers narrow viaapplies_to_channels: ["dooh"]. Location-keyed measurement differences live onsync_event_sources, not on the format. - 2 native (
native_standard,native_content) —{ kind: "image", asset_source: "buyer_uploaded", slots_override: [...] }with native-specific slots (icon, disclosure, sponsored_by).
v1_translatable: false / canonical_formats_only: true for canonicals the spec is explicit about not having v1 forms.
The “no new canonical” pattern (normative for future contributions). Generative, broadcast, DOOH, and native all looked like they wanted new canonicals. None of them got one. The pattern: refine an existing canonical via asset_source + slots_override + applies_to_channels, route measurement/tracking differences to sync_event_sources / event_log, and only mint a new canonical when the CREATIVE ASSET itself is structurally different (which is rare). The “How does a seller say ‘I don’t do generative’?” question is settled by this principle — automatic, via the default asset_source: buyer_uploaded and required image_main: image slot.
Related
- v1 → canonical-formats migration guide — concrete migration paths for sellers, creative agents, buyers, and publisher-direct integrations
- RFC #3305 — v2 architecture decisions and rationale
- PR #3307 — Phase 1 + Phase 2 implementation, on hold pending 3.1.0 beta cycle
- Asset group vocabulary — canonical slot-name registry
- Video Brief schema — typed per-segment generation brief for build_creative (renamed from earlier
scenes) - Universal macros — substitution patterns referenced from canonical tracking