Skip to main content

Canonical Formats

TL;DR for adopters reading cold:
  • 9 of 12 canonical formats ship non-experimental at 3.1 GA (image, html5, display_tag, image_carousel, video_hosted, video_vast, audio_hosted, audio_daast, native_in_feed). 3 canonicals stay experimental past GA (sponsored_placement, responsive_creative, agent_placement), and the custom escape-hatch format_kind is inherently experimental until a shape is promoted.
  • AdCP 3.2 is canonical-first end to end. New publisher catalogs, products, manifests, creative-agent capabilities, and build requests use canonical declarations. Named format_id values and list_creative_formats are deprecated compatibility surfaces.
  • Legacy support is read/project only. Implementations MAY derive named-format responses for older 3.x peers, but MUST NOT make a legacy catalog the source of truth for newly-authored 3.2 data.
  • Exact plural format_ids fields end at 4.0. This deadline does not itself remove singular format_id, named-format definitions, or list_creative_formats; those compatibility surfaces have separate schedules.
  • SDK codegen is the gating dependency for ergonomic 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/then narrowing on format_kind: "custom" and result_kind.
Status: Canonical formats were introduced in AdCP 3.1 and are the required authoring and discovery model in AdCP 3.2. Three canonicals plus custom remain marked experimental until adopter evidence supports promotion. Historical design context lives in RFC #3305 and #3307. 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 on Product.format_ids vs Product.format_options.
Canonical 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 inline 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. For hands-on authoring practice, use the S2 creative specialist module, which links to this reference instead of duplicating it.

Glossary

Architectural shift

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).

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 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 three canonicals plus the custom escape hatch: The 7 IAB / VAST / DAAST / IAB-Native re-encodings (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 declaration experimental: true, buyers stay on the canonical path but SHOULD preflight the exact declaration and SHOULD avoid production spend unless the buyer has explicitly opted into experimental inventory. A deprecated named-format projection does not make an experimental runtime path safer.

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 modelcomposition_model: deterministic | algorithmic on the format declaration. Describes how the surface composes per-impression:
  • deterministic — buyer can predict per-slot rendering. The surface serves what it received. (image, video_hosted, audio_hosted, video_vast, audio_daast, sponsored_placement.)
  • algorithmic — surface picks combinations from a buyer-supplied asset pool per-impression. The buyer ships a pool; the surface composes. (responsive_creative for Google PMax / Meta Advantage+; agent_placement for AI-surface composition.)
Production sourceasset_source describes who renders or owns the source asset, and when:
  • asset_source on image, video_hosted, audio_hosted — shared enum: buyer_uploaded | publisher_host_recorded | seller_pre_rendered_from_brief | seller_human_designed | agent_synthesized | publisher_owned_reference. publisher_host_recorded is audio-specific (podcast host-read pattern) and meaningful only on audio_hosted. publisher_owned_reference is meaningful when the product’s slots accept a reference asset such as published_post.
  • required_connections on any canonical declaration — downstream platform connections or grants the seller needs in addition to the single AdCP caller credential. Use it for products that require multiple platform-side connections, such as an advertiser account plus a publisher identity for published-post references.
  • item_production_model on sponsored_placement — same axis, 4-value subset (drops publisher_host_recorded), applied per catalog item (the multi-output generative case: 1 brief × N catalog items → N rendered creatives)
The two axes don’t collapse. A generative DSP that produces ONE rendered image from a brief is composition_model: deterministic (the surface serves what it received) + 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 or resolves 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 vs existing post reference). Downstream platform authorization is separate from production source. If a format requires platform-side connections, declare them with required_connections[]. For example, a published-post reference product can require both advertiser_account and publisher_identity; the buyer still authenticates to the seller once, while the seller manages those downstream grants. Missing grants surface as AUTHORIZATION_REQUIRED with error.details.missing_connections[], not as a new format family or identity-discovery task.

Tracker assembly under seller-rendered sources

When asset_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 optional landing_page_url and the buyer’s measurement-vendor pixels declared via platform_extensions on the format, filtered by extensions[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_creatives response SHOULD include a tracker_block field listing the impression URL pattern and click URL pattern. Buyers register those with their measurement vendor at sync time. This path covers the generative-DSP pattern where the serving artifact and the tracking shape are produced together.
vast_tracker and daast_tracker decomposed tracker assets work for both buyer_uploaded and seller-rendered sources — when the seller renders, those tracker assets are inputs to the rendered tag, attached to the appropriate VAST/DAAST <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: Rule of thumb. Before reaching for a new format_kind, check whether the difference is:
  1. Creative type (image vs video vs audio vs html5 vs 3p-tag) → format_kind, the only knob it controls.
  2. Production model (who renders, when) → asset_source on the format declaration.
  3. Slot shape (what assets the buyer ships) → slots_override on the projection ref (catalog side) or on the v2 product’s format_options[] declaration.
  4. Delivery medium / channel (TV vs streaming vs DOOH vs social) → applies_to_channels on the v2 product.
  5. Measurement / tracking / event model. Splits two ways:
    • Renderer-fired trackers (the renderer hits a URL when serving / viewing / clicking) → pixel_tracker asset (or vast_tracker / daast_tracker on those formats). Lives on the creative manifest as a typed slot. Buyer’s measurement vendor URLs the seller’s renderer fires at serve time. See docs/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.
  6. Targeting context (audience vs geo vs daypart) → media-buy targeting overlay, not the format.
New 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. The rule applies symmetrically: if you’re tempted to add 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-form ext.

The mechanism

test=false
Three required pieces when format_kind: "custom":
  1. 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.
  2. format_schema — URI+digest reference to a fetchable schema describing the shape’s actual params and slots. Same hosting model as platform_extensions: open-ecosystem publishers host the artifact at the canonical URI on their subdomain; closed-platform / walled-garden shapes resolve through the AAO mirror at https://creative.adcontextprotocol.org/translated/.... Buyer agents fetch by uri@digest (immutable per digest, aggressive caching), validate params and slots against the fetched schema, and reason about manifests structurally.
  3. params — the actual structure, governed by the schema fetched from format_schema.uri. AdCP doesn’t bake the params shape; the seller’s schema does.

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).
  • $ref sandboxing: 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 $ref to arbitrary URIs MUST be rejected. $ref: file://... MUST be rejected. Transitive $ref depth ≤8 AND total $ref count ≤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, pattern regexes evaluated with re2 OR 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 via errors[], do NOT fail the whole get_products response.
  • 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

A format_shape entry is promoted to a first-class format_kind when:
  1. At least 2 production adopters ship it via custom + format_schema
  2. 90 consecutive days without a breaking change to the shape adopters converged on
  3. The shape has a defined tracking model (which signals fire, which trackers attach, what the impression contract is)
  4. The working group opens a per-canonical promotion issue, drafts a canonical schema (/schemas/formats/canonical/<name>.json), lands a fixture, and ships in the next minor release
Same governance pattern that produced the 12 canonicals from the v1 audit. The promotion queue lives at adcp#3666; current candidates are the 9 entries in the format-shape registry. Promotion is a wire-shape change for consumer code. Any client branching on 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):
  1. Transition window (≥90 days): sellers MAY emit both shapes simultaneously — format_options[] carrying one format_kind: "custom" + format_shape: "<name>" declaration AND one format_kind: "<promoted_name>" declaration.
  2. Consumer-SDK deprecation warning: SDKs SHOULD emit a structured deprecation warning via their lint channel (same surface as FORMAT_PROJECTION_FAILED) when they see format_kind: "custom" with a format_shape that’s been promoted. Payload: { format_shape, promoted_to, promotion_release, transition_end }.
  3. promotion_status lifecycle: the registry entry’s promotion_status updates from tracking — see adcp#3666 to promoted to <format_kind> in <version>; transition ends <date> when the working group schedules promotion. SDKs MAY read this at codegen / runtime.
  4. Post-transition: sellers SHOULD drop the legacy format_kind: "custom" declaration. Buyers MAY then assume format_kind == "custom" is a long-tail / non-promoted shape.
Without this contract, every promotion event silently breaks adopter code; with it, the deprecation warning is the early signal during the transition window.

Asset group vocabulary

Format slots reference canonical asset_group_id values from the vocabulary registry. The current canonical entries: Non-canonical asset_group_id values remain valid for platform-specific extensions; validators MAY emit soft warnings on non-canonical IDs to encourage convergence. Aliases are recognized one-way (v1 alias → v2 canonical) when migrating; new manifests SHOULD use the canonical IDs.

Worked example — Meta Reels

Meta Reels 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 — canonical params, an inherited or overridden slot, the brand layer, the campaign layer — and the canonical doesn’t need to grow.

Where each Reels feature lives

Where to declare a format

Canonical formats participate in three independent assertions. Pick the surface by the question being answered; no one catalog substitutes for the others.

Format contract, creative producer, and delivery runtime

Do not use “renderer” for all three roles; it hides the trust and routing boundaries: Creative declarations describe structural production and acceptance constraints. Price, floor, availability, auction mechanics, and deal eligibility remain product or pricing facts; copying them into format parameters would create multiple authorities for the same commercial term. Classification follows the buyer deliverable and who renders it, not the behavioral richness of the placement. For example, a buyer-uploaded MP4 in a docking player is video_hosted; an opaque third-party tag that produces the same experience is display_tag. Maintainers and catalog encoders can verify these boundaries against the AdCP 3.2 classification vectors. A publisher declares the format contract and enough production semantics for a buyer to know what it must supply: canonical kind, parameter constraints, slots, asset source, composition model, and required connections. It does not maintain a vendor list of every creative agent capable of producing that contract. Creative agents self-declare compatibility. For multi-publisher products, Product.format_options[] is the union of contracts the sales agent can traffic somewhere in the product. Eligibility is then computed per placement by intersecting that product set with the applicable publisher/placement declaration. One creative used across every selected placement must satisfy the intersection of all those effective sets; workflows that assign distinct creatives per placement may use the union, but must cover every placement. If the product has no explicit placements[], treat each publisher_properties[] selector as an eligibility slice. Product-local options apply to every slice; publisher-scoped options apply only to slices with the matching publisher_domain. A single creative must satisfy the intersection across slices, while distinct creatives must cover every slice. If any slice cannot be resolved from the product and publisher catalog, fail closed and require explicit placements or a refined product. For exact publisher-format support, a creative capability’s format repeats the publisher declaration’s {publisher_domain, format_option_id} identity and narrows the same canonical contract. Generic creative agents advertise a canonical parameter envelope without publisher identity. A generic capability matches only when it can satisfy every constraint on the target declaration; equal format_kind values alone are not proof of compatibility. Registries reverse-index those capability declarations. For example, a buyer can find builders for a fictional short-video publisher’s exact format with:
If a publisher wants to endorse or certify specific production partners, that is a separate trust relationship. It MUST NOT be encoded as part of the structural format declaration. Product declarations, buyer selectors, and placement references are intentionally different shapes:
  • Product.format_options[]: full declarations; never bare {format_option_id} references.
  • PackageRequest.format_option_refs[] / creative-manifest.format_option_ref: buyer selectors using FormatOptionRef.
  • adagents.json placements[].format_options[]: same-file placement references may be bare {format_option_id}.
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 format option”). Property-level format support → applies_to_property_ids. Placement-level binding → placements[].format_options[]. Naming boundary: format_option_id selects a buyable product or publisher-catalog format contract. Creative-agent capability_id remains separate: it selects a build path on creative.supported_formats when calling build_creative. Do not use capability_id on media-buy products, placements, package requests, creative manifests, or creative assets.

Sample renders and declaration authority

Starting in 3.2, sample_render_url restores the human-preview path that publisher-owned canonical formats otherwise lose when there is no creative agent owning the format. It answers “what does this declared format look like?” using example assets selected by the declaring party. It does not render buyer assets. Authority follows the enclosing declaration rather than the shared ProductFormatDeclaration type: A bare {format_option_id} in Placement.format_options[] resolves to the same file’s top-level declaration and therefore inherits its sample_render_url. A product entry is always a full declaration and does not implicitly inherit presentation metadata. When a product entry carries {publisher_domain, format_option_id}, a UI MAY resolve the catalog. A tier-1 publisher-hosted sample SHOULD be preferred when presenting a preview as publisher-authored. A community-mirror result MUST be labeled community-maintained and MUST NOT be presented as publisher-authored. A sample copied directly onto the product remains seller-supplied unless it was verified against the publisher-hosted catalog. Sample URLs are untrusted external navigation. They SHOULD be public and unauthenticated. Consumers MUST NOT auto-fetch or iframe a sample solely because it appears in a declaration, and MUST NOT attach buyer assets, authorization credentials, source-origin or account context, or user-specific query parameters. Ordinary browser state belonging to the destination origin is outside AdCP’s control. Human-facing clients SHOULD open the URL only after explicit user action in a new browsing context with opener and referrer information suppressed. Dynamic preview is a separate capability. Rendering a buyer’s creative manifest requires an authorized renderer and canonical format context for preview_creative; sample_render_url is neither renderer discovery nor a substitute for that task.

Publisher catalog resolution

Buyer agents answer “what formats does this publisher accept?” by resolving the publisher catalog, not by asking a sales agent for its global format list:
  1. Publisher-hosted: fetch https://<publisher_domain>/.well-known/adagents.json. If it carries formats[], those declarations are publisher-authoritative.
  2. AgenticAdvertising.org community mirror: on 404 or absence of formats[], query the AgenticAdvertising.org registry for a community-maintained mirror. Mirror declarations are advisory and preserve their lower-authority provenance.
  3. No publisher catalog: stop. Do not synthesize a publisher declaration from one sales agent’s products. Buyers can still discover that agent’s sellable formats from get_products, but those product declarations are not promoted into publisher facts.
Publisher-origin fetches MUST follow the same transport contract as 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. The community fallback is a query to AgenticAdvertising.org’s fixed registry API, not a client-directed fetch of an arbitrary mirror URL. Community-mirror governance. AgenticAdvertising.org publishes catalog translations for unadopted platforms. Entries are best-effort and derived from public platform specifications. Buyers MUST preserve aao_mirror provenance, prefer publisher-hosted declarations, and treat stale mirrors as unavailable rather than relabeling one seller’s product catalog as a publisher declaration. 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 a Product with publisher_properties[].publisher_domain = "meta.example" and needing to know “what formats does this publisher accept, scoped to property ID instagram?” 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. publisher_properties[].publisher_domain names the catalog host; property_id identifies the property inside that file.
Step 1 — Resolve the publisher catalog. Buyer SDK fetches https://meta.example/.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). The platform has not adopted AdCP, so the fetch returns 404. Step 2 — Fall back to the AgenticAdvertising.org registry. On Step 1’s 404 (or 200-with-no-formats[]), the buyer queries GET https://agenticadvertising.org/api/registry/publisher?domain=meta.example&include=placements. A response with files.adagents_json.status: "community" carries moderator-maintained, advisory formats[] and placement summaries; it is not publisher-origin authority. The buyer preserves that provenance and uses files.adagents_json.registry_url only when it needs the complete mirrored document rather than the registry’s intentionally lossy summary. Step 3 — Respect registry precedence. The registry prefers a successfully crawled publisher-origin document over its community entry. If the publisher later adopts AdCP, the same query reports the origin-backed source; clients do not merge the old community catalog into the publisher declaration. 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”] → matches
  • meta_feed_image → applies_to_property_ids: [“instagram”, “facebook”] → matches
  • meta_stories_video → applies_to_property_ids: [“instagram”, “facebook”] → matches
  • meta_feed_carousel → applies_to_property_ids: [“instagram”, “facebook”] → matches
The Product on hand carries a full declaration tagged with publisher_domain: "meta.example" and format_option_id: "meta_reels" (for example, format_kind: "video_hosted" plus params). The {publisher_domain, format_option_id} pair lets the buyer match that product declaration to the catalog declaration; the product entry is not a bare reference. Step 5 — Resolve placement references (if any). If the publisher catalog includes placements[] and a placement carries format_options: [{ format_option_id: "meta_reels" }], the buyer resolves the format_option_id against the SAME file’s top-level formats[]. Cross-file lookup is not supported by design because same-file resolution keeps validators bounded and prevents one file from squatting on another publisher’s format_option_id. When the reference is broken — format_option_id not present in formats[] — the SDK MUST surface FORMAT_OPTION_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 plus catalog_etag when present, falling back to HTTP validators (ETag/Last-Modified) and then a bounded TTL. Subsequent products from the same publisher reuse the cached file until the catalog token or HTTP validator changes, then re-resolve placement and format references. Concrete payload sequence (Meta Reels, scoped to Instagram):
The resolver records whether the catalog came from the publisher origin or an AgenticAdvertising.org community mirror. Sales-agent product declarations retain seller provenance and are never reported as a publisher catalog.

Community-registry hosting

When a platform has not adopted AdCP, AgenticAdvertising.org may maintain an advisory community adagents.json translation and expose it through the publisher registry lookup. A mirror for fictional Acme Social can declare its short-form video once, scoped to Acme Feed and Acme Clips, for compatibility checks across sellers. Buyers query the registry API first for provenance-aware summaries and follow files.adagents_json.registry_url only when they need fields omitted by the lossy projection. When the platform publishes its own origin file, that publisher-hosted declaration takes precedence and the community entry is no longer returned as authority.
test=false
Buyer SDKs answer the publisher-format question by resolving this file (after checking the publisher origin first) and reading its canonical formats[]. They do not route the question through a sales agent’s deprecated list_creative_formats task.

Product reuses the catalog declaration

A seller’s meta_reels_us product reuses the publisher-catalog declaration by carrying a full product declaration tagged with {publisher_domain, format_option_id}. The product may narrow parts specific to that product (geography, pricing, stricter params), but it still emits format_kind and params inline; {publisher_domain, format_option_id} is the matching key, not a standalone reference payload. Until Meta publishes its own AdCP catalog, buyer SDKs resolve the catalog through the AAO mirror at creative.adcontextprotocol.org/translated/meta:
test=false
The {publisher_domain, format_option_id} pair lets buyer agents recognize this as the same Meta Reels format option they read from the publisher catalog — the seller didn’t reinvent the format, they’re selling inventory against the catalog declaration. Buyers select it with FormatOptionRef, for example { "scope": "publisher", "publisher_domain": "meta.example", "format_option_id": "meta_reels" }. 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_hosted inherits video_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 in get_products so 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 put pixel_id in platform_extensions on a creative format.
  • Media-buy surfaces — placement selection (Feed vs Reels vs Stories). Pick the right format on the publisher catalog (meta_reels vs meta_stories_video vs meta_feed_image); not a per-creative extension knob.
This separation — canonical params + canonical slots + extensions only for net-new fields + BrandRef for brand context + event_log for tracking — is what keeps the 12 canonicals from accumulating per-platform fields. The lint at 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_kind is the creative TYPE — one of image, html5, display_tag, native_in_feed, video_hosted — and never carries dimensional identity.
  • Size lives in params as one of three modes (mutually exclusive):
    • Fixed: width + height integers — 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 OpenRTB banner.format[].
    • Responsive: min_width/max_width + min_height/max_height — accepted dimensional ranges for slots that adapt to viewport.
A flexible publisher slot becomes one product with N format_options — one per creative type — each carrying the appropriate size declaration. Buyers pick the creative type they ship; the size matches one of the listed pairs (or falls within the responsive range). The example below is the NYTimes Homepage above-the-fold slot: accepts image, HTML5, or third-party tag at any of three IAB sizes. Three format_options, one product, one price.
test=false
What’s happening: one product, three 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.

Creative locale policy

locale_policy lets a seller narrow a product format by the creative languages it can execute without inventing a new format kind or an identifier such as display_image_fr_only:
test=false
Seller ranges use RFC 4647 section 3.3.1 Basic Filtering. The range fr accepts materialized variants tagged fr, fr-CA, and fr-FR; the narrower range fr-CA does not accept fr or fr-FR. Ranges are ORed, must be canonical concrete BCP 47 tags, and do not support *. Use zxx explicitly for creative with no linguistic content. und means unknown and is not a wildcard. Presence is a hard constraint. A creative is assignable to the format option only when its materialized locale topology contains at least one matching source or target variant. Source-only topology is valid for a monolingual creative. A mixed English/French creative remains one creative with one review lifecycle; the French policy creates an eligibility mask for this assignment without deleting or changing its English variant. Each locale tag asserts that the fully resolved variant—including inherited copy, disclaimers, and audio—is suitable for that locale. Sellers may reject a mislabeled or mixed-language variant during normal creative review; optional language metadata on individual text assets is not a substitute for creative locale topology. Sellers publishing any locale-constrained product or placement option MUST advertise both creative.localization and creative.has_creative_library: true from get_adcp_capabilities. Inline media-buy creatives cannot carry locale topology; buyers sync the library creative first and then assign it. The seller applies that mask before the buyer’s delivery policy:
  1. Filter source and target variants through accepted_language_ranges.
  2. Reject assignment with CREATIVE_LOCALE_NOT_ACCEPTED if none remain.
  3. Apply strict RFC 4647 Lookup, explicit locale_fallbacks, and then unmatched_locale_action only within the eligible set.
  4. When unmatched_locale_action is serve_default, reject assignment unless default_locale_variant_id is eligible. A buyer fallback or default never overrides seller eligibility.
These checks run independently for every effective placement format option where the assignment may serve. An assignment spanning several placements is rejected if any in-scope placement has no eligible variant or cannot honor serve_default; the buyer may instead narrow the assignment’s placement scope. To reuse one mixed creative across products with disjoint locale policies, use do_not_serve or separate creative topology rather than asking one global default to promise different product-specific defaults. The seller snapshots the effective policy when it accepts the assignment. Later catalog changes affect only new or changed assignments and never silently rewrite or remove an existing buyer-managed assignment. Product and placement declarations intersect. When the product has no locale policy, a placement may introduce one as a narrowing of the otherwise unconstrained option. When both declare policies, a placement may narrow product range fr to fr-CA, but may not broaden product range fr-CA to fr. A placement without a locale policy inherits the matching product format option’s policy. Locale-constrained declarations are canonical-only: they MUST set canonical_formats_only: true and MUST NOT carry v1_format_ref. Legacy format_ids cannot preserve locale eligibility, and projecting this option would recreate the non-executable-buy problem the field prevents. When a placement introduces the policy, its resolved catalog declaration and the matching product declaration are also canonical-only, and no product or placement format_id may project to that effective option. Buyers use a format-options-aware canonical selector: format_option_refs when a published option ID is available or needed for disambiguation, otherwise direct format_kind plus satisfying params; they never rely on format_ids for the constrained route.

Worked example — Podcast 30s host-read

Host-reads are the host-recorded-from-buyer-script pattern. The product declares audio_hosted narrowed to publisher-host-recorded mode with slots describing what the buyer ships (a script text asset; the publisher’s host records audio from it):
test=false
The format declaration tells the buyer everything they need to know — no extra capability lookup. The buyer ships a script text asset under that slot in the manifest’s assets map; brand context comes from the manifest’s top-level brand BrandRef. There is no separate “inputs” map — everything the buyer ships lives in assets. The buyer has two flows depending on whether the seller doubles as a creative agent and whether the buyer wants to pre-produce externally.

Flow 1 — buyer pre-produces (upstream creative agent)

The buyer calls a creative agent’s build_creative independently, gets back a rendered manifest, and submits that to the seller. Useful when the buyer has a preferred production partner (their in-house studio, AudioStack-style services) or the seller exposes itself as a creative agent.
  1. Buyer reads The Daily’s product format → sees slots: [{ asset_group_id: "script", asset_type: "text", required: true }] declared
  2. Buyer finds a compatible entry in that agent’s get_adcp_capabilities.creative.supported_formats[], then calls build_creative({ target_capability_id: <that entry's capability_id>, creative_manifest: { format_kind: "audio_hosted", format_option_ref: <The Daily product option>, assets: { script: { asset_type: "text", content: "..." } } }, brand: { domain: "..." } }) — this could be The Daily’s own creative-agent surface (if they expose one), or any other agent that advertises a compatible build capability
  3. Receives a rendered manifest with audio asset
  4. Submits the rendered manifest via sync_creatives to The Daily’s sales agent

Flow 2 — seller produces internally

The buyer submits assets directly to the seller; the seller produces internally (calls its own creative team or an upstream creative agent under the hood) and returns a registered creative.
  1. Buyer reads the same product format
  2. Buyer submits via sync_creatives with the assets in the manifest (e.g., a script text asset under that slot in the assets map)
  3. Seller produces internally; how is invisible to the buyer
  4. Returns async status; buyer polls or waits for completion
The format’s 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 (CanvasForge + StreamHaus 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 (StreamHaus) — sells the placement, validates manifests against the canonical its product narrows, and does not maintain a list of approved creative vendors
  • Creative agent (CanvasForge) — produces creatives via build_creative and declares its producible catalog through creative.supported_formats on its own get_adcp_capabilities
The buyer chooses the creative agent independently of the seller. Publishers and sellers do not maintain a vendor list. Buyers compare canonical declarations: “CanvasForge can produce image 300×250 ≤200KB; StreamHaus accepts image 300×250 ≤200KB; they’re compatible.”

1. Buyer reads StreamHaus products

Buyer calls get_products on StreamHaus. The MREC product narrows canonical image:
test=false
The product narrows the canonical; the canonical is what StreamHaus commits to validating against. StreamHaus does not validate against CanvasForge’s narrowing—buyers do not need to know which creative agent produced the manifest, and CanvasForge-specific parameters live in CanvasForge’s platform extensions if needed.

2. Buyer calls CanvasForge’s build_creative

test=false
CanvasForge renders an MREC PNG and returns a canonical manifest:
test=false

3. Buyer ships to StreamHaus

Buyer calls sync_creatives on StreamHaus with the manifest from CanvasForge. StreamHaus:
  1. Validates the manifest against canonical image (300×250, ≤200KB, SSL).
  2. Validates against the product’s narrowing (matches — same params).
  3. Does NOT validate against CanvasForge’s narrowing — that’s the creative agent’s contract with the buyer, not the seller’s contract.
  4. If valid → creative registered. If not → returns canonical violations (width mismatch, max_file_size_kb exceeded).
The seller’s validation contract is the canonical, not the creative agent. This is what makes the third-party path additive rather than coupled: the buyer can swap creative agents without changing the seller-facing flow.

Worked example — generative DSP (universalads-class, 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 at sync_creatives time — it is NOT a creative agent the buyer calls separately. The buyer ships a brief plus structured copy; the seller renders ONE image and serves it like any deterministic creative.
test=false
Buyer’s manifest carries the brief, headline, and clickthrough URL — no rendered image asset. Seller’s sync_creatives produces the rendered MREC PNG and registers it. Two axes: composition_model: deterministic (the surface serves what it received), 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 (third-party html5 OR internal display_tag)

A placement that accepts EITHER a third-party-hosted creative OR an internal tag — buyer picks at sync_creatives time by aligning their manifest’s format_kind and, when needed, format_option_ref to the matching declaration:
test=false
Buyer’s manifest, targeting the html5 option:
test=false
Routing rule for multi-element format_options (normative):
  • format_kind selects the canonical and its slot vocabulary.
  • format_option_ref is REQUIRED on the manifest when the target product’s format_options contains two or more declarations sharing the same format_kind — without it, the seller can’t disambiguate which option the buyer is shipping against.
  • format_option_ref is OPTIONAL when each format_kind in the product’s format_options is unique (the example above: one html5 entry, one display_tag entry) — format_kind alone routes the manifest. Buyers MAY still send format_option_ref as a clarity hint.
In this example each option carries a distinct format_kind, so format_option_ref 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. The cleave is asset-bundle vs catalog-row composition, not “is it Pinterest.” Same logic applies to Snap Story Ad (native_in_feed) vs Snap Collection (sponsored_placement), TikTok TopView (native_in_feed via applies_to_channels: ["social"]) vs TikTok Collection (sponsored_placement), and so on. Buyer agents route on the composition shape; the publisher’s brand of the surface is incidental. The fanout_mode: single_item case above is its own family: catalog-driven render where the platform composes one SKU per impression rather than a multi-item collection. Meta Dynamic Product Ads (single-product render), Snap Collection in single-item mode, and TikTok Shopping single-SKU all map to sponsored_placement with fanout_mode: single_item — the buyer ships a catalog reference and the seller renders one item per ad, with the platform selecting which item. This is still catalog-row composition; it differs from multi_item_in_creative only in how many items land in one creative. See Sponsored Placement adapter contracts for the per-adopter runtime contracts (the Collection-layout family in §3 covers Pinterest/Snap Collection). 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 preflight 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
Response carries per-target results. The canonical accepts the duration (canonical 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_creative call. If the manifest can’t even narrow against canonical, the buyer saves the synthesis cost. Especially relevant for nondeterministic-synthesis products where each retry has real GPU cost.
  • Multi-target preflight during product selection. A buyer comparing 10 candidate products asks validate_input once with all 10 product_ids; gets back per-target results. Cheaper than 10 separate sync_creatives round-trips.
  • Debugging a rejected manifest. When sync_creatives returns format violations, calling validate_input against the canonical alone narrows the question to “is my manifest fundamentally broken vs is the seller’s product narrowing or trafficking policy the gating constraint.”
  • Preview-render gating (formats with composition_model: algorithmic or synthesis_nondeterministic: true). The platform’s preview surface is a richer follow-on; validate_input is the cheap pre-flight that gates whether previewing is even worth attempting.
When NOT to use validate_input:
  • As the launch-critical check for a creative you intend to upload to a seller. Use sync_creatives directly, or sync_creatives with dry_run: true if you need a non-mutating rehearsal of the actual upload request.
  • For products where the seller’s narrowing is unknowable client-side without fetching extensions. validate_input pulls extensions same as sync_creatives does — there’s no discovery shortcut.
  • For high-volume per-impression decisions. validate_input is per-target, not per-impression. Operational scale (hundreds of products × N format_options) belongs to client-side filtering against the cached get_products response.

validate_input vs sync_creatives dry run

For the third-party creative-agent flow: use validate_input when it can save an expensive or slow build_creative call, then call build_creative on the creative agent and sync_creatives on the sales agent. For the in-house pre-rendered flow: skip build_creative; use validate_input only when comparing targets or debugging, and use sync_creatives with dry_run: true when you need to know whether the seller would accept the upload. For the seller-renders-from-brief flow (universalads-class): skip build_creative (the seller does the rendering at sync_creatives time); use sync_creatives or sync_creatives dry run for the operational acceptance check. See build_creative task reference for the full request/response shape.

Duration constraint precedence

Hosted video and hosted audio products can express duration constraints in two modes:
  1. duration_ms_exact for a fixed required duration
  2. duration_ms_range for a bounded or one-sided range
There are no separate min-only or max-only duration fields; one-sided duration_ms_range covers those cases without adding a third duration vocabulary. duration_ms_range is [min, max] in milliseconds. Either endpoint MAY be null to express an unbounded side: [null, 60000] means “up to 60 seconds”, and [15000, null] means “at least 15 seconds”. [null, null] is invalid because at least one endpoint must be bounded. When both modes appear on the same declaration, duration_ms_exact wins over duration_ms_range. Producers SHOULD emit only one mode. SDKs SHOULD lint a warning when both modes ship, but consumers MUST still apply the precedence rule. A fixed 60-second spot can use duration_ms_exact: 60000 or the equivalent closed range [60000, 60000]; prefer duration_ms_exact when the product truly requires one duration.

Image pixel density

Image width, height, and sizes[] are logical render dimensions. pixel_ratios declares the accepted intrinsic-pixel densities for the same logical placement:
This declaration accepts one 300×250 image at 1x or one 600×500 image at 2x. It does not require both assets, and it does not declare a logical 600×500 placement. When pixel_ratios is absent, SDKs MUST use [1] so existing declarations retain their exact-dimension behavior. An image asset MAY carry pixel_ratio. When it is omitted, SDKs MUST infer density when the logical dimensions produce exactly one matching size/ratio pair. The intrinsic width and height must scale by the same accepted ratio. If multiple pairs match (for example, 600×500 could be 600×500@1x or 300×250@2x), validation MUST fail with pixel_ratio_ambiguous until the asset declares its ratio. If the declaration has no size constraint, omitted pixel_ratios remains an inferable 1x; a declared non-1x acceptance set requires asset.pixel_ratio. SDK validation MUST follow the reference vectors in static/test-vectors/canonical-image-pixel-ratio.json. In particular, size and density are independent axes, declared metadata must agree with intrinsic dimensions, and one matching asset satisfies one required image slot unless that slot declares required rendition coverage. The same acceptance constraint is available as slots[].pixel_ratios on every canonical image-bearing slot, including native images, carousel cards (where it applies to nested image media), responsive-creative image pools, and companion images. The top-level image.params.pixel_ratios shorthand applies to the canonical image’s image_main slot. When both are present, SDKs MUST compute the effective acceptance set as their intersection for both single assets and rendition arrays; neither declaration takes precedence. An empty intersection is invalid and fails with pixel_ratio_intersection_empty. Pixel ratio is invalid on slots that cannot carry image media.

Required rendition sets

Some delivery systems select among multiple renditions of the same logical image at serve time. A slot declares that requirement with required_pixel_ratios, which is a required-coverage subset of its accepted pixel_ratios:
The matching manifest supplies the slot as a rendition array. Each entry remains an ordinary image asset with intrinsic dimensions and optional explicit density:
Every required ratio MUST appear in the effective acceptance intersection, and the array MUST contain exactly one valid rendition for each required ratio. A ratio present on the slot but excluded by top-level image.params.pixel_ratios is not accepted. Accepted ratios not listed as required are optional. All entries in a rendition set represent the same semantic image; only density-specific encoding properties such as URL, intrinsic dimensions, format, and file size may differ. SDKs MUST reject missing required coverage with required_pixel_ratio_missing, duplicate density entries with pixel_ratio_duplicate, and required ratios outside the effective accepted set with required_pixel_ratio_not_accepted. min and max SHOULD bound the expected rendition count, but density coverage—not array length alone—is authoritative. When required_pixel_ratios is absent, the original acceptance behavior remains: one image matching any accepted density satisfies the slot. Card media remains singular, so required_pixel_ratios is valid only on an image slot; a card slot may use pixel_ratios only as an acceptance constraint for each card’s image.

Format matching vs product satisfaction

Sellers and SDKs MUST normalize legacy named formats before comparing them to canonical declarations. A legacy format_id such as { "agent_url": "https://creative.adcontextprotocol.org", "id": "display_300x250" } resolves through the explicit canonical annotation, v1_format_ref, or the canonical mapping registry to a canonical declaration such as format_kind: "image" with params.width: 300 and params.height: 250. After that projection, implementations compare the canonical shape and parameters, not the raw (agent_url, id) pair. Two related checks use different directionality:
  • Equivalence matching answers whether two declarations identify the same underlying creative shape after normalization. display_300x250 and format_kind: "image" with width: 300 and height: 250 are equivalent even though their wire identifiers differ.
  • Product satisfaction answers whether a submitted or requested creative is specific enough for a product’s accepted format declaration. When the product declares a fixed width, height, duration_ms_exact, or duration_ms_range, the requested creative or package selector MUST declare and match that constraint. An under-specified request is not a wildcard for product gating.
Range constraints use containment, not overlap. A range-based request satisfies a product only when every value the request permits falls within the product’s accepted range; overlap alone is insufficient. An exact value, such as duration_ms_exact, satisfies a range when that exact value falls inside the accepted interval. Concrete examples: This asymmetry prevents two failure modes: exact-ID comparison rejecting compatible legacy/canonical pairs, and overly-broad matching allowing an under-specified request to satisfy a fixed-size or fixed-duration product.

Discovery + validation at scale

A high-product-count buyer (TTD-class with ~100s of products per get_products response) cannot pre-flight every product via validate_input per round — N products × M format_options × per-target round-trips becomes operationally expensive. Two patterns address this:
  • Client-side filtering against the cached get_products response. Buyers who know their manifest’s format_kind and parameter bucket (canonical, dimensions, duration) filter the product list client-side before validating. The format declarations are already inline on each product — buyers don’t need a separate fetch to filter. This is the dominant pattern for “validate against the products that could possibly accept my creative” and reduces the validate_input set by an order of magnitude.
  • Multi-target validate_input. When the filtered set is still wide (5-50 products), call validate_input once with all candidate product_ids in targets[]. The response carries per-target results in a single round-trip. Cheaper than per-product calls and structurally aligned with the schema (one request, many results).
For genuinely high-volume scenarios (hundreds of candidate products, real-time bidding pre-flight), buyers should rely on cached get_products responses + client-side filtering as the primary path; validate_input is reserved for the narrowed candidate set or for debugging unexpected rejections. The applies_to_channels field on each format_options element narrows further when a product spans multiple channels.

Preview as the universal “what does this produce” surface

Buyers ship assets per the format’s slots declaration; preview_creative shows what the output renders as. The seller’s response to a creative submission can also include a preview URL — the buyer doesn’t need a separate preview call to verify that their submission produced the intended output. Same surface, two production paths:
  • Direct rendering: buyer ships finished creative assets (image, video, audio) → seller renders them on the placement → preview shows the rendered output (with seller-side composition, overlays, CTA buttons applied).
  • Seller-side production: buyer ships content the seller consumes (script text, creative_brief, voice_id selection) → seller produces the rendered asset internally (host recording, generative AI synthesis, transcoding — invisible) → preview shows the produced output.
The buyer can iterate on shipped assets and inspect previews before committing to a buy. Different sellers may produce differently internally; the preview surface is uniform. This is what makes “production mechanism is invisible to the buyer” workable in practice — the buyer doesn’t need to know HOW the output was produced because they can see WHAT was produced.

Brand identity via brand.json (with override)

v2 formats no longer redeclare brand_logo, brand_colors, brand_voice, brand_tagline as explicit slots. When a manifest carries a BrandRef 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
Override fields take precedence over 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:
Each extension’s response carries the schema, the canonical pattern or slot it extends, a version, and a content digest. Hosting paths — two separate flows. v2 supports two hosting models, depending on whether the canonical URI’s owner participates in the open AdCP ecosystem or operates as a closed platform that AAO translates on its behalf. Open-ecosystem path (publisher-hosted). Used when the publisher owning the URI subdomain participates directly in AdCP — independent publishers, SSPs, retail-media networks running their own canonical extensions. The publisher hosts the artifact at the canonical URI on their subdomain. Because URIs are digest-pinned (uri@sha256:…), responses are immutable per digest — publishers SHOULD serve them with Cache-Control: public, max-age=31536000, immutable and target ≥99.9% / 30-day availability. SDKs cache aggressively by uri@digest; a hit is always correct. On 404 or resolution failure, buyers MUST degrade gracefully (treat as unavailable, skip platform-specific narrowing, don’t fail the buy). Closed-platform path (AAO-translated). Used for walled gardens (Meta, Google, Amazon, TikTok, Snap, Pinterest). These platforms are unlikely to host AdCP-shaped extension artifacts on their own subdomains (they have native SDKs and APIs that protect their revenue model; serving an immutable extension CDN gives them no benefit). Instead, AAO runs a translator that maps closed-platform format documentation into AdCP extension artifacts and hosts them under an AAO mirror namespace (e.g., https://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
Buyer’s SDK caches by URI@digest. Subsequent get_products responses can reference by digest alone if the buyer has the extension cached. Direct URI fetch is supported for tooling but the primary path is bundled-in-get_products.

Deprecated named-format compatibility projection (normative)

AdCP 3.2 products are authored with canonical format_options. A producer MAY derive deprecated format_ids from those declarations only for a negotiated older-3.x compatibility response. When both shapes ship to such a peer, they MUST refer to the same underlying format declaration — divergent shapes are a contract violation. New products and examples MUST NOT be authored from format_ids, and AdCP 4.0 removes every field named exactly format_ids.

Producer rules

  • Canonical format_options are the source of truth. Compatibility format_ids MUST be derived from them rather than maintained as a second authored catalog.
  • A producer that cannot guarantee agreement MUST omit the legacy compatibility projection.
  • For format_kind: "custom" declarations, producers MUST set canonical_formats_only: true and MUST NOT synthesize a v1 format_id. The protocol does NOT mint synthetic format_ids (an aao-synth/* namespace was considered and rejected — adopters would index on identifiers with no stable identity).
  • For format_options declarations whose canonical/parameter shape has no clean v1 named-format equivalent (e.g., a structural shape not in v1-canonical-mapping.json and not declared on any v1 file), producers SHOULD set canonical_formats_only: true rather than emit only one of the two shapes silently.

Consumer rules (legacy→canonical)

When accepting a product from a negotiated older-3.x peer, SDKs immediately project format_ids to canonical format_options using the resolution order from v1-canonical-mapping.json:
  1. Authoritative v2 → v1 link: if any v2 ProductFormatDeclaration on the same product carries v1_format_ref pointing at this v1 format_id, use that v2 declaration directly. Highest priority — seller asserts the link.
  2. Seller-asserted on the v1 file: explicit canonical field on the v1 format declaration.
  3. Registry glob: format_id_glob match.
  4. Structural match: registry structural-shape match.
  5. Fail closed: SDK MUST NOT synthesize a format_options entry. SDKs MUST augment the response’s errors[] array with an entry carrying source: "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 v2 format_options projection is absent.

Consumer rules (divergence detection)

When an older 3.x product carries BOTH deprecated format_ids and canonical 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 via errors[] augmentation with source: "sdk", sdk_id, and code: FORMAT_DECLARATION_DIVERGENT. Single mandated surface — lint-output channels are not acceptable. Hard-failing the entire get_products response 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.
The schema cannot enforce agreement (no cross-field constraint expresses “the v1 mapped form of 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 v2 format_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 narrows a v1 range when the v2 value is contained within the v1 range. v2.width: 300 narrows v1.width_range: [200, 400]; v2.duration_ms_exact: 30000 narrows v1.min_duration_ms: 3000.
  • Enum constraints: v2.enum_value is a narrowing if it appears in v1.allowed_values (or v1.allowed_values is absent — open enum). v2.image_formats: ["jpg", "png"] narrows v1.image_formats: ["jpg", "png", "gif", "webp"].
  • Pixel-density constraints: pixel_ratios is an acceptance set. A v2 set narrows a v1 set when every v2 ratio appears in the v1 set. A parameterized legacy format_id.pixel_ratio projects to a single-element v2 set. An omitted legacy ratio is 1x.
  • 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] narrows v1.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_formats constraint, v2 declares image_formats: ["jpg"]), v2 is narrowing against 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.
The narrows relation is one-directional: v2 narrows v1 (v2 is the stricter shape). The reverse (v1 narrows v2) is NOT how the dual-emission contract is checked. SDK authors implementing the narrowing check SHOULD apply parameter-by-parameter subsumption per the rules above. Edge cases (composite parameters, platform_extensions, slot vocabulary changes) may be treated as “unknown — pass” only on the explicit compatibility path and must surface a structured warning. SDK conformance for the specified scalar, enum/set, pixel-density, range, inheritance, open-baseline, and conflict cases MUST follow narrowing_vectors in static/test-vectors/v1-canonical-mapping.json.

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):
Override case (generative entries, brief-driven host-reads, anything whose v1 asset shape isn’t the canonical’s default):
Projection rules (per canonical-projection-ref.json):
  1. kindformat_kind on the projected v2 ProductFormatDeclaration.
  2. asset_source (if set) → params.asset_source. If absent, projection uses the canonical’s default (typically buyer_uploaded).
  3. slots_override (if set) REPLACES the canonical’s default slots[] on the projected declaration. If absent, projection inherits the canonical’s defaults.
  4. v1 entry’s requirements (dimensions, durations, codecs) → params fields per the canonical’s parameter schema.
  5. v1 entry’s assets[*] MUST be consistent with the resulting slots[]asset_idasset_group_id (asset-group-vocabulary resolves aliases).
Generative formats in the catalog. The 8 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 with params.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 emit FORMAT_DECLARATION_V1_LOSSY_MULTI_SIZE on the response errors[] with error.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 corresponding v1_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 under format_ids[] alongside the seller-asserted refs. SDKs that fan out MUST still emit FORMAT_DECLARATION_V1_LOSSY_MULTI_SIZE as a transparency advisory so downstream consumers know which format_ids[] entries were seller-asserted vs catalog-resolved. The advisory’s error.details SHOULD include synthesized_refs: [<list of catalog-resolved ids>].
Why MAY-do, not MUST-do or MUST-NOT-do. SDKs without catalog access can’t fan out; making it MUST creates an SDK-feature dependency. Making it MUST-NOT discards real value (a catalog with all three IAB sizes can losslessly expand a multi-size declaration). MAY-do with mandatory advisory preserves SDK choice while keeping the wire shape transparent — downstream consumers can always distinguish synthesized refs from seller-asserted ones via the advisory’s 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.

Pixel-density projection (normative)

Unversioned legacy reference-catalog bridge. For each of the seven fixed image sizes (300x250, 728x90, 320x50, 160x600, 336x280, 300x600, and 970x250), the reference catalog publishes three distinct legacy contracts: the existing display_<width>x<height>_image requires a 1x asset only; display_<width>x<height>_image_2x requires a 2x asset only; and display_<width>x<height>_image_1x_2x requires both 1x and 2x assets. All keep the logical size in renders[].dimensions, while ordinary v1 asset requirements carry the exact intrinsic dimensions. Sellers select the contract their delivery system actually needs; GAM-style device-density selection uses _image_1x_2x, while a renderer that accepts Retina alone may use _image_2x. The live legacy catalog is identified by (agent_url, id) and list_creative_formats exposes no catalog version or digest, so these additive IDs become discoverable across 3.x when the reference agent deploys; they do not require a 3.0.x or 3.1.x protocol backport. This remains legacy-format support only: canonical pixel-density and rendition-set semantics begin in AdCP 3.2, and permissive JSON acceptance does not give unknown properties protocol meaning. Density and coverage values under canonical_parameters are opaque forward-projection metadata that 3.2-aware SDKs may use when projecting the legacy ID into the 3.2 canonical model. For a mapped _image_1x_2x creative, the two v1 image assets intentionally share asset_group_id: image_main. A 3.2-aware SDK MUST collect them into the canonical image_main rendition array and validate their distinct densities before applying generic duplicate-asset-group collision handling. It MUST NOT drop the second rendition as an alias collision. AdCP 3.2 authoring path. Pixel density uses the parameterized display_image legacy template; new publishers SHOULD NOT mint image_retina or one hard-coded legacy format definition per size/density combination. A v1 placement advertises each accepted density as a parameterized reference:
SDKs project width and height as canonical logical dimensions and project a non-default pixel_ratio to pixel_ratios: [pixel_ratio]. Omitted pixel_ratio is 1x and MAY remain omitted canonically. A canonical declaration accepting multiple densities SHOULD carry one v1_format_ref per density, all pointing to the same display_image template with different parameter values. This is parameter fan-out, not format-definition proliferation. The concrete 3.1 _image_2x and _image_1x_2x aliases remain valid compatibility identifiers but SHOULD NOT be extended to new sizes after the parameterized form ships.
test=false
This replaces the earlier 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.

Canonical-only declarations on a compatibility response

A buyer reading a negotiated legacy compatibility response sees format_options entries with canonical_formats_only: true absent from format_ids. This is intentional and not a producer error. Canonical buyers reading format_options see them. Older buyers see fewer options through the deprecated projection because some canonical declarations cannot be represented safely as named formats. A declaration carrying locale_policy is always in this category. Sellers MUST NOT synthesize a v1 ID for it: the legacy shape would preserve dimensions and assets while silently dropping the hard locale constraint.

Canonical producers serving older peers

Canonical producers MUST NOT mint new named-format IDs to extend the deprecated surface. If an older peer requires compatibility, project only declarations that already have an authoritative v1_format_ref or resolve unambiguously through the maintained mapping registry. Mark declarations without a safe projection canonical_formats_only: true and omit them from format_ids. If that leaves the older peer unable to buy the product, return an unsupported-version or unsupported-feature outcome rather than inventing identity.

What’s NOT in v2

By design, v2 doesn’t introduce new vocabulary for things AdCP already handles or that belong elsewhere:
  • Brand safety vocabularies — that’s media-buy/campaign-level (creative-policy.json and broader campaign settings), not creative-format-level. Format declarations don’t redeclare brand safety.
  • Universal macros as a new schema — already documented at /docs/creative/universal-macros. Canonical formats reference them by name.
  • destination_kinds as a new schemaurl-asset.json already has url_type covering URL kind disambiguation. Platform-specific destinations (Meta messenger_thread, etc.) are platform extensions.
  • cta_vocabulary as a canonical pattern — CTAs vary meaningfully across surfaces; we let products declare cta_values arrays inline until cross-platform demand emerges.
  • list_build_capabilities as a separate tool — folded into get_adcp_capabilities under creative.supported_formats.
  • Separate build-time format option fields and an inputs map — collapsed into the canonical slots model on the format declaration. The format declares slots (canonical asset_group_id + asset_type + constraints); the manifest has a single assets map keyed by slot name; the seller dispatches per the format (render assets verbatim or consume them for production). The format itself tells the buyer what it requires; how production happens is implementation detail.

Channels 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’s asset_source, slots_override, or applies_to_channels axis handles the difference.
  • Linear / addressable TVvideo_hosted + applies_to_channels: ["linear_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 / DOOHimage (or video_hosted) + applies_to_channels: ["dooh"]. Asset is still a still image (or short video) with size constraints. Location-keyed measurement (Geopath, COMMB) belongs on sync_event_sources / event_log, not on the format.
  • Generative-from-prompt — same format_kind as the buyer-uploaded equivalent (image, video_hosted, audio_hosted) + asset_source: agent_synthesized + slots_override declaring 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.”
  • In-feed videovideo_hosted, narrowed to the product’s actual channel such as social when applicable. The asset is still a hosted video file; placement metadata captures its in-feed position. For title + image + body assembled by the renderer, use the dedicated native_in_feed canonical.
Channels that DO need their own canonical (genuinely different shape, deferred):
  • Audio dynamic ad insertion (DAI) — ad-stitched audio with mid-stream insertion has a different tracking shape than audio_hosted or audio_daast. Likely a specialized canonical or audio_daast extension parameters when the pattern stabilizes.
  • In-game — playable / in-game ads have a SDK-specific composition model. Out of scope until cross-engine standards land.
  • Live streaming — live linear video (Twitch / YouTube Live / sports streaming with mid-roll) needs concurrent-impression and stream-state tracking. The video_vast canonical handles VAST-tag-driven live insertion today; richer live patterns deferred.
Rule of thumb (the “sibling refinement before canonical multiplication” principle). Before reaching for a new canonical, check whether the difference is in (a) production model — covered by asset_source, (b) slot shape — covered by the canonical’s slots, (c) channel — covered by applies_to_channels, or (d) measurement / tracking — covered by sync_event_sources / event_log. Add a canonical only when the creative asset or renderer assembly is structurally different. That test is why broadcast, DOOH, and generative production refine existing canonicals while assembled in-feed native uses native_in_feed.

Generative-DSP and multi-output patterns are forward-looking

The asset_source enums (including seller_pre_rendered_from_brief and agent_synthesized) and item_production_model on sponsored_placement are designed for generative-DSP and AI-rendered retail-media patterns that are emerging but not yet a large share of programmatic spend in 2026. Universalads-shaped tools, Pencil, AdCreative.ai, GenStudio-shaped tools — these are real adopters, but the volume is small relative to the boring 90% (buyer ships an MREC PNG; surface serves it). Reading too much into the schema breadth is a mistake. The fields exist so generative-DSP adopters have a clean v2 home; the worked examples include them so adopters can map their adapter cleanly. They are not a signal that the v2 narrative is AI-first. The dominant flows for 3.1 are still buyer-uploaded assets going through deterministic surfaces.

Creative-agent business model

The third-party-creative-agent worked example assumes Flashtalking-shaped tools serve buyers via build_creative and let the buyer ship the produced manifest to the seller. Operators reading this should not infer that v2 strips creative agents of their hosting / serving / tracking revenue. Production happens at build_creative; the produced manifest can include hosted asset URLs on the creative agent’s CDN (Flashtalking-hosted asset URLs in the example), and platform extensions can attach creative-agent-specific tracking (Flashtalking pixel IDs, viewability vendor configurations) that the seller honors at serve time. The v2 disaggregation is conceptual (the spec separates production from serving from tracking) — the operational integration path lets creative agents continue to host and instrument their produced creatives. v2 doesn’t dictate where the asset bytes live or whose tracking JS runs; it only formalizes the production-vs-serving boundary that already exists implicitly.

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 omits format_shape or format_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 ProductFormatDeclaration parsed 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.
This is a doc concern, not a schema concern. The schema is more strict than the codegenned types; runtime validation closes the gap.

Migration

Legacy named formats are deprecated in 3.2. Implementations MAY provide a server-side compatibility projection derived from canonical declarations for older 3.x peers. They MUST NOT author new products, manifests, capabilities, or examples from the legacy shape, and list_creative_formats MUST NOT be treated as a source of truth. Exact plural format_ids fields are removed in 4.0; other named-format compatibility surfaces follow their own schedules.

Realistic 3.1 coverage

v1-canonical-mapping.json remains the compatibility bridge for reading deployed named-format catalogs. Coverage gaps are migration diagnostics, not a reason to continue authoring legacy formats. A 3.2 producer authors canonical declarations first; optional legacy output is mechanically projected from that source.

Phase status

Empirical projection coverage

The AAO catalog at creative.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 on asset_source + slots_override; no image_generative canonical needed.
  • 3 broadcast (broadcast_spot_15s/30s/60s) — { kind: "video_hosted" }. Sellers narrow via applies_to_channels: ["linear_tv"] on the canonical product. Same asset shape (video file + duration + codec).
  • 4 DOOH (dooh_billboard_*, dooh_transit_screen) — { kind: "image" }. Sellers narrow via applies_to_channels: ["dooh"]. Location-keyed measurement differences live on sync_event_sources, not on the format.
  • 2 native (native_standard, native_content) — { kind: "native_in_feed" }, whose canonical slots cover title, body, icon, main image, sponsor, CTA, landing URL, and trackers.
The SDK side’s v1→v2 projection (validated in adcp-client #1815) projects cleanly across all 50. Same architecture symmetry on v2→v1: clean projection plus honest fail-closed via v1_translatable: false / canonical_formats_only: true for canonicals the spec is explicit about not having v1 forms. The canonical-promotion pattern (normative for future contributions). Refine an existing canonical via asset_source, its declared slots, and applies_to_channels when the asset shape is unchanged; route measurement differences to sync_event_sources / event_log. Promote a separate canonical when renderer assembly is structurally different and broadly shared, as with native_in_feed. A seller that does not support generative production simply omits the generative input slots or asset_source refinement.