diff --git a/.changeset/fuzzy-badgers-report.md b/.changeset/fuzzy-badgers-report.md new file mode 100644 index 000000000..320f05c60 --- /dev/null +++ b/.changeset/fuzzy-badgers-report.md @@ -0,0 +1,8 @@ +--- +'@adcp/sdk': patch +--- + +Adopt the signed AdCP 3.2.0-beta.6 schema and compliance bundle, including +delivery metric leaf identities, sortable transactional metrics, +`time_based_views`, qualifier-aware vendor metric rows, requested metric +narrowing, and negotiated delivery breakdown sorting and completeness echoes. diff --git a/ADCP_VERSION b/ADCP_VERSION index f579aa970..0a86cfcc4 100644 --- a/ADCP_VERSION +++ b/ADCP_VERSION @@ -1 +1 @@ -3.2.0-beta.5 +3.2.0-beta.6 diff --git a/docs/TYPE-SUMMARY.md b/docs/TYPE-SUMMARY.md index 6161d9397..04a7f1614 100644 --- a/docs/TYPE-SUMMARY.md +++ b/docs/TYPE-SUMMARY.md @@ -1,6 +1,6 @@ # AdCP Type Summary -> Generated at: 2026-08-23 +> Generated at: 2026-08-24 > @adcp/sdk v14.0.0-beta.7 Curated reference of the types that matter for using the AdCP client. For full generated types see `src/lib/types/tools.generated.ts` and `src/lib/types/core.generated.ts`. @@ -202,7 +202,7 @@ Each tool is called as `agent.(params)` and returns `TaskResult Generated at: 2026-08-23 +> Generated at: 2026-08-24 > Library: @adcp/sdk v14.0.0-beta.7 > AdCP major version: 3 > Canonical URL: https://adcontextprotocol.github.io/adcp-client/llms.txt @@ -267,10 +267,10 @@ Every tool is an MCP tool called via `agent.(params)`. Returns `Task Request parameters for cross-protocol capability discovery. **Request:** -- Optional: `protocols: string[]`, `context: Context` +- Optional: `protocols: ('media_buy' | 'signals' | 'governance' | 'sponsored_intelligence' | 'creative')[]`, `context: Context` **Response (success branch):** -- Required: `adcp: object`, `supported_protocols: string[]` +- Required: `adcp: object`, `supported_protocols: ('media_buy' | 'signals' | 'governance' | 'sponsored_intelligence' | 'creative' | 'brand' | 'measurement')[]` - Optional: `account: object`, `media_buy: object`, `signals: object`, `governance: object`, `sponsored_intelligence: object`, `brand: object`, `creative: object`, `oauth: object`, +14 more #### `get_task_status` @@ -306,7 +306,7 @@ Register, replace, pause, or clear agent-level webhook subscribers such as capab **Response (success branch):** - Required: `action: 'updated' | 'unchanged' | 'cleared' | 'failed'` -- Optional: `dry_run: boolean`, `notification_configs: object[]`, `errors: object[]`, `context: Context` +- Optional: `dry_run: boolean`, `notification_configs: Agent Notification Config[]`, `errors: Error[]`, `context: Context` ### Account Management @@ -318,15 +318,15 @@ Request parameters for listing accounts accessible to the authenticated agent. - Optional: `account: Account Ref`, `status: 'active' | 'pending_approval' | 'rejected' | 'payment_required' | 'suspended' | 'closed'`, `pagination: Pagination Request`, `sandbox: boolean`, `include_webhook_activity: boolean`, `webhook_activity_limit: integer`, `context: Context` **Response (success branch):** -- Required: `accounts: object[]` -- Optional: `errors: object[]`, `pagination: Pagination Response`, `context: Context` +- Required: `accounts: Account With Authorization[]` +- Optional: `errors: Error[]`, `pagination: Pagination Response`, `context: Context` #### `sync_accounts` Request parameters for syncing advertiser accounts with a seller. **Request:** -- Required: `idempotency_key: string`, `accounts: object[]` +- Required: `idempotency_key: string`, `accounts: (ProvisioningMode | SettingsUpdateMode)[]` - Optional: `delete_missing: boolean`, `dry_run: boolean`, `push_notification_config: Push Notification Config`, `context: Context` **Response (success branch):** @@ -355,7 +355,7 @@ Request parameters for reporting vendor service consumption after delivery. **Response (success branch):** - Required: `accepted: integer` -- Optional: `errors: object[]`, `sandbox: boolean`, `context: Context` +- Optional: `errors: Error[]`, `sandbox: boolean`, `context: Context` #### `get_account_financials` @@ -380,10 +380,10 @@ AdCP 3. **Request:** - Required: `buying_mode: 'brief' | 'wholesale' | 'refine'` -- Optional: `idempotency_key: string`, `brief: string`, `refine: object[]`, `brand: Brand Ref`, `catalog: Catalog`, `account: Account Ref`, `preferred_delivery_types: object[]`, `filters: Product Filters`, +11 more +- Optional: `idempotency_key: string`, `brief: string`, `refine: object[]`, `brand: Brand Ref`, `catalog: Catalog`, `account: Account Ref`, `preferred_delivery_types: Delivery Type[]`, `filters: Product Filters`, +11 more **Response (success branch):** -- Optional: `products: object[]`, `targeting_resolution: Get Products Targeting Resolution`, `extensions: object`, `proposals: object[]`, `errors: object[]`, `reason: string`, `suggestions: string[]`, `property_list_applied: boolean`, +11 more +- Optional: `products: Product[]`, `targeting_resolution: Get Products Targeting Resolution`, `extensions: object`, `proposals: Proposal[]`, `errors: Error[]`, `reason: string`, `suggestions: string[]`, `property_list_applied: boolean`, +11 more **Watch out:** - `cache_scope` is required whenever the response includes `products` or `unchanged: true`. Use `public` for the universal rate card and `account` for account-specific rate cards or pricing overlays. @@ -416,7 +416,7 @@ Request parameters for creating actionable seller proposals. Request parameters for creating one or more proposal revisions. **Request:** -- Required: `idempotency_key: string`, `refinements: object[]` +- Required: `idempotency_key: string`, `refinements: Proposal Refinement[]` - Optional: `adcp_version: Adcp_version`, `context_id: string`, `context`, `governance_context: string`, `push_notification_config` **Response (success branch):** @@ -427,7 +427,7 @@ Request parameters for creating one or more proposal revisions. Request parameters for terminally declining one or more proposals. **Request:** -- Required: `idempotency_key: string`, `declines: object[]` +- Required: `idempotency_key: string`, `declines: Proposal Decline[]` - Optional: `adcp_version: Adcp_version`, `context_id: string`, `context`, `governance_context: string`, `push_notification_config`, `opportunity: Opportunity Context` #### `buy_products` @@ -435,7 +435,7 @@ Request parameters for terminally declining one or more proposals. Create a MediaBuy directly from canonical published product offers. **Request:** -- Required: `idempotency_key: string`, `account`, `feed_version: string`, `purchases: object[]`, `start_time: Start Timing`, `end_time: string` +- Required: `idempotency_key: string`, `account`, `feed_version: string`, `purchases: Product Purchase[]`, `start_time: Start Timing`, `end_time: string` - Optional: `adcp_version: Adcp_version`, `brand: Brand Key`, `advertiser_industry: Advertiser Industry`, `pricing_version: string`, `total_budget: object`, `daily_budget_cap: number`, `budget_cap_timezone: string`, `budget_allocation: Canonical Budget Allocation`, +11 more #### `accept_proposal` @@ -456,18 +456,18 @@ Apply operational controls inside accepted commercial terms. **Response (success branch):** - Required: `status: 'completed'`, `media_buy_id: string`, `revision: integer` -- Optional: `media_buy_status: Media Buy Status`, `implementation_date: string,null`, `affected_package_ids: string[]`, `available_actions: object[]`, `warnings: object[]`, `context: Context`, `replayed: 'true'` +- Optional: `media_buy_status: Media Buy Status`, `implementation_date: string,null`, `affected_package_ids: string[]`, `available_actions: Canonical Media Buy Action[]`, `warnings: object[]`, `context: Context`, `replayed: 'true'` #### `list_creative_formats` Deprecated 3. **Request:** -- Optional: `format_ids: object[]`, `asset_types: object[]`, `max_width: integer`, `max_height: integer`, `min_width: integer`, `min_height: integer`, `is_responsive: boolean`, `name_search: string`, +9 more +- Optional: `format_ids: Format Id[]`, `asset_types: Asset Content Type[]`, `max_width: integer`, `max_height: integer`, `min_width: integer`, `min_height: integer`, `is_responsive: boolean`, `name_search: string`, +9 more **Response (success branch):** -- Required: `formats: object[]` -- Optional: `source: 'publisher' | 'aao_mirror' | 'agent_derived'`, `creative_agents: object[]`, `errors: object[]`, `pagination: Pagination Response`, `sandbox: boolean`, `context: Context` +- Required: `formats: Format[]` +- Optional: `source: 'publisher' | 'aao_mirror' | 'agent_derived'`, `creative_agents: object[]`, `errors: Error[]`, `pagination: Pagination Response`, `sandbox: boolean`, `context: Context` **Watch out:** - Each `renders[]` entry satisfies a `oneOf` — exactly one of `dimensions` (object) OR `parameters_from_format_id: true`. A render with only `{ role }` (or `{ role, duration_seconds }`) fails validation. @@ -483,7 +483,7 @@ AdCP 3. - Optional: `governance_context: string`, `plan_id: string`, `proposal_id: string`, `opportunity`, `total_budget: object`, `daily_budget_cap: number`, `budget_cap_timezone: string`, `budget_allocation`, +14 more **Response (success branch):** -- Required: `media_buy_id: string`, `confirmed_at: string,null`, `revision: integer`, `packages: object[]` +- Required: `media_buy_id: string`, `confirmed_at: string,null`, `revision: integer`, `packages: Package[]` - Optional: `proposal_id: string`, `name: string`, `account: Account`, `invoice_recipient: Business Entity`, `media_buy_status: Media Buy Status`, `creative_deadline: string`, `currency: string`, `total_budget: number`, +11 more **Watch out:** @@ -509,22 +509,22 @@ AdCP 3. Request parameters for retrieving media buy status, creative approvals, and delivery snapshots. **Request:** -- Optional: `account: Account Ref`, `media_buy_ids: string[]`, `status_filter: Media Buy Status | object[]`, `indicator_types: object[]`, `include_snapshot: boolean`, `include_history: integer`, `include_webhook_activity: boolean`, `webhook_activity_limit: integer`, +2 more +- Optional: `account: Account Ref`, `media_buy_ids: string[]`, `status_filter: Media Buy Status | Media Buy Status[]`, `indicator_types: Indicator Type[]`, `include_snapshot: boolean`, `include_history: integer`, `include_webhook_activity: boolean`, `webhook_activity_limit: integer`, +2 more **Response (success branch):** - Required: `media_buys: object[]` -- Optional: `errors: object[]`, `pagination: Pagination Response`, `sandbox: boolean`, `context: Context` +- Optional: `errors: Error[]`, `pagination: Pagination Response`, `sandbox: boolean`, `context: Context` #### `get_media_buy_delivery` Request parameters for retrieving comprehensive delivery metrics. **Request:** -- Optional: `account: Account Ref`, `media_buy_ids: string[]`, `status_filter: Media Buy Status | object[]`, `start_date: string`, `end_date: string`, `include_package_daily_breakdown: boolean`, `time_granularity: Reporting Frequency`, `include_window_breakdown: boolean`, +3 more +- Optional: `account: Account Ref`, `media_buy_ids: string[]`, `status_filter: Media Buy Status | Media Buy Status[]`, `start_date: string`, `end_date: string`, `include_package_daily_breakdown: boolean`, `requested_metrics: Available Metric[]`, `time_granularity: Reporting Frequency`, +4 more **Response (success branch):** - Required: `reporting_period: object`, `currency: string`, `media_buy_deliveries: object[]` -- Optional: `notification_type: 'scheduled' | 'final' | 'delayed' | 'adjusted' | 'window_update'`, `partial_data: boolean`, `unavailable_count: integer`, `sequence_number: integer`, `next_expected_at: string`, `attribution_window: Attribution Window`, `aggregated_totals: object`, `errors: object[]`, +2 more +- Optional: `notification_type: 'scheduled' | 'final' | 'delayed' | 'adjusted' | 'window_update'`, `partial_data: boolean`, `unavailable_count: integer`, `sequence_number: integer`, `next_expected_at: string`, `attribution_window: Attribution Window`, `aggregated_totals: object`, `errors: Error[]`, +2 more #### `provide_performance_feedback` @@ -555,7 +555,7 @@ Request parameters for configuring event sources on an account. Request parameters for logging conversion or marketing events. **Request:** -- Required: `event_source_id: string`, `events: object[]`, `idempotency_key: string` +- Required: `event_source_id: string`, `events: Event[]`, `idempotency_key: string` - Optional: `test_event_code: string`, `context: Context` **Response (success branch):** @@ -580,11 +580,11 @@ Request parameters for syncing catalog feeds (products, inventory, stores, promo **Request:** - Required: `idempotency_key: string`, `account: Account Ref` -- Optional: `catalogs: object[]`, `item_availability_updates: object[]`, `item_availability_queries: object[]`, `catalog_ids: string[]`, `delete_missing: boolean`, `dry_run: boolean`, `validation_mode: Validation Mode`, `push_notification_config: Push Notification Config`, +1 more +- Optional: `catalogs: Catalog[]`, `item_availability_updates: Catalog Item Availability Update[]`, `item_availability_queries: Catalog Item Availability Ref[]`, `catalog_ids: string[]`, `delete_missing: boolean`, `dry_run: boolean`, `validation_mode: Validation Mode`, `push_notification_config: Push Notification Config`, +1 more **Response (success branch):** - Required: `catalogs: object[]` -- Optional: `status: 'completed'`, `dry_run: boolean`, `item_availability_updates: object[]`, `item_availability_states: object[]`, `sandbox: boolean`, `context: Context` +- Optional: `status: 'completed'`, `dry_run: boolean`, `item_availability_updates: Catalog Item Availability Update Result[]`, `item_availability_states: Catalog Item Availability State[]`, `sandbox: boolean`, `context: Context` **Deep dive:** - docs/getting-started.md — installation, auth, basic usage @@ -631,11 +631,11 @@ Request parameters for generating creative previews. Deprecated 3. **Request:** -- Optional: `format_ids: object[]`, `type: 'audio' | 'video' | 'display' | 'dooh'`, `asset_types: object[]`, `max_width: integer`, `max_height: integer`, `min_width: integer`, `min_height: integer`, `is_responsive: boolean`, +10 more +- Optional: `format_ids: Format Id[]`, `type: 'audio' | 'video' | 'display' | 'dooh'`, `asset_types: Asset Content Type[]`, `max_width: integer`, `max_height: integer`, `min_width: integer`, `min_height: integer`, `is_responsive: boolean`, +10 more **Response (success branch):** -- Required: `formats: object[]` -- Optional: `creative_agents: object[]`, `errors: object[]`, `pagination: Pagination Response`, `context: Context` +- Required: `formats: Format[]` +- Optional: `creative_agents: object[]`, `errors: Error[]`, `pagination: Pagination Response`, `context: Context` **Watch out:** - Each `renders[]` entry satisfies a `oneOf` — exactly one of `dimensions` (object) OR `parameters_from_format_id: true`. A render with only `{ role }` (or `{ role, duration_seconds }`) fails validation. @@ -647,11 +647,11 @@ Deprecated 3. Request parameters for discovering account-scoped creative transformers (the creative analog of products), with optional brief filtering, per-param option expansion, and pricing. **Request:** -- Optional: `transformer_ids: string[]`, `input_format_ids: object[]`, `output_format_ids: object[]`, `input_format_kinds: object[]`, `output_capability_ids: string[]`, `name_search: string`, `brief: string`, `expand_params: string[]`, +5 more +- Optional: `transformer_ids: string[]`, `input_format_ids: Format Id[]`, `output_format_ids: Format Id[]`, `input_format_kinds: Canonical Format Kind[]`, `output_capability_ids: string[]`, `name_search: string`, `brief: string`, `expand_params: string[]`, +5 more **Response (success branch):** -- Required: `transformers: object[]` -- Optional: `errors: object[]`, `pagination: Pagination Response`, `context: Context` +- Required: `transformers: Transformer[]` +- Optional: `errors: Error[]`, `pagination: Pagination Response`, `context: Context` #### `get_creative_delivery` @@ -662,7 +662,7 @@ Request parameters for retrieving creative delivery data with variant-level brea **Response (success branch):** - Required: `currency: string`, `reporting_period: object`, `creatives: object[]` -- Optional: `account_id: string`, `media_buy_id: string`, `pagination: object`, `errors: object[]`, `context: Context` +- Optional: `account_id: string`, `media_buy_id: string`, `pagination: object`, `errors: Error[]`, `context: Context` #### `list_creatives` @@ -672,8 +672,8 @@ Request parameters for querying creative library with filtering and pagination. - Optional: `filters: Creative Filters`, `sort: object`, `pagination: Pagination Request`, `include_assignments: boolean`, `assignment_projection: 'all' | 'matching'`, `assignment_limit: integer`, `include_snapshot: boolean`, `include_items: boolean`, +8 more **Response (success branch):** -- Required: `query_summary: object`, `pagination: Pagination Response`, `creatives: object[]` -- Optional: `format_summary: object`, `status_summary: object`, `errors: object[]`, `sandbox: boolean`, `context: Context` +- Required: `query_summary: object`, `pagination: Pagination Response`, `creatives: (Listed creative (named-format reference) | Listed creative (canonical format kind))[]` +- Optional: `format_summary: object`, `status_summary: object`, `errors: Error[]`, `sandbox: boolean`, `context: Context` #### `sync_creatives` @@ -681,7 +681,7 @@ Request parameters for syncing creative assets with upsert semantics. **Request:** - Required: `account: Account Ref`, `idempotency_key: string` -- Optional: `creatives: object[]`, `creative_ids: string[]`, `assignments: object[]`, `assignment_operations: object[]`, `delete_missing: boolean`, `dry_run: boolean`, `validation_mode: Validation Mode`, `push_notification_config: Push Notification Config`, +1 more +- Optional: `creatives: object[]`, `creative_ids: string[]`, `assignments: object[]`, `assignment_operations: (Assign or update | Unassign | Replace assignment)[]`, `delete_missing: boolean`, `dry_run: boolean`, `validation_mode: Validation Mode`, `push_notification_config: Push Notification Config`, +1 more **Response (success branch):** - Required: `creatives: object[]` @@ -693,10 +693,10 @@ Request parameters for validating a creative manifest against canonical formats **Request:** - Required: `manifest: Creative Manifest` -- Optional: `account: Account Ref`, `brand: Brand Ref`, `targets: object[]` +- Optional: `account: Account Ref`, `brand: Brand Ref`, `targets: union[]` **Response (success branch):** -- Required: `results: object[]` +- Required: `results: Validate Input Result[]` **Deep dive:** - docs/guides/BUILD-AN-AGENT.md — building a creative agent (server-side) @@ -709,21 +709,21 @@ Request parameters for validating a creative manifest against canonical formats Request parameters for discovering signals based on description. **Request:** -- Optional: `discovery_mode: 'brief' | 'wholesale'`, `account: Account Ref`, `signal_spec: string`, `signal_refs: object[]`, `signal_ids: object[]`, `destinations: object[]`, `countries: string[]`, `filters: Signal Filters`, +7 more +- Optional: `discovery_mode: 'brief' | 'wholesale'`, `account: Account Ref`, `signal_spec: string`, `signal_refs: Signal Ref[]`, `signal_ids: Signal Id[]`, `destinations: Destination[]`, `countries: string[]`, `filters: Signal Filters`, +7 more **Response (success branch):** -- Optional: `signals: object[]`, `errors: object[]`, `incomplete: object[]`, `wholesale_feed_version: string`, `pricing_version: string`, `cache_scope: 'public' | 'account'`, `unchanged: 'true'`, `pagination: Pagination Response`, +2 more +- Optional: `signals: object[]`, `errors: Error[]`, `incomplete: object[]`, `wholesale_feed_version: string`, `pricing_version: string`, `cache_scope: 'public' | 'account'`, `unchanged: 'true'`, `pagination: Pagination Response`, +2 more #### `activate_signal` Request parameters for activating a signal on a specific platform/account. **Request:** -- Required: `signal_agent_segment_id: string`, `destinations: object[]`, `idempotency_key: string` +- Required: `signal_agent_segment_id: string`, `destinations: Destination[]`, `idempotency_key: string` - Optional: `action: 'activate' | 'deactivate'`, `pricing_option_id: string`, `governance_context: string`, `account: Account Ref`, `context: Context` **Response (success branch):** -- Required: `deployments: object[]` +- Required: `deployments: Deployment[]` - Optional: `sandbox: boolean`, `context: Context` **Deep dive:** @@ -737,7 +737,7 @@ Request parameters for creating a new property list. **Request:** - Required: `name: string`, `idempotency_key: string` -- Optional: `account: Account Ref`, `description: string`, `base_properties: object[]`, `filters: Property List Filters`, `brand: Brand Ref`, `context: Context` +- Optional: `account: Account Ref`, `description: string`, `base_properties: Base Property Source[]`, `filters: Property List Filters`, `brand: Brand Ref`, `context: Context` **Response (success branch):** - Required: `list: Property List`, `auth_token: string` @@ -749,7 +749,7 @@ Request parameters for updating an existing property list. **Request:** - Required: `list_id: string`, `idempotency_key: string` -- Optional: `account: Account Ref`, `name: string`, `description: string`, `base_properties: object[]`, `filters: Property List Filters`, `brand: Brand Ref`, `webhook_url: string`, `context: Context` +- Optional: `account: Account Ref`, `name: string`, `description: string`, `base_properties: Base Property Source[]`, `filters: Property List Filters`, `brand: Brand Ref`, `webhook_url: string`, `context: Context` **Response (success branch):** - Required: `list: Property List` @@ -765,7 +765,7 @@ Request parameters for retrieving a property list with resolved properties. **Response (success branch):** - Required: `list: Property List` -- Optional: `identifiers: object[]`, `pagination: Pagination Response`, `resolved_at: string`, `cache_valid_until: string`, `coverage_gaps: object`, `context: Context` +- Optional: `identifiers: Identifier[]`, `pagination: Pagination Response`, `resolved_at: string`, `cache_valid_until: string`, `coverage_gaps: object`, `context: Context` #### `list_property_lists` @@ -775,7 +775,7 @@ Request parameters for listing property lists. - Optional: `account: Account Ref`, `name_contains: string`, `pagination: Pagination Request`, `context: Context` **Response (success branch):** -- Required: `lists: object[]` +- Required: `lists: Property List[]` - Optional: `pagination: Pagination Response`, `context: Context` #### `delete_property_list` @@ -796,7 +796,7 @@ Request parameters for creating a new collection list. **Request:** - Required: `name: string`, `idempotency_key: string` -- Optional: `account: Account Ref`, `description: string`, `base_collections: object[]`, `filters: Collection List Filters`, `brand: Brand Ref`, `context: Context` +- Optional: `account: Account Ref`, `description: string`, `base_collections: Base Collection Source[]`, `filters: Collection List Filters`, `brand: Brand Ref`, `context: Context` **Response (success branch):** - Required: `list: Collection List`, `auth_token: string` @@ -808,7 +808,7 @@ Request parameters for updating an existing collection list. **Request:** - Required: `list_id: string`, `idempotency_key: string` -- Optional: `account: Account Ref`, `name: string`, `description: string`, `base_collections: object[]`, `filters: Collection List Filters`, `brand: Brand Ref`, `webhook_url: string`, `context: Context` +- Optional: `account: Account Ref`, `name: string`, `description: string`, `base_collections: Base Collection Source[]`, `filters: Collection List Filters`, `brand: Brand Ref`, `webhook_url: string`, `context: Context` **Response (success branch):** - Required: `list: Collection List` @@ -834,7 +834,7 @@ Request parameters for listing collection lists. - Optional: `account: Account Ref`, `name_contains: string`, `pagination: Pagination Request`, `context: Context` **Response (success branch):** -- Required: `lists: object[]` +- Required: `lists: Collection List[]` - Optional: `pagination: Pagination Response`, `context: Context` #### `delete_collection_list` @@ -854,10 +854,10 @@ Request parameters for deleting a collection list. Request parameters for listing content standards configurations. **Request:** -- Optional: `channels: object[]`, `languages: string[]`, `countries: string[]`, `pagination: Pagination Request`, `context: Context` +- Optional: `channels: Channels[]`, `languages: string[]`, `countries: string[]`, `pagination: Pagination Request`, `context: Context` **Response (success branch):** -- Required: `standards: object[]` +- Required: `standards: Content Standards[]` - Optional: `pagination: Pagination Response`, `context: Context` #### `get_content_standards` @@ -877,7 +877,7 @@ Request parameters for creating a new content standards configuration. **Request:** - Required: `scope: object`, `idempotency_key: string` -- Optional: `registry_policy_ids: string[]`, `policies: object[]`, `calibration_exemplars: object`, `context: Context` +- Optional: `registry_policy_ids: string[]`, `policies: Policy Entry[]`, `calibration_exemplars: object`, `context: Context` **Response (success branch):** - Required: `standards_id: string` @@ -889,7 +889,7 @@ Request parameters for updating an existing content standards configuration. **Request:** - Required: `standards_id: string`, `idempotency_key: string` -- Optional: `scope: object`, `registry_policy_ids: string[]`, `policies: object[]`, `calibration_exemplars: object`, `context: Context` +- Optional: `scope: object`, `registry_policy_ids: string[]`, `policies: Policy Entry[]`, `calibration_exemplars: object`, `context: Context` **Response (success branch):** - Required: `success: 'true'`, `standards_id: string` @@ -940,8 +940,8 @@ Request parameters for evaluating creative features from a governance agent. - Optional: `feature_ids: string[]`, `account: Account Ref`, `context: Context` **Response (success branch):** -- Required: `results: object[]` -- Optional: `detail_url: string`, `audit_observations: object[]`, `pricing_option_id: string`, `vendor_cost: number`, `currency: string`, `consumption: Creative Consumption`, `context: Context` +- Required: `results: Creative Feature Result[]` +- Optional: `detail_url: string`, `audit_observations: Audit Observation[]`, `pricing_option_id: string`, `vendor_cost: number`, `currency: string`, `consumption: Creative Consumption`, `context: Context` #### `sync_plans` @@ -984,7 +984,7 @@ Seller-authenticated append-only commitment adjustment report. Retrieve governance state and audit trail for a plan. **Request:** -- Optional: `plan_ids: string[]`, `portfolio_plan_ids: string[]`, `governance_contexts: string[]`, `purchase_types: object[]`, `include_entries: boolean`, `context: Context` +- Optional: `plan_ids: string[]`, `portfolio_plan_ids: string[]`, `governance_contexts: string[]`, `purchase_types: Purchase Type[]`, `include_entries: boolean`, `context: Context` **Response (success branch):** - Required: `plans: object[]` @@ -1029,7 +1029,7 @@ Host initiates SI session with brand agent - includes context, identity, and cap **Response (success branch):** - Required: `session_id: string`, `session_status: Si Session Status` -- Optional: `response: object`, `negotiated_capabilities: Si Capabilities`, `sponsored_context: Si Sponsored Context`, `session_ttl_seconds: integer`, `errors: object[]`, `context: Context` +- Optional: `response: object`, `negotiated_capabilities: Si Capabilities`, `sponsored_context: Si Sponsored Context`, `session_ttl_seconds: integer`, `errors: Error[]`, `context: Context` #### `si_send_message` @@ -1041,7 +1041,7 @@ Send a message within an active SI session. **Response (success branch):** - Required: `session_id: string`, `session_status: Si Session Status` -- Optional: `response: object`, `mcp_resource_uri: string`, `sponsored_context: Si Sponsored Context`, `handoff: object`, `errors: object[]`, `context: Context` +- Optional: `response: object`, `mcp_resource_uri: string`, `sponsored_context: Si Sponsored Context`, `handoff: object`, `errors: Error[]`, `context: Context` #### `si_terminate_session` @@ -1053,7 +1053,7 @@ Terminate an SI session with reason (handoff_transaction, handoff_complete, user **Response (success branch):** - Required: `session_id: string`, `terminated: boolean` -- Optional: `session_status: Si Session Status`, `acp_handoff: object`, `follow_up: object`, `errors: object[]`, `context: Context` +- Optional: `session_status: Si Session Status`, `acp_handoff: object`, `follow_up: object`, `errors: Error[]`, `context: Context` **Deep dive:** - docs/guides/ASYNC-DEVELOPER-GUIDE.md — session lifecycle patterns @@ -1162,6 +1162,12 @@ Flow: `build_creative → sync_accounts → sync_governance → list_transformer **Canonical format validate_input** — Validates 3.1 canonical-format dry-run semantics: structural pass/fail across canonical slots and unvalidatable_nondeterministic for seeded products. Flow: `validate_input` +**CTV experience profile validate_input** — Validates AdCP 3.2 ctv_ad_experience matrix pairings, duration/interactivity constraint profiles, the menu focus/video pairing, and non-blocking activation-copy warnings via validate_input against seeded CTV products. +Flow: `validate_input` + +**Premium display canonical validation** — Validates AdCP 3.2 seller_rendered_stateful_display supply-mode contracts (components, rendered_canvases, layered_source), single-state reveal shape, policy floors, and coordinated_placements sequence/serving_policy, alongside transition/canvas coverage and shared-slot resolution. +Flow: `validate_input` + ### Campaign Governance **Governance denial and human escalation** — Buyer's governance agent denies a media buy that exceeds spending authority, escalates to a human who approves with conditions. @@ -1688,6 +1694,7 @@ Agents use the `recovery` classification to decide what to do: `transient` → r | `FORMAT_NOT_SUPPORTED` | correctable | A requested creative operation route is not supported by this creative agent. On the canonical 3.2 path, returned when build_creative.target_capability_id(s), preview_creative.target_capability_id, or validate_input targets[] kind capability does not match an advertised creative.supported_formats[].capability_id carrying the requested operation. Also returned when preview renderer inference has zero or multiple compatible matches. Sellers SHOULD attribute the error to the selector field and MAY include supported capability IDs in error.details.supported_capability_ids when safe. Deprecated target_format_id(s) and preview format_id retain legacy named-format error attribution during the 3.x compatibility window. | | `FORMAT_OPTION_UNRESOLVED` | correctable | Non-fatal advisory raised when a placement in `adagents.json` (or any consumer of `placement-definition.json`) carries `format_options[].format_option_id` referencing a `format_option_id` that does NOT exist in the file's top-level `formats[]`. The reference is broken — the publisher's catalog claims the placement accepts a format option that isn't declared. **Resolution scope is same-file only.** Cross-file `format_option_id` lookup is not supported by design (closes off format_option_id squatting across publisher boundaries — a malicious file cannot reference another publisher's format_option_id and claim its narrowing). Buyer SDKs MUST fail closed for the placement (drop the format from the placement's accepted format set) and MUST surface this code rather than silently dropping or guessing what the publisher meant. Surface placement: same single-mandate as the other FORMAT_* codes — SDKs that detect on consumption MUST augment the response's `errors[]` with `source: "sdk"`, `sdk_id`, `code: "FORMAT_OPTION_UNRESOLVED"`, `field` pointing at the offending placement (e.g., `placements[2].format_options[1].format_option_id`), and `error.details` SHOULD carry `{ placement_id, format_option_id, declared_format_options: [] }` so the publisher can fix. | | `FORMAT_PROJECTION_FAILED` | correctable | Non-fatal advisory raised when a legacy named format on a product cannot be projected to a canonical-formats `ProductFormatDeclaration` via the resolution order in `v1-canonical-mapping.json` (explicit `canonical` field → format_id_glob → structural match → fail-closed). The product is still valid on the legacy named-format path; only the 3.1+ `format_options` projection failed. Primarily a **consumer-SDK concern** — the seller didn't fail; the consumer-side SDK couldn't project on their behalf. `error.field` MUST point at the offending product (e.g., `products[3].format_ids[0]`); `error.details` SHOULD carry `{ format_id, product_id, resolution_failure: "no_explicit_canonical" | "no_registry_match" | "no_structural_match" }` so buyer SDKs can route remediation (suggest the seller add an explicit `canonical` field, or file a registry PR). **Surface placement (normative).** SDKs that detect this on consumption MUST augment the response's `errors[]` array with an entry carrying `source: "sdk"`, `sdk_id: "@"`, `code: "FORMAT_PROJECTION_FAILED"`, and the field+details described above. This is the single mandated surface — logger-only is insufficient and a separate lint-output channel is NOT acceptable (AdCP is a multi-hop agent network; warnings need to propagate across hops or each hop has to re-detect locally). Sellers MAY emit this code on their own response when they self-detect a non-projectable format on emit; producer-emitted entries omit `source` (or set `source: "producer"`). The response stays 200/success regardless of who emits; this is non-fatal. **Multi-hop deduplication.** Each hop that detects the same condition SHOULD deduplicate by `(code, field)` rather than re-emit. The existing entry's `sdk_id` identifies which earlier processor saw it first; downstream SDKs SHOULD NOT add a second entry for the same `(code, field)` pair unless they have materially different `error.details` (e.g., a different `resolution_failure` reason from a different registry version). See canonical-formats.mdx 'Dual emission and v2↔v1 projection' for the full rules. | +| `FORMAT_SHAPE_PROMOTED` | correctable | Non-fatal deprecation advisory raised when a 3.2-aware SDK encounters `format_kind: custom` with a `format_shape` that has been promoted to a first-class canonical. SDKs MUST preserve the declaration during its transition window and SHOULD augment the containing response's `errors[]` with `source: sdk`, `sdk_id`, this code, and `details: { format_shape, promoted_to, promotion_release, transition_end }`. Producers that self-detect their own legacy declaration MAY emit the same advisory with `source: producer`. Recovery is seller-side: dual-emit during the published transition window, migrate consumers, then replace the custom declaration with the promoted canonical. | | `GOVERNANCE_DENIED` | correctable | A registered governance agent denied the transaction. Sellers MUST place the denial in the operation's structured rejection arm when one exists (e.g., `acquire_rights` → `AcquireRightsRejected`, or an `approval_webhook` delivery → `CreativeRejected`); otherwise in `errors[]` + `adcp_error`. Buyers MUST dispatch on the response's discriminated `status` first and fall back to `errors[].code` / `adcp_error.code` only when no rejection arm exists for that operation. The buyer may restructure the buy (e.g., reduce budget, split into smaller transactions), escalate to human spending authority, or contact the governance agent for details. Wire placement (full guidance). Governance denial is a structured business outcome, not a system error — the governance call SUCCEEDED and the agent returned a denial verdict. Two cases: 1. Operation or webhook payload defines a structured rejection arm. The arm IS the canonical denial shape. The seller populates `reason` (human-readable, propagating governance findings) and `suggestions` (optional) and does NOT additionally emit `GOVERNANCE_DENIED` in `errors[]` or `adcp_error`. The rejection arms enforce this at the schema layer: e.g., `AcquireRightsRejected` and `CreativeRejected` both declare `not: { required: [errors] }`, so dual-emission is already a schema violation. The code does not appear on the wire when the rejection arm is used. Transport-level success markers MUST NOT be flipped (HTTP 200, MCP `isError: false`, A2A `succeeded`) — the operation completed successfully and produced a structured response. 2. Operation response has no rejection arm (e.g., `create_media_buy` returns Success / Error / Submitted arms only). The seller populates `errors[].code: GOVERNANCE_DENIED` in the payload AND `adcp_error.code: GOVERNANCE_DENIED` on the envelope per the two-layer model in `error-handling.mdx#envelope-vs-payload-errors-the-two-layer-model`. Transport-level failure markers DO flip in this case (HTTP 4xx, MCP `isError: true`, A2A `failed`) — the task could not produce a success artifact. The rule generalizes to any current or future operation or webhook payload whose response defines a discriminated rejection arm. In either placement, sellers SHOULD propagate governance findings verbatim — buyers' recovery decisions depend on what specifically was rejected. `GOVERNANCE_DENIED` is reserved for verdicts received from a reachable governance agent; if the governance call itself failed (timeout, network, config error), use `GOVERNANCE_UNAVAILABLE` instead. | | `GOVERNANCE_UNAVAILABLE` | transient | A registered governance agent is unreachable. Sellers MUST place this code in `errors[]` + `adcp_error` (never a structured rejection arm) and flip transport-level failure markers (HTTP 5xx, MCP `isError: true`, A2A `failed`). Distinct from `GOVERNANCE_DENIED` (agent reachable and explicitly denied — see that code's wire-placement guidance). Wire placement (full guidance). Governance unavailability is a system error — the governance call FAILED (timeout, network, config error) and the seller could not get a verdict at all. Always populate both layers per the two-layer model in `error-handling.mdx#envelope-vs-payload-errors-the-two-layer-model`. Do NOT use a structured rejection arm for unavailability even when the task offers one — the buyer's recovery semantics differ (retry-with-backoff for unavailability vs. restructure-or-escalate for denial), and conflating them masks the system-error signal. | | `IDEMPOTENCY_CONFLICT` | correctable | An earlier request with the same idempotency_key was processed with a different canonical payload within the seller's replay window. Distinct from CONFLICT (concurrent write) — this indicates the client reused a key across semantically different requests. | diff --git a/package.json b/package.json index 3375521fc..c826c7d9f 100644 --- a/package.json +++ b/package.json @@ -485,9 +485,9 @@ "pretest": "npm run schemas:ensure", "test": "npm run test:node && npm test --workspace=packages/eslint-plugin --if-present && npm test --workspace=@adcp/reference-renderers", "test:node": "npm run test:node:fast && npm run test:node:slow", - "test:node:fast": "NODE_ENV=test node --test-timeout=60000 --test-force-exit --test $(find test -maxdepth 1 -name '*.test.js' ! -name 'generate-zod-object-intersections.test.js' -print) $(find test/lib -maxdepth 1 -name '*.test.js' ! -name 'cli-auth-scheme.test.js' ! -name 'cli-soft-fail.test.js' ! -name 'cli-webhook-receiver-flag.test.js' ! -name 'conformance-cli.test.js' ! -name 'storyboard-notices.test.js' -print)", - "test:node:fast:shard": "test -n \"$TEST_SHARD\" && NODE_ENV=test node --test-timeout=60000 --test-force-exit --test-shard=$TEST_SHARD --test $(find test -maxdepth 1 -name '*.test.js' ! -name 'generate-zod-object-intersections.test.js' -print) $(find test/lib -maxdepth 1 -name '*.test.js' ! -name 'cli-auth-scheme.test.js' ! -name 'cli-soft-fail.test.js' ! -name 'cli-webhook-receiver-flag.test.js' ! -name 'conformance-cli.test.js' ! -name 'storyboard-notices.test.js' -print)", - "test:node:slow": "NODE_ENV=test node --test-timeout=180000 --test-force-exit --test test/generate-zod-object-intersections.test.js test/lib/cli-auth-scheme.test.js test/lib/cli-soft-fail.test.js test/lib/cli-webhook-receiver-flag.test.js test/lib/conformance-cli.test.js test/lib/storyboard-notices.test.js", + "test:node:fast": "NODE_ENV=test node --test-timeout=60000 --test-force-exit --test $(find test -maxdepth 1 -name '*.test.js' ! -name 'canonical-creatives-a2a-e2e.test.js' ! -name 'generate-zod-object-intersections.test.js' -print) $(find test/lib -maxdepth 1 -name '*.test.js' ! -name 'cli-auth-scheme.test.js' ! -name 'cli-soft-fail.test.js' ! -name 'cli-webhook-receiver-flag.test.js' ! -name 'conformance-cli.test.js' ! -name 'storyboard-notices.test.js' -print)", + "test:node:fast:shard": "test -n \"$TEST_SHARD\" && NODE_ENV=test node --test-timeout=60000 --test-force-exit --test-shard=$TEST_SHARD --test $(find test -maxdepth 1 -name '*.test.js' ! -name 'canonical-creatives-a2a-e2e.test.js' ! -name 'generate-zod-object-intersections.test.js' -print) $(find test/lib -maxdepth 1 -name '*.test.js' ! -name 'cli-auth-scheme.test.js' ! -name 'cli-soft-fail.test.js' ! -name 'cli-webhook-receiver-flag.test.js' ! -name 'conformance-cli.test.js' ! -name 'storyboard-notices.test.js' -print)", + "test:node:slow": "NODE_ENV=test node --test-timeout=180000 --test-force-exit --test test/canonical-creatives-a2a-e2e.test.js test/generate-zod-object-intersections.test.js test/lib/cli-auth-scheme.test.js test/lib/cli-soft-fail.test.js test/lib/cli-webhook-receiver-flag.test.js test/lib/conformance-cli.test.js test/lib/storyboard-notices.test.js", "pretest:lib": "npm run schemas:ensure", "test:lib": "npm run test:lib:fast && npm run test:lib:slow", "test:lib:fast": "NODE_ENV=test node --test-timeout=60000 --test-force-exit --test $(find test/lib -maxdepth 1 -name '*.test.js' ! -name 'cli-auth-scheme.test.js' ! -name 'cli-soft-fail.test.js' ! -name 'cli-webhook-receiver-flag.test.js' ! -name 'conformance-cli.test.js' ! -name 'storyboard-notices.test.js' -print)", @@ -672,7 +672,7 @@ }, "minimatch": "^10.2.1" }, - "adcp_version": "3.2.0-beta.5", + "adcp_version": "3.2.0-beta.6", "sideEffects": [ "**/default-invariants.*" ] diff --git a/scripts/check-package-size.mjs b/scripts/check-package-size.mjs index 4b48a94d7..feffc0e82 100644 --- a/scripts/check-package-size.mjs +++ b/scripts/check-package-size.mjs @@ -11,9 +11,14 @@ import { readFileSync } from 'node:fs'; import * as path from 'node:path'; import { fileURLToPath } from 'node:url'; -const MAX_PACKED_TARBALL_BYTES = 45 * 1024 * 1024; -const MAX_UNPACKED_PACKAGE_BYTES = 305 * 1024 * 1024; -const MAX_PACKED_FILE_COUNT = 7_400; +// The signed 3.2.0-beta.6 bundle adds two substantial canonical format +// schemas and expands the reporting/tool projections. Keep the budgets tight +// to the resulting artifact (about 48 MiB packed, 337 MiB unpacked, 7,811 +// files) while allowing normal compressor variance across supported Node/npm +// versions. +const MAX_PACKED_TARBALL_BYTES = 50 * 1024 * 1024; +const MAX_UNPACKED_PACKAGE_BYTES = 340 * 1024 * 1024; +const MAX_PACKED_FILE_COUNT = 7_900; const MAX_CJS_SCHEMA_DECLARATION_BYTES = 45 * 1024 * 1024; const MAX_ESM_SCHEMA_FACADE_BYTES = 1024; const EXPECTED_ESM_SCHEMA_FACADE = "export * from './schemas.generated.js';\n"; diff --git a/scripts/generate-agent-docs.ts b/scripts/generate-agent-docs.ts index 30944f1ee..737e81ef0 100644 --- a/scripts/generate-agent-docs.ts +++ b/scripts/generate-agent-docs.ts @@ -190,8 +190,8 @@ function fieldType(prop: any): string { if (prop.enum) return prop.enum.map((v: string) => `'${v}'`).join(' | '); if (prop.const) return `'${prop.const}'`; if (prop.type === 'array') { - const itemType = prop.items?.title || prop.items?.type || 'object'; - return `${itemType}[]`; + const itemType = fieldType(prop.items) || 'object'; + return itemType.includes(' | ') ? `(${itemType})[]` : `${itemType}[]`; } if (prop.type === 'object' && prop.title) return prop.title; if (prop.$ref) { diff --git a/scripts/generate-types.ts b/scripts/generate-types.ts index 5baa554d2..9df11593b 100644 --- a/scripts/generate-types.ts +++ b/scripts/generate-types.ts @@ -72,6 +72,7 @@ const PRIORITY_CANONICAL_SCHEMAS = [ 'core/forecast-point.json', 'core/targeting-overlay-support.json', 'core/targeting-overlay-requirements.json', + 'core/canonical-format-option.json', 'core/delivery-metric-aggregate.json', 'core/cancellation-policy.json', 'media-buy/package-update.json', @@ -93,6 +94,8 @@ const PRIORITY_CANONICAL_SCHEMAS = [ 'formats/canonical/native_in_feed.json', 'formats/canonical/responsive_creative.json', 'formats/canonical/agent_placement.json', + 'formats/canonical/seller_rendered_stateful_display.json', + 'formats/canonical/coordinated_placements.json', // Present in the signed 3.2 manifest but omitted from index.json's legacy // governance task aggregation. Keep its public validators available until // the index and manifest converge upstream. @@ -140,6 +143,7 @@ const PRIORITY_CANONICAL_TYPE_NAMES = new Set([ 'ForecastPoint', 'TargetingOverlaySupport', 'TargetingOverlayRequirements', + 'CanonicalFormatOption', 'DeliveryMetricAggregate', 'CancellationPolicy', 'PackageUpdate', @@ -157,6 +161,8 @@ const PRIORITY_CANONICAL_TYPE_NAMES = new Set([ 'CanonicalFormatNativeInFeed', 'CanonicalFormatResponsiveCreative', 'CanonicalFormatAgentPlacementAISurfaceSponsoredPlacement', + 'CanonicalFormatSellerRenderedStatefulDisplay', + 'CanonicalFormatCoordinatedPlacements', 'SizeModeMutex', 'Fixed', 'MultiSize', @@ -903,12 +909,68 @@ function normalizePostalAreaForCodegen(schema: any): any { }; } +/** + * Keep the optional, closed qualifier object on VendorMetricValue visible to + * json-schema-to-typescript. + * + * jsts drops an anonymous object when all of its properties are optional and + * `additionalProperties` is false. Giving this schema-owned object the same + * semantic title as the canonical qualifier makes jsts emit the field and + * reuse the existing qualifier shape without changing wire validation. + */ +export function nameVendorMetricValueQualifierForCodegen(schema: any): any { + if (schema?.title !== 'Vendor Metric Value') return schema; + const qualifier = schema.properties?.qualifier; + if (!qualifier || typeof qualifier !== 'object' || Array.isArray(qualifier)) return schema; + + return { + ...schema, + properties: { + ...schema.properties, + qualifier: { + ...qualifier, + title: 'Canonical Metric Qualifier', + }, + }, + }; +} + +/** + * Keep the compact format-option discriminator aligned with the authoritative + * canonical-kind vocabulary. The beta.6 option document retained a stale + * inline enum while canonical-format-kind.json added two promoted formats. + */ +export function normalizeCanonicalFormatOptionKindsForCodegen(schema: any): any { + if (schema?.title !== 'Canonical Format Option') return schema; + const formatKind = schema.properties?.format_kind; + if (!formatKind || typeof formatKind !== 'object' || Array.isArray(formatKind)) return schema; + + const vocabularyPath = path.join(LATEST_CACHE_DIR, 'core/canonical-format-kind.json'); + const vocabulary = JSON.parse(readFileSync(vocabularyPath, 'utf8')) as { enum?: unknown }; + if (!Array.isArray(vocabulary.enum) || !vocabulary.enum.every(value => typeof value === 'string')) { + throw new Error('canonical-format-kind.json must contain a string enum for code generation.'); + } + + return { + ...schema, + properties: { + ...schema.properties, + format_kind: { + ...formatKind, + enum: [...vocabulary.enum], + }, + }, + }; +} + export function enforceStrictSchema(schema: any): any { if (!schema || typeof schema !== 'object') { return schema; } schema = normalizePostalAreaForCodegen(schema); + schema = nameVendorMetricValueQualifierForCodegen(schema); + schema = normalizeCanonicalFormatOptionKindsForCodegen(schema); schema = expandConditionalRequiredDiscriminator(schema); schema = preservePostalCountrySystemRequiredness(schema); schema = dropValidationOnlyAnyOf(schema); diff --git a/scripts/generate-zod-from-ts.ts b/scripts/generate-zod-from-ts.ts index 1e750ad96..894a12cde 100644 --- a/scripts/generate-zod-from-ts.ts +++ b/scripts/generate-zod-from-ts.ts @@ -80,7 +80,12 @@ function postProcessUndefinedImports(content: string): string { * Generated annotations are intentionally verbose; add new TS7056 cases here * instead of hand-authoring equivalent schema declarations elsewhere. */ -const TS7056_SCHEMAS: Array<{ name: string; tsType?: string; objectShape?: boolean }> = [ +const TS7056_SCHEMAS: Array<{ + name: string; + tsType?: string; + objectShape?: boolean; + typeSource?: 'tools' | 'core'; +}> = [ { name: 'AdCPAsyncResponseDataSchema' }, { name: 'MCPWebhookPayloadSchema' }, // 3.1.0-beta.2 pin flip — `.and(z.union([...]))` compound patterns push @@ -119,12 +124,39 @@ const TS7056_SCHEMAS: Array<{ name: string; tsType?: string; objectShape?: boole { name: 'WholesaleFeedWebhookSchema' }, { name: 'ComplyTestControllerRequestSchema', objectShape: true }, { name: 'ListCreativesResponseSchema' }, + // 3.2.0-beta.6 expands canonical format declarations and creative-agent + // responses enough to exceed declaration serialization limits. + { name: 'ProductFormatDeclarationSchema', tsType: 'ProductFormatDeclaration', objectShape: true }, + { name: 'PlacementSchema', tsType: 'Placement' }, + { name: 'FormatSchema', tsType: 'Format', objectShape: true, typeSource: 'core' }, + { name: 'TransformerSchema', tsType: 'Transformer', objectShape: true }, + { name: 'AvailablePackageSchema', tsType: 'AvailablePackage', objectShape: true, typeSource: 'core' }, + { name: 'ListCreativeFormatsResponseSchema', tsType: 'ListCreativeFormatsResponse', objectShape: true }, + { name: 'PackageStatusSchema', tsType: 'PackageStatus', objectShape: true }, + { + name: 'ListTransformersResponseCreativeAgentSchema', + tsType: 'ListTransformersResponseCreativeAgent', + objectShape: true, + }, + { name: 'GetAdCPCapabilitiesResponseSchema', tsType: 'GetAdCPCapabilitiesResponse', objectShape: true }, + { name: 'ListTransformersResponseSchema', tsType: 'ListTransformersResponse', objectShape: true }, + { + name: 'CanonicalFormatSellerRenderedStatefulDisplaySchema', + tsType: 'CanonicalFormatSellerRenderedStatefulDisplay', + objectShape: true, + }, + { + name: 'CanonicalFormatCoordinatedPlacementsSchema', + tsType: 'CanonicalFormatCoordinatedPlacements', + objectShape: true, + }, ]; function postProcessTS7056Annotations(content: string): string { let result = content; - const typesToImport: string[] = []; - for (const { name, tsType, objectShape } of TS7056_SCHEMAS) { + const toolTypesToImport = new Set(); + const coreTypesToImport = new Set(); + for (const { name, tsType, objectShape, typeSource = 'tools' } of TS7056_SCHEMAS) { const pattern = new RegExp(`export const ${name} = `); if (!pattern.test(result)) { throw new Error( @@ -152,7 +184,7 @@ function postProcessTS7056Annotations(content: string): string { ? `{ request_type: z.ZodType } & Record` : `{ [K in keyof ${tsType}]-?: z.ZodType<${tsType}[K], ${tsType}[K]> }`; annotation = `z.ZodObject<${objectShapeType}, any> & z.ZodType<${widened}, ${widened}>`; - typesToImport.push(tsType); + (typeSource === 'core' ? coreTypesToImport : toolTypesToImport).add(tsType); } else { annotation = name === 'ProductSchema' @@ -162,7 +194,7 @@ function postProcessTS7056Annotations(content: string): string { } else if (tsType) { const widened = `${tsType} & Record`; annotation = `z.ZodType<${widened}, ${widened}>`; - typesToImport.push(tsType); + (typeSource === 'core' ? coreTypesToImport : toolTypesToImport).add(tsType); } else { annotation = 'z.ZodType'; } @@ -173,9 +205,18 @@ function postProcessTS7056Annotations(content: string): string { } // Inject `import type { ... } from './tools.generated'` for the typed-zod // entries. The compound schemas reference response types defined there. - if (typesToImport.length > 0) { - const importStatement = `import type { ${typesToImport.join(', ')} } from './tools.generated';\n`; - result = result.replace(/import { z } from "zod";\n/, `import { z } from "zod";\n${importStatement}`); + if (toolTypesToImport.size > 0 || coreTypesToImport.size > 0) { + const importStatements = [ + toolTypesToImport.size > 0 + ? `import type { ${[...toolTypesToImport].join(', ')} } from './tools.generated';` + : undefined, + coreTypesToImport.size > 0 + ? `import type { ${[...coreTypesToImport].join(', ')} } from './core.generated';` + : undefined, + ] + .filter(Boolean) + .join('\n'); + result = result.replace(/import { z } from "zod";\n/, `import { z } from "zod";\n${importStatements}\n`); } return result; } @@ -882,6 +923,39 @@ function postProcessCanonicalProposalRuntimeConstraints(content: string, exactSc ); } +/** Replace a generated schema expression with one projected from its authoritative JSON Schema. */ +function postProcessExactSchema(content: string, schemaName: string, exactSchemaExpression: string): string { + const target = findSchemaExportExpressions(content).find(entry => entry.name === schemaName); + if (!target) throw new Error(`postProcessExactSchema: unable to locate ${schemaName}.`); + return content.slice(0, target.expressionStart) + exactSchemaExpression + content.slice(target.expressionEnd); +} + +/** Add a refinement to one property schema inside a generated z.object expression. */ +function refineGeneratedObjectProperty(expression: string, propertyName: string, refinement: string): string { + const marker = `"${propertyName}": `; + const propertyStart = expression.indexOf(marker); + if (propertyStart < 0) throw new Error(`Unable to locate generated property ${propertyName}.`); + const valueStart = propertyStart + marker.length; + let depth = 0; + + for (let i = valueStart; i < expression.length; i++) { + const literalEnd = skipQuotedOrRegexLiteral(expression, i); + if (literalEnd !== undefined) { + i = literalEnd - 1; + continue; + } + const ch = expression[i]; + if (ch === '(' || ch === '{' || ch === '[') depth++; + else if (ch === ')' || ch === '}' || ch === ']') depth--; + else if (ch === ',' && depth === 0) { + const value = expression.slice(valueStart, i); + return expression.slice(0, valueStart) + `${value}.superRefine(${refinement})` + expression.slice(i); + } + } + + throw new Error(`Unable to locate the end of generated property ${propertyName}.`); +} + /** * Restore refine_proposals runtime constraints that TypeScript cannot retain. * @@ -1140,7 +1214,7 @@ function postProcessCreativeRuntimeConstraints(content: string): string { // resolving it until parse time to avoid a top-level TDZ reference. .replace( /assets: z\.record\(z\.string\(\), (?:z\.unknown\(\)|z\.union\(\[AssetVariantSchema, z\.array\(AssetVariantSchema\)\]\))\)/, - 'assets: CreativeAssetsSchema' + 'assets: CreativeAssetsRuntimeSchema' ); if (constrainedAssets === schema.block) { throw new Error(`Unable to preserve creative asset constraints on ${schemaName}.`); @@ -1171,6 +1245,8 @@ function postProcessCreativeRuntimeConstraints(content: string): string { preserveCreativeConstraints('CreativeManifestSchema'); const creativeManifest = schemaBlock('CreativeManifestSchema'); + // Keep this runtime validator distinct from the public CreativeAssetsSchema + // that newer protocol bundles generate for the named TypeScript interface. const assetValueSchema = `const CreativeAssetValueSchema: z.ZodType = z.unknown().superRefine((value, ctx) => { const variants = Array.isArray(value) ? value : [value]; if (variants.length === 0 || variants.some(variant => !AssetVariantSchema.safeParse(variant).success)) { @@ -1181,7 +1257,7 @@ function postProcessCreativeRuntimeConstraints(content: string): string { } }); -const CreativeAssetsSchema: z.ZodType> = z.record(z.string(), z.unknown()).superRefine((assets, ctx) => { +const CreativeAssetsRuntimeSchema: z.ZodType> = z.record(z.string(), z.unknown()).superRefine((assets, ctx) => { for (const [slotKey, assetValue] of Object.entries(assets)) { if (/^[a-z0-9_]+$/.test(slotKey) && !CreativeAssetValueSchema.safeParse(assetValue).success) { ctx.addIssue({ @@ -2491,6 +2567,167 @@ function postProcessObjectIntersections(content: string): string { } } +/** + * Keep the beta.6 image-format motion narrowing object-shaped. + * + * The source type intersects a narrow image-only motion_level with the shared + * canonical-format object, whose motion_level enum is broader. A normal Zod + * merge would let the broader property overwrite the narrowing, while `.and()` + * loses the public ZodObject composition helpers. Remove the broad property + * before merging the narrow object so runtime validation and object helpers are + * both preserved. + */ +function postProcessCanonicalImageMotionNarrowing(content: string): string { + const target = findSchemaExportExpressions(content).find(entry => entry.name === 'CanonicalFormatImageSchema'); + if (!target) throw new Error('postProcessCanonicalImageMotionNarrowing: schema export not found.'); + const expression = content.slice(target.expressionStart, target.expressionEnd); + let depth = 0; + + for (let i = 0; i < expression.length; i++) { + const literalEnd = skipQuotedOrRegexLiteral(expression, i); + if (literalEnd !== undefined) { + i = literalEnd - 1; + continue; + } + const ch = expression[i]; + if (ch === '(' || ch === '{' || ch === '[') depth++; + else if (ch === ')' || ch === '}' || ch === ']') depth--; + else if (depth === 0 && expression.startsWith('.and(', i)) { + const broad = scanBalanced(expression, i + '.and'.length); + if (!broad || expression.slice(broad.end).trim()) break; + const narrow = expression.slice(0, i); + const rewritten = `${broad.body}.omit({ motion_level: true }).merge(${narrow})`; + return content.slice(0, target.expressionStart) + rewritten + content.slice(target.expressionEnd); + } + } + + throw new Error('postProcessCanonicalImageMotionNarrowing: expected a top-level object intersection.'); +} + +/** Restore beta.6 reporting constraints that the general compatibility loosening intentionally drops. */ +function postProcessBeta6ReportingConstraints(content: string): string { + let result = content; + + const requestedMetrics = 'requested_metrics: z.array(AvailableMetricSchema).optional(),'; + if (!result.includes(requestedMetrics)) { + throw new Error('postProcessBeta6ReportingConstraints: requested_metrics projection not found.'); + } + result = result.replace( + requestedMetrics, + `requested_metrics: z.array(AvailableMetricSchema).min(1).refine( + values => new Set(values).size === values.length, + { message: "requested_metrics must contain unique metrics" } + ).optional(),` + ); + + const delivery = findSchemaExportExpressions(result).find(entry => entry.name === 'DeliveryMetricsSchema'); + if (!delivery) throw new Error('postProcessBeta6ReportingConstraints: DeliveryMetricsSchema not found.'); + const deliveryExpression = result.slice(delivery.expressionStart, delivery.expressionEnd); + const threshold = 'threshold_seconds: z.number(),'; + if (!deliveryExpression.includes(threshold)) { + throw new Error('postProcessBeta6ReportingConstraints: time-based view threshold not found.'); + } + result = + result.slice(0, delivery.expressionStart) + + deliveryExpression.replace(threshold, 'threshold_seconds: z.number().gt(0),') + + result.slice(delivery.expressionEnd); + + const qualifier = findSchemaExportExpressions(result).find(entry => entry.name === 'CanonicalMetricQualifierSchema'); + if (!qualifier) throw new Error('postProcessBeta6ReportingConstraints: CanonicalMetricQualifierSchema not found.'); + const qualifierExpression = result.slice(qualifier.expressionStart, qualifier.expressionEnd); + if (!qualifierExpression.endsWith('.passthrough()')) { + throw new Error('postProcessBeta6ReportingConstraints: qualifier is no longer a passthrough object.'); + } + result = + result.slice(0, qualifier.expressionStart) + + qualifierExpression.replace(/\.passthrough\(\)$/, '.strict()') + + result.slice(qualifier.expressionEnd); + + const canonicalQualifierBody = `z.object({ + viewability_standard: ViewabilityStandardSchema.optional(), + completion_source: CompletionSourceSchema.optional(), + attribution_methodology: AttributionMethodologySchema.optional(), + attribution_window: DurationSchema.optional(), + lift_dimension: LiftDimensionSchema.optional() + }).passthrough()`; + const qualifierEnd = findSchemaExportExpressions(result).find( + entry => entry.name === 'CanonicalMetricQualifierSchema' + )!.expressionEnd; + const tail = result.slice(qualifierEnd).replaceAll(canonicalQualifierBody, 'CanonicalMetricQualifierSchema'); + result = result.slice(0, qualifierEnd) + tail; + + const vendorMetric = findSchemaExportExpressions(result).find(entry => entry.name === 'VendorMetricValueSchema'); + if ( + !vendorMetric || + !result + .slice(vendorMetric.expressionStart, vendorMetric.expressionEnd) + .includes('qualifier: CanonicalMetricQualifierSchema.optional()') + ) { + throw new Error('postProcessBeta6ReportingConstraints: VendorMetricValue qualifier was not canonicalized.'); + } + + const deliveryAggregate = findSchemaExportExpressions(result).find( + entry => entry.name === 'DeliveryMetricAggregateSchema' + ); + if (!deliveryAggregate) { + throw new Error('postProcessBeta6ReportingConstraints: DeliveryMetricAggregateSchema not found.'); + } + let aggregateExpression = result.slice(deliveryAggregate.expressionStart, deliveryAggregate.expressionEnd); + const inlineQualifier = `z.object({ + viewability_standard: ViewabilityStandardSchema.optional(), + completion_source: CompletionSourceSchema.optional(), + attribution_methodology: AttributionMethodologySchema.optional(), + attribution_window: DurationSchema.optional(), + lift_dimension: LiftDimensionSchema.optional() + }).passthrough()`; + const inlineQualifierCount = aggregateExpression.split(inlineQualifier).length - 1; + if (inlineQualifierCount !== 2) { + throw new Error( + `postProcessBeta6ReportingConstraints: expected two aggregate qualifier projections, found ${inlineQualifierCount}.` + ); + } + aggregateExpression = aggregateExpression.replaceAll(inlineQualifier, 'CanonicalMetricQualifierSchema'); + aggregateExpression += `.superRefine((row, ctx) => { + if (row.scope !== "standard") return; + const requiredComponents: Record = { + viewable_rate: ["measurable_impressions", "viewable_impressions"], + completion_rate: ["impressions", "completed_views"], + cost_per_acquisition: ["spend", "conversions"], + roas: ["spend", "conversion_value"] + }; + for (const field of requiredComponents[row.metric_id] ?? []) { + if ((row as unknown as Record)[field] === undefined) { + ctx.addIssue({ code: "custom", path: [field], message: \`\${field} is required for \${row.metric_id}\` }); + } + } +})`; + result = + result.slice(0, deliveryAggregate.expressionStart) + + aggregateExpression + + result.slice(deliveryAggregate.expressionEnd); + + return result; +} + +/** Keep compact product format options aligned with the beta.6 canonical-kind vocabulary. */ +function postProcessBeta6CanonicalFormatOptionKinds(content: string): string { + const target = findSchemaExportExpressions(content).find(entry => entry.name === 'CanonicalFormatOptionSchema'); + if (!target) throw new Error('postProcessBeta6CanonicalFormatOptionKinds: schema export not found.'); + let expression = content.slice(target.expressionStart, target.expressionEnd); + if (expression.includes('z.literal("seller_rendered_stateful_display")')) return content; + + const finalOldKind = 'z.literal("agent_placement"), z.literal("custom")'; + const occurrences = expression.split(finalOldKind).length - 1; + if (occurrences < 1) { + throw new Error('postProcessBeta6CanonicalFormatOptionKinds: canonical format-kind union not found.'); + } + expression = expression.replaceAll( + finalOldKind, + 'z.literal("agent_placement"), z.literal("seller_rendered_stateful_display"), z.literal("coordinated_placements"), z.literal("custom")' + ); + return content.slice(0, target.expressionStart) + expression + content.slice(target.expressionEnd); +} + function postProcessObjectUnionIntersections(content: string): string { const schemaExpressions = extractSchemaExports(content); const shapeCache = new Map(); @@ -3017,6 +3254,163 @@ async function generateZodSchemas() { }); })()`; zodSchemas = postProcessCanonicalProposalRuntimeConstraints(zodSchemas, exactCanonicalProposal); + + // These promoted beta.6 canonical formats contain nested required-only + // unions, conditionals, and `contains` constraints that TypeScript cannot + // faithfully carry through ts-to-zod. Project their dereferenced wire + // schemas directly so public Zod validation fails closed. + for (const [schemaName, schemaFile] of [ + ['CanonicalFormatSellerRenderedStatefulDisplaySchema', 'seller_rendered_stateful_display.json'], + ['CanonicalFormatCoordinatedPlacementsSchema', 'coordinated_placements.json'], + ] as const) { + const source = JSON.parse( + readFileSync(path.join(__dirname, '../schemas/cache/latest/formats/canonical', schemaFile), 'utf8') + ); + const dereferenced = (await $RefParser.dereference(source)) as any; + removeDiscriminatorHints(dereferenced); + // Both promoted formats extend exactly one plain object base. Flatten + // that structural allOf before Zod projection so the public export stays + // a real ZodObject (and therefore keeps shape/pick/omit/extend) while + // local property overrides replace their base declarations cleanly. + const baseSchemas = Array.isArray(dereferenced.allOf) ? dereferenced.allOf : []; + if ( + baseSchemas.length !== 1 || + baseSchemas[0]?.type !== 'object' || + typeof baseSchemas[0]?.properties !== 'object' + ) { + throw new Error(`${schemaFile}: expected one dereferenced object base in allOf.`); + } + const baseSchema = baseSchemas[0] as Record; + const localProperties = dereferenced.properties as Record; + dereferenced.type = 'object'; + dereferenced.properties = { ...baseSchema.properties }; + for (const [propertyName, localProperty] of Object.entries(localProperties ?? {})) { + const baseProperty = baseSchema.properties[propertyName]; + dereferenced.properties[propertyName] = + baseProperty && typeof baseProperty === 'object' && localProperty && typeof localProperty === 'object' + ? { ...baseProperty, ...(localProperty as Record) } + : localProperty; + } + dereferenced.required = [...new Set([...(baseSchema.required ?? []), ...(dereferenced.required ?? [])])]; + delete dereferenced.allOf; + // Defaults in overlapping allOf branches can produce different parsed + // values for the same key, which Zod intersections cannot merge. Wire + // validation should not mutate caller input, so discard annotation-only + // defaults before projecting the authoritative constraints. + const seenDefaults = new WeakSet(); + const removeDefaults = (value: unknown): void => { + if (!value || typeof value !== 'object' || seenDefaults.has(value)) return; + seenDefaults.add(value); + if (!Array.isArray(value)) delete (value as Record).default; + Object.values(value).forEach(removeDefaults); + }; + removeDefaults(dereferenced); + let exact = jsonSchemaToZod(dereferenced, converterOptions).replaceAll('.strict()', '.passthrough()'); + if (schemaName === 'CanonicalFormatSellerRenderedStatefulDisplaySchema') { + exact = refineGeneratedObjectProperty( + exact, + 'breakpoints', + `(breakpoints, ctx) => { + breakpoints.forEach((breakpoint, index) => { + const widthKeys = ["width", "width_range", "width_mode"].filter(key => breakpoint[key] !== undefined); + const heightKeys = ["height", "height_range", "viewport_height_percent"].filter(key => breakpoint[key] !== undefined); + if (widthKeys.length !== 1) ctx.addIssue({ code: "custom", path: [index], message: "breakpoint requires exactly one width mode" }); + if (heightKeys.length !== 1) ctx.addIssue({ code: "custom", path: [index], message: "breakpoint requires exactly one height mode" }); + }); + }` + ); + exact = refineGeneratedObjectProperty( + exact, + 'transitions', + `(transitions, ctx) => { + if (transitions === undefined) return; + const rules: Record = { + timer: { required: ["delay_ms"], forbidden: ["input", "media_event", "scroll_reference", "scroll_threshold_percent", "scroll_start_percent", "scroll_end_percent"], modes: ["instant", "animated"] }, + in_view_timer: { required: ["delay_ms"], forbidden: ["input", "media_event", "scroll_reference", "scroll_threshold_percent", "scroll_start_percent", "scroll_end_percent"], modes: ["instant", "animated"] }, + scroll_threshold: { required: ["input", "scroll_reference", "scroll_threshold_percent"], forbidden: ["delay_ms", "media_event", "scroll_start_percent", "scroll_end_percent"], modes: ["instant", "animated"] }, + scroll_progress: { required: ["input", "scroll_reference", "scroll_start_percent", "scroll_end_percent"], forbidden: ["delay_ms", "media_event", "scroll_threshold_percent", "direction"], modes: ["scroll_linked"] }, + user_action: { required: ["input"], forbidden: ["delay_ms", "media_event", "scroll_reference", "scroll_threshold_percent", "scroll_start_percent", "scroll_end_percent", "direction"], modes: ["instant", "animated"] }, + media_event: { required: ["media_event"], forbidden: ["input", "delay_ms", "scroll_reference", "scroll_threshold_percent", "scroll_start_percent", "scroll_end_percent", "direction"], modes: ["instant", "animated"] } + }; + transitions.forEach((transition, index) => { + const rule = rules[transition.trigger]; + if (!rule) return; + rule.required.forEach((key: string) => { + if (transition[key] === undefined) ctx.addIssue({ code: "custom", path: [index, key], message: "transition field is required for this trigger" }); + }); + rule.forbidden.forEach((key: string) => { + if (transition[key] !== undefined) ctx.addIssue({ code: "custom", path: [index, key], message: "transition field is forbidden for this trigger" }); + }); + if (!rule.modes.includes(transition.transition_mode)) ctx.addIssue({ code: "custom", path: [index, "transition_mode"], message: "invalid transition_mode for this trigger" }); + if ((transition.trigger === "scroll_threshold" || transition.trigger === "scroll_progress") && transition.input !== "scroll") { + ctx.addIssue({ code: "custom", path: [index, "input"], message: "scroll transitions require input=scroll" }); + } + }); + }` + ); + exact = refineGeneratedObjectProperty( + exact, + 'duration_ms_range', + `(range, ctx) => { + if (range !== undefined && !range.some(value => value !== null)) { + ctx.addIssue({ code: "custom", path: [], message: "duration_ms_range requires at least one finite bound" }); + } + }` + ); + } + if (schemaName === 'CanonicalFormatCoordinatedPlacementsSchema') { + exact = refineGeneratedObjectProperty( + exact, + 'components', + `(components, ctx) => { + if (!components.some(component => component.required === true)) { + ctx.addIssue({ code: "custom", path: [], message: "At least one component must be required" }); + } + components.forEach((component, index) => { + const referenced = component.format_option_ref !== undefined; + const hasKind = component.format_kind !== undefined; + const hasParams = component.params !== undefined; + if (referenced === (hasKind || hasParams) || (!referenced && (!hasKind || !hasParams))) { + ctx.addIssue({ code: "custom", path: [index], message: "Each component must select exactly one referenced or inline format" }); + return; + } + if (!referenced) { + const paramsSchema = CoordinatedPlacementInlineParamsRuntimeSchemas[component.format_kind]; + if (!paramsSchema) { + ctx.addIssue({ code: "custom", path: [index, "format_kind"], message: "Unsupported coordinated placement format_kind" }); + } else if (!paramsSchema.safeParse(component.params).success) { + ctx.addIssue({ code: "custom", path: [index, "params"], message: "params do not match format_kind" }); + } + } + }); + }` + ); + } + zodSchemas = postProcessExactSchema(zodSchemas, schemaName, exact); + if (schemaName === 'CanonicalFormatCoordinatedPlacementsSchema') { + const exportMarker = `export const ${schemaName}`; + const exportStart = zodSchemas.indexOf(exportMarker); + if (exportStart < 0) throw new Error(`${schemaFile}: unable to locate coordinated schema export.`); + const inlineParamsSchemas = `const CoordinatedPlacementInlineParamsRuntimeSchemas: Record = { + image: CanonicalFormatImageSchema, + html5: CanonicalFormatHTML5BannerSchema, + display_tag: CanonicalFormatDisplayTagSchema, + image_carousel: CanonicalFormatImageCarouselSchema, + video_hosted: CanonicalFormatHostedVideoSchema, + video_vast: CanonicalFormatVASTVideoSchema, + audio_hosted: CanonicalFormatHostedAudioSchema, + audio_daast: CanonicalFormatDAASTAudioSchema, + sponsored_placement: CanonicalFormatSponsoredPlacementRetailMediaCatalogDrivenSchema, + native_in_feed: CanonicalFormatNativeInFeedSchema, + responsive_creative: CanonicalFormatResponsiveCreativeSchema, + agent_placement: CanonicalFormatAgentPlacementAISurfaceSponsoredPlacementSchema, + seller_rendered_stateful_display: CanonicalFormatSellerRenderedStatefulDisplaySchema +}; + +`; + zodSchemas = zodSchemas.slice(0, exportStart) + inlineParamsSchemas + zodSchemas.slice(exportStart); + } + } zodSchemas = postProcessRefineProposalsRuntimeConstraints(zodSchemas); // Post-process: Distribute object-envelope intersections over union object arms. @@ -3033,6 +3427,18 @@ async function generateZodSchemas() { // richer/conflicting intersections alone so future schema changes do not weaken checks. zodSchemas = postProcessObjectIntersections(zodSchemas); + // Preserve the image format's beta.6 motion-level refinement without + // regressing its public ZodObject composition surface. + zodSchemas = postProcessCanonicalImageMotionNarrowing(zodSchemas); + + // Preserve the non-empty/unique requested metric set, positive view + // threshold, and closed canonical qualifier from the beta.6 wire schema. + zodSchemas = postProcessBeta6ReportingConstraints(zodSchemas); + + // The beta.6 compact option source missed the two promoted canonical + // kinds even though the shared canonical-kind vocabulary includes them. + zodSchemas = postProcessBeta6CanonicalFormatOptionKinds(zodSchemas); + // Keep the create-media-buy request's public schema object-shaped while // enforcing its lifecycle-mode union as a refinement. zodSchemas = postProcessCreateMediaBuyRequestObject(zodSchemas); diff --git a/skills/adcp-creative/SKILL.md b/skills/adcp-creative/SKILL.md index 277ba7777..591106466 100644 --- a/skills/adcp-creative/SKILL.md +++ b/skills/adcp-creative/SKILL.md @@ -32,7 +32,7 @@ The Creative Protocol provides standardized discovery, build, and preview tasks: ## Canonical formats (AdCP 3.2) -Products and manifests use 12 canonical `format_kind` values: `image`, `html5`, `display_tag`, `image_carousel`, `video_hosted`, `video_vast`, `audio_hosted`, `audio_daast`, `sponsored_placement`, `native_in_feed`, `responsive_creative`, and `agent_placement`. Use `custom` only for a shape outside those canonicals, with required `format_shape` and `format_schema`. +Products and manifests use 14 canonical `format_kind` values: `image`, `html5`, `display_tag`, `image_carousel`, `video_hosted`, `video_vast`, `audio_hosted`, `audio_daast`, `sponsored_placement`, `native_in_feed`, `responsive_creative`, `agent_placement`, `seller_rendered_stateful_display`, and `coordinated_placements`. Use `custom` only for a shape outside those canonicals, with required `format_shape` and `format_schema`. A `ProductFormatDeclaration` carries: diff --git a/skills/adcp-media-buy/SKILL.md b/skills/adcp-media-buy/SKILL.md index bf8dccd5b..8b3b7e08f 100644 --- a/skills/adcp-media-buy/SKILL.md +++ b/skills/adcp-media-buy/SKILL.md @@ -58,7 +58,7 @@ The Media Buy Protocol provides these common standardized tasks: Products carry `format_options[]`: a list of `ProductFormatDeclaration` entries describing the creative shapes the product accepts. Each declaration carries: -- `format_kind` — one of the 12 canonicals: `image`, `html5`, `display_tag`, `image_carousel`, `video_hosted`, `video_vast`, `audio_hosted`, `audio_daast`, `sponsored_placement`, `native_in_feed`, `responsive_creative`, or `agent_placement`; use `custom` only with `format_shape` and `format_schema` +- `format_kind` — one of the 14 canonicals: `image`, `html5`, `display_tag`, `image_carousel`, `video_hosted`, `video_vast`, `audio_hosted`, `audio_daast`, `sponsored_placement`, `native_in_feed`, `responsive_creative`, `agent_placement`, `seller_rendered_stateful_display`, or `coordinated_placements`; use `custom` only with `format_shape` and `format_schema` - `params` — per-canonical parameters narrowing the format (dimensions, durations, codecs, char limits, CTA enums) - Optional `format_option_id` — disambiguates product options and identifies publisher-catalog declarations when paired with `publisher_domain` - Optional `v1_format_ref: [{agent_url, id}]` — array linking this v2 declaration to one or more v1 named formats (for dual emission during the v1↔v2 migration). Multi-size declarations should carry one ref per size diff --git a/src/lib/conformance/schemaArbitrary.ts b/src/lib/conformance/schemaArbitrary.ts index 1ccb4603b..95b72c3f2 100644 --- a/src/lib/conformance/schemaArbitrary.ts +++ b/src/lib/conformance/schemaArbitrary.ts @@ -527,11 +527,40 @@ function enforceSimpleConditionals(value: Record, schema: JsonS for (const entry of (schema.allOf as JsonSchema[] | undefined) ?? []) { current = enforceRequiredTriggerConst(current, entry); current = avoidUnsatisfiedConstConditional(current, entry, schema); + current = enforceElseForbiddenRequired(current, entry); } current = enforceConstThenForbidden(current, schema); return current; } +/** + * Honor the common `if` / `else: { not: { required: [...] } }` shape by + * removing fields that are forbidden when the discriminator does not match. + * The beta.6 creative manifest uses this to reserve `component_assets` for + * coordinated placements while keeping it out of every other format kind. + */ +function enforceElseForbiddenRequired( + value: Record, + conditional: JsonSchema +): Record { + const ifSchema = conditional.if as JsonSchema | undefined; + const ifProps = ifSchema?.properties as Record | undefined; + const triggerKeys = Array.isArray(ifSchema?.required) ? (ifSchema.required as string[]) : []; + const elseNot = (conditional.else as JsonSchema | undefined)?.not as JsonSchema | undefined; + const forbidden = Array.isArray(elseNot?.required) ? (elseNot.required as string[]) : []; + if (!ifProps || triggerKeys.length === 0 || forbidden.length === 0) return value; + + const matches = triggerKeys.every(key => { + const prop = ifProps[key]; + return key in value && prop && typeof prop === 'object' && 'const' in prop && value[key] === prop.const; + }); + if (matches) return value; + + const next = { ...value }; + for (const key of forbidden) delete next[key]; + return next; +} + /** * Optional selector fields commonly activate a required sibling (or a * required nested field) through if/then. When the random base record did diff --git a/src/lib/core/SingleAgentClient.ts b/src/lib/core/SingleAgentClient.ts index e50f11698..0a6784fff 100644 --- a/src/lib/core/SingleAgentClient.ts +++ b/src/lib/core/SingleAgentClient.ts @@ -7,10 +7,12 @@ import { ADCP_ENVELOPE_FIELDS } from '../types/adcp'; import { parseAdcpMajorVersion, type AdcpVersion } from '../version'; import { isAdcpVersionSupported, + isAdcpVersionAtLeast, isPre31AdcpVersion, isPre32AdcpVersion, resolveAdcpVersion, } from '../utils/adcp-version-config'; +import { beta6ReportingRequestIssue, type Beta6DeliveryRequestIssue } from '../media-buy/reporting-version'; import { getVersionAdapter, resolveAdapterKey } from '../adapters/version'; import { isExternalSchemaRootActive, schemaAllowsTopLevelField } from '../validation/schema-loader'; import type { @@ -7085,7 +7087,13 @@ export class SingleAgentClient { [CAPABILITY_DISCOVERY_CONTEXT]: capabilityDiscoveryContext, }; const serverVersion = await this.detectServerVersion(detectionOptions); - this.assertRequestSupportedByTargetVersion(taskName, normalizedParams, capabilityDiscoveryContext.capabilities); + this.assertRequestSupportedByTargetVersion( + taskName, + normalizedParams, + capabilityDiscoveryContext.capabilities, + false, + serverVersion + ); const { options: effectiveOptions, driftLog: webhookDriftLog } = this.suppressPre31DiscoveryWebhook( taskName, options, @@ -8331,6 +8339,11 @@ export class SingleAgentClient { _options?: TaskOptions, canonicalCreativeInvocation = false ): void { + const configuredVersion = this.config.wireAdcpVersion ?? this.resolvedAdcpVersion; + const beta6Issue = beta6ReportingRequestIssue(taskName, params); + if (beta6Issue && !isAdcpVersionAtLeast(configuredVersion, '3.2.0-beta.6')) { + this.throwBeta6ReportingUnsupported(taskName, beta6Issue, configuredVersion); + } if ( taskName === 'preview_creative' && canonicalCreativeInvocation && @@ -8428,6 +8441,28 @@ export class SingleAgentClient { canonicalCreativeInvocation = false, serverVersion?: 'v2' | 'v3' ): void { + const beta6Issue = beta6ReportingRequestIssue(taskName, params); + if (beta6Issue) { + const advertisedVersions = capabilities?.supportedVersions ?? []; + const responseVersion = + typeof capabilities?._raw?.adcp_version === 'string' ? capabilities._raw.adcp_version : undefined; + const supportsBeta6 = + advertisedVersions.some(version => isAdcpVersionAtLeast(version, '3.2.0-beta.6')) || + (advertisedVersions.length === 0 && isAdcpVersionAtLeast(responseVersion, '3.2.0-beta.6')); + const hasAuthoritativeLegacyEvidence = + serverVersion === 'v2' || + advertisedVersions.length > 0 || + responseVersion !== undefined || + (capabilities?._synthetic === false && capabilities?.version === 'v3'); + if (!supportsBeta6 && hasAuthoritativeLegacyEvidence) { + this.throwBeta6ReportingUnsupported( + taskName, + beta6Issue, + advertisedVersions.join(', ') || responseVersion || serverVersion || capabilities?.version || 'unknown', + 'the target seller does not advertise AdCP 3.2.0-beta.6 support' + ); + } + } if (taskName === 'preview_creative' && canonicalCreativeInvocation) { const advertisedVersions = capabilities?.supportedVersions ?? []; const responseVersion = @@ -8499,6 +8534,28 @@ export class SingleAgentClient { }); } + private throwBeta6ReportingUnsupported( + taskName: string, + issue: Beta6DeliveryRequestIssue, + currentVersion: string, + incompatibility = `this client is pinned to ${currentVersion}` + ): never { + const suggestion = 'Negotiate AdCP 3.2.0-beta.6 or omit the beta.6 reporting field or metric.'; + throw new ProtocolFeatureUnsupportedError([`${taskName}.${issue.field}`], [], this.agent.agent_uri, { + message: `${taskName} ${issue.field} requires AdCP 3.2.0-beta.6 or later; ${incompatibility}. ${suggestion}`, + field: issue.field, + suggestion, + details: { + feature: `${taskName}.${issue.field}`, + required_version: '3.2.0-beta.6', + capability_path: 'adcp.supported_versions', + current_version: currentVersion, + tool: taskName, + field: issue.field, + }, + }); + } + private throwCanonicalPreviewUnsupported(currentVersion: string, incompatibility?: string): never { const suggestion = 'Use previewCreativeLegacy() for format_id-based sellers, or negotiate AdCP 3.2 before calling previewCreative().'; diff --git a/src/lib/media-buy/compatibility.ts b/src/lib/media-buy/compatibility.ts index 2986978b8..2f112baf5 100644 --- a/src/lib/media-buy/compatibility.ts +++ b/src/lib/media-buy/compatibility.ts @@ -105,6 +105,7 @@ import { type EstablishedProposalTransitionResult, type ProposalSnapshotEntry as DurableProposalSnapshotEntry, } from './established-proposal-store'; +import { beta6ReportingRequestIssue } from './reporting-version'; type ActiveLegacyPurchaseOperation = Exclude; @@ -2990,6 +2991,17 @@ export class MediaBuyLifecycleCoordinator { } } + private assertBeta6ReportingRequest(operation: string, toolName: string, input: unknown): void { + if (compareRelease(this.negotiated_version, '3.2.0-beta.6') >= 0) return; + const issue = beta6ReportingRequestIssue(toolName, input); + if (!issue) return; + throw this.unsupported( + operation, + issue.field, + `The negotiated ${this.negotiated_version} seller cannot represent ${issue.detail}. No request was sent.` + ); + } + private assertProposalLifecycleAvailable(operation: string): void { if (compareRelease(this.negotiated_version, '3.0') >= 0) return; throw this.unsupported( @@ -5132,6 +5144,7 @@ export class MediaBuyLifecycleCoordinator { this.assertActive('listProducts'); const input = record(params); const lifecycle = this.selectLifecycle('list_products'); + this.assertBeta6ReportingRequest('listProducts', 'list_products', input); this.assertLegacyReferenceShapes('listProducts', input); if (lifecycle === 'compact') { this.assertValidCompactRequest('list_products', params, lifecycle); @@ -5213,6 +5226,7 @@ export class MediaBuyLifecycleCoordinator { this.assertActive('requestProposals'); const input = record(params); const lifecycle = this.selectLifecycle('request_proposals'); + this.assertBeta6ReportingRequest('requestProposals', 'request_proposals', input); this.assertLegacyReferenceShapes('requestProposals', input); if (lifecycle === 'compact') { this.assertValidCompactRequest('request_proposals', params, lifecycle, true); @@ -8227,6 +8241,7 @@ export class MediaBuyLifecycleCoordinator { ): Promise> { this.assertActive('refineProposals'); const lifecycle = this.selectLifecycle('refine_proposals'); + this.assertBeta6ReportingRequest('refineProposals', 'refine_proposals', params); this.assertValidCompactRequest('refine_proposals', params, lifecycle, true); if (lifecycle === 'compact') { const proposalIds = params.refinements.map(refinement => refinement.proposal_id); @@ -8789,6 +8804,7 @@ export class MediaBuyLifecycleCoordinator { this.assertActive('buyProducts'); const input = record(params); const lifecycle = this.selectLifecycle('buy_products'); + this.assertBeta6ReportingRequest('buyProducts', 'buy_products', input); this.assertLegacyReferenceShapes('buyProducts', input); if (lifecycle === 'compact') { this.assertValidCompactRequest('buy_products', params, lifecycle, true); @@ -8940,6 +8956,7 @@ export class MediaBuyLifecycleCoordinator { this.assertActive('acceptProposal'); const input = record(params); const lifecycle = this.selectLifecycle('accept_proposal'); + this.assertBeta6ReportingRequest('acceptProposal', 'accept_proposal', input); this.assertLegacyReferenceShapes('acceptProposal', input); if (lifecycle === 'compact') { const proposalId = optionalString(input.proposal_id); @@ -9468,6 +9485,7 @@ export class MediaBuyLifecycleCoordinator { this.assertActive('controlMediaBuy'); const input = record(params); const lifecycle = this.selectLifecycle('control_media_buy'); + this.assertBeta6ReportingRequest('controlMediaBuy', 'control_media_buy', input); this.assertLegacyReferenceShapes('controlMediaBuy', input); const canceledControlConflicts = [ 'name', @@ -9804,6 +9822,7 @@ export class MediaBuyLifecycleCoordinator { } this.assertSharedToolAdvertised('get_media_buy_delivery'); const input = record(params); + this.assertBeta6ReportingRequest('getMediaBuyDelivery', 'get_media_buy_delivery', input); if (compareRelease(this.negotiated_version, '3.1') < 0) { this.assertCompactWireFieldsAbsent('getMediaBuyDelivery', input, [ 'include_window_breakdown', diff --git a/src/lib/media-buy/reporting-version.ts b/src/lib/media-buy/reporting-version.ts new file mode 100644 index 000000000..9e7acc017 --- /dev/null +++ b/src/lib/media-buy/reporting-version.ts @@ -0,0 +1,140 @@ +const BETA6_REPORTING_DIMENSIONS = new Set(['catalog_item', 'creative', 'keyword', 'format']); +const BETA6_SORT_METRICS = new Set([ + 'commissionable_value', + 'plays', + 'cost_per_completed_view', + 'cpm', + 'downloads', + 'units_sold', + 'new_to_brand_units', + 'viewable_rate', + 'viewable_impressions', + 'measurable_impressions', + 'viewed_seconds', + 'quartile_25', + 'quartile_50', + 'quartile_75', + 'quartile_100', +]); + +const BETA6_AVAILABLE_METRICS = new Set([ + 'measurable_impressions', + 'quartile_25', + 'quartile_50', + 'quartile_75', + 'quartile_100', + 'time_based_views', + 'viewable_impressions', + 'viewable_rate', + 'viewed_seconds', +]); + +const BETA6_REPORTING_TASKS = new Set([ + 'get_products', + 'create_media_buy', + 'update_media_buy', + 'provide_performance_feedback', + 'list_products', + 'request_proposals', + 'refine_proposals', + 'buy_products', + 'accept_proposal', + 'control_media_buy', +]); + +function record(value: unknown): Record { + return value !== null && typeof value === 'object' && !Array.isArray(value) ? (value as Record) : {}; +} + +export interface Beta6DeliveryRequestIssue { + field: string; + detail: string; +} + +/** Return the first request feature introduced by the beta.6 reporting contract. */ +export function beta6DeliveryRequestIssue(params: unknown): Beta6DeliveryRequestIssue | undefined { + const input = record(params); + if (input.requested_metrics !== undefined) { + return { field: 'requested_metrics', detail: 'requested delivery metrics' }; + } + const dimensions = record(input.reporting_dimensions); + for (const [dimension, value] of Object.entries(dimensions)) { + if (BETA6_REPORTING_DIMENSIONS.has(dimension)) { + return { + field: `reporting_dimensions.${dimension}`, + detail: `the ${dimension} delivery breakdown`, + }; + } + const settings = record(value); + if (settings.sort_direction !== undefined) { + return { + field: `reporting_dimensions.${dimension}.sort_direction`, + detail: 'delivery sort direction', + }; + } + if (settings.sort_by !== undefined && BETA6_SORT_METRICS.has(String(settings.sort_by))) { + return { + field: `reporting_dimensions.${dimension}.sort_by`, + detail: `delivery sort metric ${String(settings.sort_by)}`, + }; + } + } + return undefined; +} + +function beta6MetricIssue(value: unknown, path = ''): Beta6DeliveryRequestIssue | undefined { + if (Array.isArray(value)) { + for (let index = 0; index < value.length; index += 1) { + const issue = beta6MetricIssue(value[index], `${path}[${index}]`); + if (issue) return issue; + } + return undefined; + } + if (value === null || typeof value !== 'object') return undefined; + + const current = value as Record; + if (current.scope === 'standard' && BETA6_AVAILABLE_METRICS.has(String(current.metric_id))) { + return { + field: path ? `${path}.metric_id` : 'metric_id', + detail: `the beta.6 metric ${String(current.metric_id)}`, + }; + } + if (current.scope === 'vendor' && current.qualifier !== undefined) { + return { + field: path ? `${path}.qualifier` : 'qualifier', + detail: 'the beta.6 vendor metric qualifier', + }; + } + + for (const [key, child] of Object.entries(current)) { + const field = path ? `${path}.${key}` : key; + if ((key === 'required_metrics' || key === 'requested_metrics') && Array.isArray(child)) { + const metric = child.find(candidate => typeof candidate === 'string' && BETA6_AVAILABLE_METRICS.has(candidate)); + if (metric !== undefined) { + return { field, detail: `the beta.6 metric ${String(metric)}` }; + } + } + if (key === 'committed_metrics' && Array.isArray(child)) { + const index = child.findIndex(candidate => { + const metric = record(candidate); + return metric.scope === 'standard' && BETA6_AVAILABLE_METRICS.has(String(metric.metric_id)); + }); + if (index >= 0) { + const metric = record(child[index]); + return { + field: `${field}[${index}].metric_id`, + detail: `the beta.6 metric ${String(metric.metric_id)}`, + }; + } + } + const issue = beta6MetricIssue(child, field); + if (issue) return issue; + } + return undefined; +} + +/** Return the first beta.6-only reporting feature used by a media-buy request. */ +export function beta6ReportingRequestIssue(taskName: string, params: unknown): Beta6DeliveryRequestIssue | undefined { + if (taskName === 'get_media_buy_delivery') return beta6DeliveryRequestIssue(params); + return BETA6_REPORTING_TASKS.has(taskName) ? beta6MetricIssue(params) : undefined; +} diff --git a/src/lib/protocols/index.ts b/src/lib/protocols/index.ts index 65052259c..8b51a5da5 100644 --- a/src/lib/protocols/index.ts +++ b/src/lib/protocols/index.ts @@ -71,6 +71,7 @@ import { resolveBundleKey, toReleasePrecisionWire, validateAdcpVersionWire } fro import { buildAgentSigningContext, CAPABILITY_OP, ensureCapabilityLoaded } from '../signing/client'; import { withResponseSizeLimit } from './responseSizeLimit'; import { preparedProtocolToolCallFor } from './prepared-call-context'; +import { isAdcpVersionAtLeast } from '../utils/adcp-version-config'; import { withTransportDiagnostics, type TransportActivityHandler as TransportActivityHandlerFn, @@ -461,8 +462,8 @@ export function prepareProtocolToolCall( applyPublishedSchemaCompatibility(options.toolName, argsWithVersion, args); const effectiveBundle = typeof argsWithVersion.adcp_version === 'string' ? resolveBundleKey(argsWithVersion.adcp_version) : undefined; - const usesBeta5ApplicationRegistration = - options.serverVersion !== 'v2' && (effectiveBundle === '3.2.0-beta.5' || effectiveBundle === '3.2-beta.5'); + const usesApplicationRegistration = + options.serverVersion !== 'v2' && isAdcpVersionAtLeast(effectiveBundle, '3.2.0-beta.5'); let pushNotificationConfig: PushNotificationConfig | undefined; let applicationPushNotificationConfig: PushNotificationConfig | undefined; @@ -485,7 +486,7 @@ export function prepareProtocolToolCall( ...(options.webhookToken && { token: options.webhookToken }), }; } - if (usesBeta5ApplicationRegistration && pushNotificationConfig) { + if (usesApplicationRegistration && pushNotificationConfig) { operationId = options.operationId ?? randomUUID(); applicationPushNotificationConfig = { ...pushNotificationConfig, diff --git a/src/lib/server/create-adcp-server.ts b/src/lib/server/create-adcp-server.ts index 351bbee2a..76f28429e 100644 --- a/src/lib/server/create-adcp-server.ts +++ b/src/lib/server/create-adcp-server.ts @@ -53,7 +53,11 @@ import { toReleasePrecisionVersion, type AdcpVersion, } from '../version'; -import { isMovingAdcpPrereleaseFamilyAlias, resolveAdcpVersion } from '../utils/adcp-version-config'; +import { + isAdcpVersionAtLeast, + isMovingAdcpPrereleaseFamilyAlias, + resolveAdcpVersion, +} from '../utils/adcp-version-config'; import { getValidator, hasSchemaBundle, resolveBundleKey, getMcpProfileInputSchema } from '../validation/schema-loader'; import { TOOL_INPUT_SHAPES } from '../schemas'; import { TaskTypeValues } from '../types/enums.generated'; @@ -4265,8 +4269,7 @@ function compareAdcpRelease(left: ParsedAdcpRelease, right: ParsedAdcpRelease): const PUSH_OPERATION_ID_PATTERN = /^[A-Za-z0-9_.:-]{1,255}$/; function releaseRequiresPushOperationId(release: string): boolean { - const bundle = resolveBundleKey(release); - return bundle === '3.2.0-beta.5' || bundle === '3.2-beta.5'; + return isAdcpVersionAtLeast(resolveBundleKey(release), '3.2.0-beta.5'); } function pushOperationIdError( diff --git a/src/lib/server/decisioning/runtime/entity-hydration.generated.ts b/src/lib/server/decisioning/runtime/entity-hydration.generated.ts index ab365252f..07a397434 100644 --- a/src/lib/server/decisioning/runtime/entity-hydration.generated.ts +++ b/src/lib/server/decisioning/runtime/entity-hydration.generated.ts @@ -1,6 +1,6 @@ // Generated entity-hydration field map — do NOT edit by hand // -// Source: `schemas/cache/3.2.0-beta.5/manifest.json` + per-tool request +// Source: `schemas/cache/3.2.0-beta.6/manifest.json` + per-tool request // schemas. Every top-level `x-entity`-tagged string field on a request // schema lands here. The runtime hydrator (`from-platform.ts` → // `hydrateForTool`) walks this map plus the hand-curated diff --git a/src/lib/server/decisioning/runtime/from-platform.ts b/src/lib/server/decisioning/runtime/from-platform.ts index ec4f45633..8d01f3c3b 100644 --- a/src/lib/server/decisioning/runtime/from-platform.ts +++ b/src/lib/server/decisioning/runtime/from-platform.ts @@ -71,6 +71,7 @@ import { import type { DecisioningPlatform, RequiredPlatformsFor, RequiredCapabilitiesFor } from '../platform'; import type { ComplianceTestingCapabilities } from '../capabilities'; import { normalizeTargetingCapabilities } from '../capabilities'; +import { isAdcpVersionAtLeast } from '../../../utils/adcp-version-config'; import type { Account, ResolvedAuthInfo, ResolveContext } from '../account'; import { AccountNotFoundError, @@ -4297,17 +4298,14 @@ function buildTaskWebhookPayload( } function resolveWebhookPayloadOperationId(opts: DispatchHitlOpts, taskId: string): string { - if ( - opts.pushNotificationOperationId === undefined && - (opts.servedAdcpVersion === '3.2.0-beta.5' || opts.servedAdcpVersion === '3.2-beta.5') - ) { + if (opts.pushNotificationOperationId === undefined && isAdcpVersionAtLeast(opts.servedAdcpVersion, '3.2.0-beta.5')) { throw new AdcpError('INVALID_REQUEST', { message: 'push_notification_config.operation_id is required for webhook delivery', field: 'push_notification_config.operation_id', }); } // Older negotiated bundles predate buyer-supplied operation IDs. Preserve - // their stable compatibility value without allowing beta.5 to synthesize. + // their stable compatibility value without allowing beta.5+ to synthesize. return opts.pushNotificationOperationId ?? `${opts.tool}.${taskId}`; } diff --git a/src/lib/server/wire-spec-fields.generated.ts b/src/lib/server/wire-spec-fields.generated.ts index a43edb787..fc66bc235 100644 --- a/src/lib/server/wire-spec-fields.generated.ts +++ b/src/lib/server/wire-spec-fields.generated.ts @@ -1,6 +1,6 @@ // AUTO-GENERATED by scripts/generate-wire-spec-fields.ts. DO NOT EDIT. -// Source: schemas/cache/3.2.0-beta.5/**/*-request.json -// Generated at: 2026-08-20T19:30:56.305Z +// Source: schemas/cache/3.2.0-beta.6/**/*-request.json +// Generated at: 2026-08-24T04:19:40.835Z import type { AcquireRightsRequest, @@ -53,147 +53,147 @@ import type { * pollution or shared-state mutation of the allowlist. */ export const WIRE_SPEC_FIELDS = Object.freeze({ - /** schemas/cache/3.2.0-beta.5/brand/acquire-rights-request.json */ + /** schemas/cache/3.2.0-beta.6/brand/acquire-rights-request.json */ AcquireRightsRequest: Object.freeze({ fields: Object.freeze(["account","adcp_major_version","adcp_version","buyer","campaign","context","ext","governance_context","idempotency_key","pricing_option_id","push_notification_config","revocation_webhook","rights_id"]) as readonly string[], __type: null as unknown as AcquireRightsRequest, }), - /** schemas/cache/3.2.0-beta.5/mcp/2026-07-28/profiles/production/signals/activate-signal-request.json */ + /** schemas/cache/3.2.0-beta.6/mcp/2026-07-28/profiles/production/signals/activate-signal-request.json */ ActivateSignalRequest: Object.freeze({ fields: Object.freeze(["account","action","adcp_major_version","adcp_version","context","destinations","ext","governance_context","idempotency_key","pricing_option_id","signal_agent_segment_id"]) as readonly string[], __type: null as unknown as ActivateSignalRequest, }), - /** schemas/cache/3.2.0-beta.5/mcp/2026-07-28/media-buy/build-creative-request.json */ + /** schemas/cache/3.2.0-beta.6/mcp/2026-07-28/media-buy/build-creative-request.json */ BuildCreativeRequest: Object.freeze({ fields: Object.freeze(["account","adcp_major_version","adcp_version","brand","concept_id","config","context","creative_id","creative_manifest","evaluator","ext","governance_context","idempotency_key","include_preview","item_limit","keep_mode","macro_values","max_creatives","max_spend","max_variants","media_buy_id","message","mode","package_id","preview_inputs","preview_output_format","preview_quality","push_notification_config","quality","refine_from_build_variant_id","selection_strategy","signal_conditions","target_capability_id","target_capability_ids","target_format_id","target_format_ids","transformer_id","variant_axis"]) as readonly string[], __type: null as unknown as BuildCreativeRequest, }), - /** schemas/cache/3.2.0-beta.5/content-standards/calibrate-content-request.json */ + /** schemas/cache/3.2.0-beta.6/content-standards/calibrate-content-request.json */ CalibrateContentRequest: Object.freeze({ fields: Object.freeze(["adcp_major_version","adcp_version","artifact","context","ext","idempotency_key","standards_id"]) as readonly string[], __type: null as unknown as CalibrateContentRequest, }), - /** schemas/cache/3.2.0-beta.5/collection/create-collection-list-request.json */ + /** schemas/cache/3.2.0-beta.6/collection/create-collection-list-request.json */ CreateCollectionListRequest: Object.freeze({ fields: Object.freeze(["account","adcp_major_version","adcp_version","base_collections","brand","context","description","ext","filters","idempotency_key","name"]) as readonly string[], __type: null as unknown as CreateCollectionListRequest, }), - /** schemas/cache/3.2.0-beta.5/content-standards/create-content-standards-request.json */ + /** schemas/cache/3.2.0-beta.6/content-standards/create-content-standards-request.json */ CreateContentStandardsRequest: Object.freeze({ fields: Object.freeze(["adcp_major_version","adcp_version","calibration_exemplars","context","ext","idempotency_key","policies","registry_policy_ids","scope"]) as readonly string[], __type: null as unknown as CreateContentStandardsRequest, }), - /** schemas/cache/3.2.0-beta.5/mcp/2026-07-28/media-buy/create-media-buy-request.json */ + /** schemas/cache/3.2.0-beta.6/mcp/2026-07-28/media-buy/create-media-buy-request.json */ CreateMediaBuyRequest: Object.freeze({ fields: Object.freeze(["account","adcp_major_version","adcp_version","advertiser_industry","agency_estimate_number","artifact_webhook","bidding","brand","budget_allocation","budget_cap_timezone","context","daily_budget_cap","end_time","ext","governance_context","idempotency_key","invoice_recipient","io_acceptance","name","opportunity","pacing","packages","paused","plan_id","po_number","proposal_id","push_notification_config","reporting_webhook","start_time","total_budget"]) as readonly string[], __type: null as unknown as CreateMediaBuyRequest, }), - /** schemas/cache/3.2.0-beta.5/mcp/2026-07-28/profiles/production/property/create-property-list-request.json */ + /** schemas/cache/3.2.0-beta.6/mcp/2026-07-28/profiles/production/property/create-property-list-request.json */ CreatePropertyListRequest: Object.freeze({ fields: Object.freeze(["account","adcp_major_version","adcp_version","base_properties","brand","context","description","ext","filters","idempotency_key","name"]) as readonly string[], __type: null as unknown as CreatePropertyListRequest, }), - /** schemas/cache/3.2.0-beta.5/collection/delete-collection-list-request.json */ + /** schemas/cache/3.2.0-beta.6/collection/delete-collection-list-request.json */ DeleteCollectionListRequest: Object.freeze({ fields: Object.freeze(["account","adcp_major_version","adcp_version","context","ext","idempotency_key","list_id"]) as readonly string[], __type: null as unknown as DeleteCollectionListRequest, }), - /** schemas/cache/3.2.0-beta.5/mcp/2026-07-28/profiles/production/property/delete-property-list-request.json */ + /** schemas/cache/3.2.0-beta.6/mcp/2026-07-28/profiles/production/property/delete-property-list-request.json */ DeletePropertyListRequest: Object.freeze({ fields: Object.freeze(["account","adcp_major_version","adcp_version","context","ext","idempotency_key","list_id"]) as readonly string[], __type: null as unknown as DeletePropertyListRequest, }), - /** schemas/cache/3.2.0-beta.5/mcp/2026-07-28/media-buy/get-media-buy-delivery-request.json */ + /** schemas/cache/3.2.0-beta.6/mcp/2026-07-28/media-buy/get-media-buy-delivery-request.json */ GetMediaBuyDeliveryRequest: Object.freeze({ - fields: Object.freeze(["account","adcp_major_version","adcp_version","attribution_window","context","end_date","ext","include_package_daily_breakdown","include_window_breakdown","media_buy_ids","reporting_dimensions","start_date","status_filter","time_granularity"]) as readonly string[], + fields: Object.freeze(["account","adcp_major_version","adcp_version","attribution_window","context","end_date","ext","include_package_daily_breakdown","include_window_breakdown","media_buy_ids","reporting_dimensions","requested_metrics","start_date","status_filter","time_granularity"]) as readonly string[], __type: null as unknown as GetMediaBuyDeliveryRequest, }), - /** schemas/cache/3.2.0-beta.5/mcp/2026-07-28/media-buy/log-event-request.json */ + /** schemas/cache/3.2.0-beta.6/mcp/2026-07-28/media-buy/log-event-request.json */ LogEventRequest: Object.freeze({ fields: Object.freeze(["adcp_major_version","adcp_version","context","event_source_id","events","ext","idempotency_key","test_event_code"]) as readonly string[], __type: null as unknown as LogEventRequest, }), - /** schemas/cache/3.2.0-beta.5/mcp/2026-07-28/media-buy/provide-performance-feedback-request.json */ + /** schemas/cache/3.2.0-beta.6/mcp/2026-07-28/media-buy/provide-performance-feedback-request.json */ ProvidePerformanceFeedbackRequest: Object.freeze({ fields: Object.freeze(["adcp_major_version","adcp_version","as_of","baseline","context","creative_id","evidence","evidence_ref","ext","feedback_source","final","idempotency_key","measurement_period","media_buy_id","methodology","methodology_version","metric","metric_type","package_id","performance_index","producer","study_ref","supersedes_feedback_id","vendor"]) as readonly string[], __type: null as unknown as ProvidePerformanceFeedbackRequest, }), - /** schemas/cache/3.2.0-beta.5/governance/report-plan-outcome-request.json */ + /** schemas/cache/3.2.0-beta.6/governance/report-plan-outcome-request.json */ ReportPlanOutcomeRequest: Object.freeze({ fields: Object.freeze(["adcp_major_version","adcp_version","check_id","context","delivery","error","ext","governance_context","idempotency_key","outcome","plan_id","purchase_type","seller_response"]) as readonly string[], __type: null as unknown as ReportPlanOutcomeRequest, }), - /** schemas/cache/3.2.0-beta.5/account/report-usage-request.json */ + /** schemas/cache/3.2.0-beta.6/account/report-usage-request.json */ ReportUsageRequest: Object.freeze({ fields: Object.freeze(["adcp_major_version","adcp_version","context","ext","idempotency_key","reporting_period","usage"]) as readonly string[], __type: null as unknown as ReportUsageRequest, }), - /** schemas/cache/3.2.0-beta.5/mcp/2026-07-28/profiles/production/sponsored-intelligence/si-initiate-session-request.json */ + /** schemas/cache/3.2.0-beta.6/mcp/2026-07-28/profiles/production/sponsored-intelligence/si-initiate-session-request.json */ SIInitiateSessionRequest: Object.freeze({ fields: Object.freeze(["adcp_major_version","adcp_version","context","ext","idempotency_key","identity","intent","media_buy_id","offering_id","offering_token","placement","sponsored_context_receipt","supported_capabilities"]) as readonly string[], __type: null as unknown as SIInitiateSessionRequest, }), - /** schemas/cache/3.2.0-beta.5/mcp/2026-07-28/profiles/production/sponsored-intelligence/si-send-message-request.json */ + /** schemas/cache/3.2.0-beta.6/mcp/2026-07-28/profiles/production/sponsored-intelligence/si-send-message-request.json */ SISendMessageRequest: Object.freeze({ fields: Object.freeze(["action_response","adcp_major_version","adcp_version","context","ext","idempotency_key","message","session_id","sponsored_context_receipt"]) as readonly string[], __type: null as unknown as SISendMessageRequest, }), - /** schemas/cache/3.2.0-beta.5/account/sync-accounts-request.json */ + /** schemas/cache/3.2.0-beta.6/account/sync-accounts-request.json */ SyncAccountsRequest: Object.freeze({ fields: Object.freeze(["accounts","adcp_major_version","adcp_version","context","delete_missing","dry_run","ext","idempotency_key","push_notification_config"]) as readonly string[], __type: null as unknown as SyncAccountsRequest, }), - /** schemas/cache/3.2.0-beta.5/mcp/2026-07-28/media-buy/sync-audiences-request.json */ + /** schemas/cache/3.2.0-beta.6/mcp/2026-07-28/media-buy/sync-audiences-request.json */ SyncAudiencesRequest: Object.freeze({ fields: Object.freeze(["account","adcp_major_version","adcp_version","audiences","context","delete_missing","ext","idempotency_key"]) as readonly string[], __type: null as unknown as SyncAudiencesRequest, }), - /** schemas/cache/3.2.0-beta.5/mcp/2026-07-28/media-buy/sync-catalogs-request.json */ + /** schemas/cache/3.2.0-beta.6/mcp/2026-07-28/media-buy/sync-catalogs-request.json */ SyncCatalogsRequest: Object.freeze({ fields: Object.freeze(["account","adcp_major_version","adcp_version","catalog_ids","catalogs","context","delete_missing","dry_run","ext","idempotency_key","item_availability_queries","item_availability_updates","push_notification_config","validation_mode"]) as readonly string[], __type: null as unknown as SyncCatalogsRequest, }), - /** schemas/cache/3.2.0-beta.5/creative/sync-creatives-request.json */ + /** schemas/cache/3.2.0-beta.6/creative/sync-creatives-request.json */ SyncCreativesRequest: Object.freeze({ fields: Object.freeze(["account","adcp_major_version","adcp_version","assignment_operations","assignments","context","creative_ids","creatives","delete_missing","dry_run","ext","idempotency_key","push_notification_config","validation_mode"]) as readonly string[], __type: null as unknown as SyncCreativesRequest, }), - /** schemas/cache/3.2.0-beta.5/mcp/2026-07-28/media-buy/sync-event-sources-request.json */ + /** schemas/cache/3.2.0-beta.6/mcp/2026-07-28/media-buy/sync-event-sources-request.json */ SyncEventSourcesRequest: Object.freeze({ fields: Object.freeze(["account","adcp_major_version","adcp_version","context","delete_missing","event_sources","ext","idempotency_key"]) as readonly string[], __type: null as unknown as SyncEventSourcesRequest, }), - /** schemas/cache/3.2.0-beta.5/account/sync-governance-request.json */ + /** schemas/cache/3.2.0-beta.6/account/sync-governance-request.json */ SyncGovernanceRequest: Object.freeze({ fields: Object.freeze(["accounts","adcp_major_version","adcp_version","context","ext","idempotency_key"]) as readonly string[], __type: null as unknown as SyncGovernanceRequest, }), - /** schemas/cache/3.2.0-beta.5/governance/sync-plans-request.json */ + /** schemas/cache/3.2.0-beta.6/governance/sync-plans-request.json */ SyncPlansRequest: Object.freeze({ fields: Object.freeze(["adcp_major_version","adcp_version","context","ext","idempotency_key","plans"]) as readonly string[], __type: null as unknown as SyncPlansRequest, }), - /** schemas/cache/3.2.0-beta.5/collection/update-collection-list-request.json */ + /** schemas/cache/3.2.0-beta.6/collection/update-collection-list-request.json */ UpdateCollectionListRequest: Object.freeze({ fields: Object.freeze(["account","adcp_major_version","adcp_version","base_collections","brand","context","description","ext","filters","idempotency_key","list_id","name","webhook_url"]) as readonly string[], __type: null as unknown as UpdateCollectionListRequest, }), - /** schemas/cache/3.2.0-beta.5/content-standards/update-content-standards-request.json */ + /** schemas/cache/3.2.0-beta.6/content-standards/update-content-standards-request.json */ UpdateContentStandardsRequest: Object.freeze({ fields: Object.freeze(["adcp_major_version","adcp_version","calibration_exemplars","context","ext","idempotency_key","policies","registry_policy_ids","scope","standards_id"]) as readonly string[], __type: null as unknown as UpdateContentStandardsRequest, }), - /** schemas/cache/3.2.0-beta.5/mcp/2026-07-28/media-buy/update-media-buy-request.json */ + /** schemas/cache/3.2.0-beta.6/mcp/2026-07-28/media-buy/update-media-buy-request.json */ UpdateMediaBuyRequest: Object.freeze({ fields: Object.freeze(["account","adcp_major_version","adcp_version","bidding","budget_allocation","budget_cap_timezone","canceled","cancellation_reason","context","daily_budget_cap","end_time","ext","governance_context","idempotency_key","invoice_recipient","media_buy_id","name","new_packages","pacing","packages","paused","push_notification_config","reporting_webhook","revision","start_time","total_budget"]) as readonly string[], __type: null as unknown as UpdateMediaBuyRequest, }), - /** schemas/cache/3.2.0-beta.5/mcp/2026-07-28/profiles/production/property/update-property-list-request.json */ + /** schemas/cache/3.2.0-beta.6/mcp/2026-07-28/profiles/production/property/update-property-list-request.json */ UpdatePropertyListRequest: Object.freeze({ fields: Object.freeze(["account","adcp_major_version","adcp_version","base_properties","brand","context","description","ext","filters","idempotency_key","list_id","name","webhook_url"]) as readonly string[], __type: null as unknown as UpdatePropertyListRequest, }), - /** schemas/cache/3.2.0-beta.5/brand/update-rights-request.json */ + /** schemas/cache/3.2.0-beta.6/brand/update-rights-request.json */ UpdateRightsRequest: Object.freeze({ fields: Object.freeze(["account","adcp_major_version","adcp_version","context","end_date","ext","governance_context","idempotency_key","impression_cap","paused","pricing_option_id","push_notification_config","rights_id"]) as readonly string[], __type: null as unknown as UpdateRightsRequest, diff --git a/src/lib/types/core.generated.ts b/src/lib/types/core.generated.ts index cd322650b..6aa01dffd 100644 --- a/src/lib/types/core.generated.ts +++ b/src/lib/types/core.generated.ts @@ -1,5 +1,5 @@ -// Generated AdCP core types from official schemas v3.2.0-beta.5 -// Generated at: 2026-08-22T21:13:17.326Z +// Generated AdCP core types from official schemas v3.2.0-beta.6 +// Generated at: 2026-08-24T06:28:26.392Z // ACCOUNTCURRENCYMODE CANONICAL ENUM /** @@ -44,6 +44,18 @@ export type ActionSource = | 'system_generated' | 'other'; +// CREATIVEACTIVATIONMETHOD CANONICAL ENUM +/** + * Viewer activation mechanism a CTV or high-impact unit offers beyond a rendered impression: `qr_code` (on-glass QR to a buyer URL), `deep_link` (app or content deep link, including app-store detail pages), `push_notification` and `email` (send-to-device follow-ups with buyer-supplied copy), `tune_in` (jump to linear/virtual channel or content), `text_message` (SMS follow-up with buyer-supplied copy). Activations are engagement events, never additional impressions. Formats offering copy-bearing activations declare normative character-limited copy slots via `slots_override`: `push_notification` → `activation_message`; `text_message` → `activation_text_message`; `email` → `activation_email_subject` + `activation_email_body`. The activation URL rides `landing_page_url` unless a dedicated slot is declared. + */ +export type CreativeActivationMethod = + | 'qr_code' + | 'deep_link' + | 'push_notification' + | 'email' + | 'tune_in' + | 'text_message'; + // ADCPPROTOCOL CANONICAL ENUM /** * AdCP protocols for task categorization — referenced by tasks-list-request, webhook payloads, and other task-lifecycle surfaces. Values are kebab-case. This enum shares the same axis as supported_protocols (see /schemas/protocol/get-adcp-capabilities-response.json), which uses snake_case on the wire. Compliance testing support is declared via the `capabilities.compliance_testing` block, not as a protocol value. @@ -277,7 +289,7 @@ export type AvailabilityStatus = 'available' | 'unavailable'; // AVAILABLEMETRIC CANONICAL ENUM /** - * Standard delivery and performance metrics available for reporting + * Standard delivery and performance metrics available for reporting. Most values name flat numeric fields of `core/delivery-metrics.json`. Leaf identities address a single numeric value nested inside an object-shaped metric so it can be declared, committed, aggregated, and sorted individually: `quartile_25`/`quartile_50`/`quartile_75`/`quartile_100` resolve to `quartile_data.q1_views`–`q4_views`, and `viewable_rate`/`viewable_impressions`/`measurable_impressions`/`viewed_seconds` resolve to the same-named fields of `viewability`. The nested object remains the canonical carrier of the reported value — leaf identities never introduce duplicate flat response fields. The container tokens (`viewability`, `quartile_data`, `dooh_metrics`, and the `time_based_views` array) declare the whole nested surface at once. **Container subsumption**: in every set operation over this enum — capability declaration, `required_metrics` filtering, format `reported_metrics` intersection, and `requested_metrics` selection — a container token subsumes its leaf identities: declaring `viewability` satisfies a requirement or request for `viewable_rate`, and requesting a leaf identity selects its canonical carrier object in the response (never a flat duplicate). A leaf declaration does not imply sibling leaves or the carrier's non-numeric fields. */ export type AvailableMetric = | 'impressions' @@ -303,7 +315,16 @@ export type AvailableMetric = | 'saves' | 'profile_visits' | 'viewability' + | 'viewable_rate' + | 'viewable_impressions' + | 'measurable_impressions' + | 'viewed_seconds' | 'quartile_data' + | 'quartile_25' + | 'quartile_50' + | 'quartile_75' + | 'quartile_100' + | 'time_based_views' | 'dooh_metrics' | 'cost_per_click' | 'cost_per_completed_view' @@ -625,6 +646,12 @@ export type CreativeSortField = 'created_date' | 'updated_date' | 'name' | 'stat */ export type CreativeStatus = 'processing' | 'pending_review' | 'approved' | 'suspended' | 'rejected' | 'archived'; +// CTVADEXPERIENCE CANONICAL ENUM +/** + * IAB Tech Lab CTV Ad Portfolio experience for a format option. `menu` covers smart-TV home/menu surfaces (tiles and headline banners, OpenRTB Native plcmttype 1/3); `pause`, `screensaver`, `overlay`, `squeezeback`, and `in_scene` are the on-glass experiences the portfolio signals via OpenRTB Video with VAST NonLinearAds (AdCOM plcmt 5-9). One format option declares at most one experience; sellers offering several publish sibling format options. Each canonical format permits only the experiences in its documented matrix — pairings outside the matrix fail validation. Linear CTV video is ordinary video_vast/video_hosted and declares no experience. + */ +export type CTVAdExperience = 'menu' | 'pause' | 'screensaver' | 'overlay' | 'squeezeback' | 'in_scene'; + // DAASTTRACKINGEVENT CANONICAL ENUM /** * Tracking events for audio ads. Aligned to the IAB DAAST 1.1 `Tracking@event` enumeration from §3.2.1.7 of the spec (creativeView, start, firstQuartile, midpoint, thirdQuartile, complete, mute, unmute, pause, rewind, resume, skip, progress) — plus `close` (referenced descriptively in DAAST 1.1 §3.2.4.2 contrasting it with `skip`), and AdCP-flattened representations of the `Impression`, `Error`, and click elements (``, ``, and the click children of ``) so a single declared list can cover everything a measurement vendor wants to track. `viewable` / `notViewable` / `viewUndetermined` and `measurableImpression` / `viewableImpression` are AdCP extensions for OM-SDK Audio measurability signals (DAAST 1.1 itself does not define a `` element). DAAST 1.1 audio-incompatible video events are deliberately omitted: no `loaded`, `playerExpand` / `playerCollapse`, `fullscreen` / `exitFullscreen`, `acceptInvitation`, `adExpand` / `adCollapse`, `minimize`, `overlayViewDuration`, or `interactiveStart`. @@ -903,6 +930,7 @@ export type ErrorCode = | 'PRIVATE_FIELD_IN_PUBLIC_PLACEMENT' | 'FORMAT_PROJECTION_FAILED' | 'FORMAT_DECLARATION_DIVERGENT' + | 'FORMAT_SHAPE_PROMOTED' | 'FORMAT_DECLARATION_V1_AMBIGUOUS' | 'FORMAT_OPTION_UNRESOLVED' | 'FORMAT_DECLARATION_V1_LOSSY_MULTI_SIZE' @@ -1384,6 +1412,12 @@ export type MetroAreaSystem = 'nielsen_dma' | 'uk_itl1' | 'uk_itl2' | 'eurostat_ */ export type MoovAtomPosition = 'start' | 'end'; +// CREATIVEMOTIONLEVEL CANONICAL ENUM +/** + * Motion class of a rendered creative, mirroring AdCOM creative attributes 21 (Static Visual), 22 (Limited Motion / Cinemagraph), and 23 (Full-Motion Video). Sellers constrain the accepted level on a format option; buyers declare the level their creative carries. OpenRTB bridges map the value to `attr` on the bid and `battr` on the request. + */ +export type CreativeMotionLevel = 'static' | 'limited_motion' | 'full_motion'; + // NOTIFICATIONTYPE CANONICAL ENUM /** * Type of push notification fired by a seller agent. Media-buy-anchored notifications (`scheduled`, `final`, `delayed`, `adjusted`, `window_update`, `impairment`) fire against a media buy's `push_notification_config`. Account-anchored notifications (`creative.status_changed`, `creative.assignment_changed`, `indicators.changed`, `creative.purged`, `account.status_changed`, `product.*`, `signal.*`, `wholesale_feed.bulk_change`) fire against an account's `notification_configs[]` entries whose `event_types` include the value — these outlive any single media buy and anchor at the account. `indicators.changed` and `creative.assignment_changed` are invalidations repaired completely through `get_media_buys`; `list_creatives` may provide a bounded reverse projection. Agent-anchored notifications (`capabilities.changed`) fire against the agent-level subscriber set managed by `sync_agent_notification_configs`; they are valid before a buyer has any account. Account status changes use `account.status_changed` as an invalidation signal; receivers repair by re-reading `list_accounts`. Wholesale feed notifications carry the actual change payload in `/schemas/core/wholesale-feed-webhook.json`; product mirrors repair through `list_products` using `if_feed_version` and signal mirrors through `get_signals` using `if_wholesale_feed_version` (`get_products` remains the deprecated 3.x product fallback). Capability-change notifications carry only an invalidation payload in `/schemas/core/capabilities-changed-webhook.json`; receivers repair by re-reading `get_adcp_capabilities`. New notification types added to this enum MUST declare their anchor (media-buy, account, or agent), logical `notification_id` semantics, and repair key in the enumDescription. Sellers MUST reject `notification_configs[]` entries whose `event_types` include any media-buy-anchored or agent-anchored type, MUST reject `sync_agent_notification_configs` entries whose `event_types` include any media-buy-anchored or account-anchored type, and MUST reject `push_notification_config` registrations for persistent account-anchored or agent-anchored types. @@ -1715,7 +1749,7 @@ export type SortDirection = 'asc' | 'desc'; // SORTMETRIC CANONICAL ENUM /** - * Numeric delivery metrics available for sorting breakdown rows. Subset of delivery-metrics fields that are flat numeric values (excludes nested objects like quartile_data, dooh_metrics, viewability, by_event_type, by_action_source). + * Numeric delivery metrics available for sorting breakdown rows. Contains the flat numeric fields of `core/delivery-metrics.json` plus leaf identities that resolve to a single numeric value nested inside an object-shaped metric: `quartile_25`–`quartile_100` order rows by `quartile_data.q1_views`–`q4_views`; `viewable_rate`, `viewable_impressions`, `measurable_impressions`, and `viewed_seconds` order rows by the same-named fields of `viewability`. Sorting by a leaf identity orders on the nested value — the nested object remains the only carrier of the value in responses; no duplicate flat fields exist. Excludes object/array containers (`quartile_data`, `dooh_metrics`, `viewability`, `time_based_views`, `by_event_type`, `by_action_source`, `vendor_metric_values`) and the survey/model-based lift scalars (`incremental_sales_lift`, `brand_lift`, `foot_traffic`, `conversion_lift`, `brand_search_lift`), which are package-grain estimates rather than row-grain orderings. */ export type SortMetric = | 'impressions' @@ -1739,7 +1773,22 @@ export type SortMetric = | 'saves' | 'profile_visits' | 'engagement_rate' - | 'cost_per_click'; + | 'cost_per_click' + | 'commissionable_value' + | 'plays' + | 'cost_per_completed_view' + | 'cpm' + | 'downloads' + | 'units_sold' + | 'new_to_brand_units' + | 'viewable_rate' + | 'viewable_impressions' + | 'measurable_impressions' + | 'viewed_seconds' + | 'quartile_25' + | 'quartile_50' + | 'quartile_75' + | 'quartile_100'; // SPECIALCATEGORY CANONICAL ENUM /** @@ -2044,6 +2093,12 @@ export type VASTVersion = '2.0' | '3.0' | '4.0' | '4.1' | '4.2' | '4.3'; */ export type VideoPlacementType = 'instream' | 'accompanying_content' | 'interstitial' | 'standalone'; +// VIEWTHRESHOLDBASIS CANONICAL ENUM +/** + * Basis governing when the clock runs for a time-threshold view count. 'play_time' counts continuous playback time as measured by the serving platform's own play definition, with no independent viewability qualification (the platform-counted model behind short-form feed 2-second and 6-second video views; individual platforms may fold additional conditions into their play definition). 'in_view' counts continuous in-view time under an independent viewability standard (the IAB/MRC viewable-video model: 50% of pixels for 2 continuous seconds). Counts under different bases are not comparable and MUST NOT be summed. Extensible: additional bases (e.g., an audible-playback basis for audio verified-listen thresholds) ship explicitly in future minors. + */ +export type ViewThresholdBasis = 'play_time' | 'in_view'; + // VIEWABILITYSTANDARD CANONICAL ENUM /** * Viewability measurement standard applied to determine whether an impression qualifies as viewable. These are materially different thresholds and should not be compared across standards. @@ -2296,6 +2351,21 @@ export interface ImageAsset { * Intrinsic pixels per logical render pixel (for example `2` for a 600×500 image intended to render at 300×250). Optional because a validator can infer the ratio when the target format declares logical dimensions. When supplied, it MUST agree with both `width / logical_width` and `height / logical_height`; it is never a substitute for the intrinsic `width` and `height` fields. */ pixel_ratio?: number; + /** + * Binding used only when this image populates a `seller_rendered_stateful_display` `state_canvases` slot. It MUST match one declared `states[].state_id` (semantic validators resolve it). Omit for ordinary image slots. + */ + state_id?: string; + /** + * Binding used only when this image populates a `seller_rendered_stateful_display` `state_canvases` slot. It MUST match one breakpoint declared on the selected state (semantic validators resolve it). Omit for ordinary image slots. + */ + breakpoint_id?: string; + /** + * Normalized `[x, y]` coordinates (0–1 from top-left) of the image's visual anchor. Seller-side renderers crop toward the focal point when deriving renditions across breakpoints and aspect ratios; absent, cropping falls back to center-weighted defaults. + * + * @minItems 2 + * @maxItems 2 + */ + focal_point?: [number, number]; /** * Image file format (jpg, png, gif, webp, etc.) */ @@ -2757,7 +2827,6 @@ export interface PlatformExtensionReference { * Identifier for the metric within the vendor's vocabulary. Matches a `vendor_metrics[].metric_id` declaration on the product. */ export type VendorMetricID = string; - /** * Standard delivery metrics that can be reported at media buy, package, or creative level */ @@ -2918,7 +2987,7 @@ export interface DeliveryMetrics { */ frequency?: number; /** - * Audio/video quartile completion data. Null indicates the metric is not applicable to this package/buy (e.g. quartile data on a non-video buy). + * Audio/video quartile completion data. Null indicates the metric is not applicable to this package/buy (e.g. quartile data on a non-video buy). Individual quartiles are addressable via the leaf metric identities `quartile_25` (q1_views), `quartile_50` (q2_views), `quartile_75` (q3_views), and `quartile_100` (q4_views) for declaration, commitments, aggregates, and breakdown sorting; this object remains the canonical carrier of the values. Quartiles are player-fired events (VAST firstQuartile/midpoint/thirdQuartile/complete). `quartile_100` counts 100%-of-duration completions and is distinct from `completed_views`, which counts completions at the seller's billable view threshold (`view_duration_seconds`) when one is set. */ quartile_data?: { /** @@ -2942,6 +3011,22 @@ export interface DeliveryMetrics { */ q4_views?: number; } | null; + /** + * Time-threshold video view counts. Each entry reports views that met a continuous duration threshold under a stated basis, rather than a completion percentage (percentage-based completion is quartile_data). Thresholds of 2 and 6 seconds are RECOMMENDED cross-platform reporting points; any seller-defined threshold is permitted. One entry per (threshold_seconds, basis) pair per reporting period — sellers MUST de-duplicate before emission and MUST NOT emit the same pair twice; buyers MAY treat duplicate pairs as a seller-side conformance bug. Entries under different bases are different metrics and MUST NOT be summed (see view-threshold-basis). Primarily an autoplay/skippable-video metric (social, olv, in-feed video); completion metrics remain the currency for lean-back CTV/cinema inventory. Distinct from `views` (the single billable-threshold scalar) and from `viewability.viewed_seconds` (average in-view duration, not a threshold count). Array entries are not individually sortable in breakdown sort_by. Disclosure-grade surface: (threshold_seconds, basis) is not part of the committed-metric qualifier vocabulary, so a `committed_metrics` entry for `time_based_views` contracts the array's presence, not specific thresholds. + */ + time_based_views?: { + /** + * Continuous duration threshold in seconds an impression must meet to count as a view in this entry. + */ + threshold_seconds: number; + basis: ViewThresholdBasis; + /** + * Count of views meeting this entry's threshold and basis. + * @minimum 0 + */ + views: number; + standard?: ViewabilityStandard; + }[]; /** * DOOH-specific metrics (only included for DOOH campaigns) */ @@ -3005,7 +3090,7 @@ export interface DeliveryMetrics { }[]; }; /** - * Viewability metrics. Viewable rate should be calculated as viewable_impressions / measurable_impressions (not total impressions), since some environments cannot measure viewability. Includes `viewed_seconds` — average in-view duration — since duration is governed by the same viewability threshold (`standard`) and shares the same `measurable_impressions` denominator. Sellers SHOULD include `standard` whenever measured viewability values are reported because MRC and GroupM rows are not interchangeable. + * Viewability metrics. Viewable rate should be calculated as viewable_impressions / measurable_impressions (not total impressions), since some environments cannot measure viewability. Includes `viewed_seconds` — average in-view duration — since duration is governed by the same viewability threshold (`standard`) and shares the same `measurable_impressions` denominator. Sellers SHOULD include `standard` whenever measured viewability values are reported because MRC and GroupM rows are not interchangeable. The numeric leaves are addressable via the leaf metric identities `viewable_rate`, `viewable_impressions`, `measurable_impressions`, and `viewed_seconds` for declaration, commitments, aggregates, and breakdown sorting; this object remains the canonical carrier of the values. When a buy reports under more than one standard, contract a specific standard via the `viewability_standard` qualifier on `committed_metrics`; when the package's `committed_metrics` carry a `viewability_standard` qualifier, sellers MUST populate `standard` on reported viewability objects so reconciliation can match the qualifier. */ viewability?: { vendor?: BrandReference; @@ -3109,7 +3194,7 @@ export interface DeliveryMetrics { value?: number; }[]; /** - * Reported values for vendor-defined metrics that the product's `reporting_capabilities.vendor_metrics` declared. Each entry carries the vendor (BrandRef), the metric identifier within the vendor's vocabulary, the value, optional unit, and `measurable_impressions` as the coverage denominator — vendor measurement is rarely 100% of delivered impressions, since vendors only score impressions where their SDK fires or their panel matches. When a declared vendor metric is omitted from this array, buyers infer no measurement happened (no integration). One row per `(vendor.domain, vendor.brand_id, metric_id)` per reporting period — sellers MUST de-duplicate before emission and MUST NOT emit the same vendor metric twice; buyers MAY treat duplicate rows as a seller-side conformance bug. The structured `vendor_metric_values` array is the recommended path for vendor metrics; `additionalProperties: true` on this parent object is preserved so existing free-form vendor emissions remain conformant during migration. + * Reported values for vendor-defined metrics that the product's `reporting_capabilities.vendor_metrics` declared. Each entry carries the vendor (BrandRef), the metric identifier within the vendor's vocabulary, the value, optional unit, and `measurable_impressions` as the coverage denominator — vendor measurement is rarely 100% of delivered impressions, since vendors only score impressions where their SDK fires or their panel matches. When a declared vendor metric is omitted from this array, buyers infer no measurement happened (no integration). One row per `(vendor.domain, vendor.brand_id, metric_id, qualifier)` per reporting period — the same vendor metric MAY appear in multiple rows only when each carries a distinct qualifier (e.g., 7-day and 30-day attribution windows); sellers MUST de-duplicate before emission and MUST NOT emit two rows with the same tuple; buyers MAY treat duplicate rows as a seller-side conformance bug. The structured `vendor_metric_values` array is the recommended path for vendor metrics; `additionalProperties: true` on this parent object is preserved so existing free-form vendor emissions remain conformant during migration. */ vendor_metric_values?: VendorMetricValue[]; } @@ -3144,6 +3229,16 @@ export interface VendorMetricValue { * Number of impressions in this reporting period that the vendor was able to measure. Coverage denominator — buyers compute coverage rate as `measurable_impressions / impressions`. When absent, coverage is unspecified — buyers MUST NOT compute a coverage rate or assume full coverage. When the vendor measured zero impressions but is integrated, set to 0 explicitly. When the entry is omitted from `vendor_metric_values` entirely, the buyer infers no measurement happened (no integration). This pattern parallels `viewability.measurable_impressions` (`delivery-metrics.json#/properties/viewability`), which has handled vendor coverage in the IAS/DV/MRC ecosystem for over a decade — same convention: absence is unknown, not full. */ measurable_impressions?: number; + /** + * Optional qualifier disambiguating this row from sibling rows for the same (vendor, metric_id) — e.g., the same vendor outcome metric reported under 7-day and 30-day attribution windows. Same closed key set as `committed-metric`. When the matching `committed_metrics` entry carries a qualifier, this row MUST carry the identical qualifier so reconciliation joins on `(vendor, metric_id, qualifier)`. + */ + qualifier?: { + viewability_standard?: ViewabilityStandard; + completion_source?: CompletionSource; + attribution_methodology?: AttributionMethodology; + attribution_window?: Duration; + lift_dimension?: LiftDimension; + }; /** * Optional structured payload for vendor metrics that don't fit a single scalar — panel demographic breakouts, co-view audience composition, incremental reach + frequency + lift decompositions. Free-form; the keys and value semantics are defined by the vendor (see the vendor's `brand.json` measurement-agent docs). Buyers MUST treat this object as opaque without consulting the vendor's documentation. Vendors place any fields beyond the standard envelope (e.g., confidence intervals, panel sizes) inside this object rather than at the top level. */ @@ -4041,6 +4136,72 @@ export interface CatalogRequirement { */ system_versions?: [string, ...string[]]; } +/** + * A well-formed BCP 47 language tag used by AdCP only as language identity. Script and region may refine that identity; other valid BCP 47 subtags remain part of tag matching but do not make this a general locale-settings object. It does not determine currency, time zone, number/date formatting, market, or legal jurisdiction. The AdCP canonical wire profile requires lower-case language and variants, title-case script, and upper-case region (for example `en-US`, `zh-Hant-TW`, or `x-private`). RFC 5646 comparisons are case-insensitive and its case regularization is optional; AdCP intentionally requires this stricter single wire spelling and receivers MUST reject differently cased tags rather than silently normalizing them. The schema pattern enforces the AdCP casing profile and extension structure for commonly used tags; conforming receivers additionally validate the complete RFC 5646 grammar and registry rules. Every new AdCP field carrying BCP 47 language identity or a concrete language range MUST reference this schema instead of declaring independent string constraints. + */ +export type LanguageTag = string; + +/** + * Compact canonical creative-format declaration. Legacy named-format links are intentionally absent; params are validated against the canonical schema selected by format_kind without inlining every format union into product discovery. + */ +export interface CanonicalFormatOption { + /** + * @minLength 1 + */ + format_option_id?: string; + /** + * @pattern ^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$ + */ + publisher_domain?: string; + /** + * @minLength 1 + */ + display_name?: string; + /** + * @pattern ^https:\/\/ + */ + sample_render_url?: string; + applies_to_channels?: MediaChannel[]; + seller_preference?: 'preferred' | 'accepted' | 'discouraged'; + locale_policy?: CreativeLocalePolicy; + canonical_formats_only?: boolean; + experimental?: boolean; + format_kind: + | 'image' + | 'html5' + | 'display_tag' + | 'image_carousel' + | 'video_hosted' + | 'video_vast' + | 'audio_hosted' + | 'audio_daast' + | 'sponsored_placement' + | 'native_in_feed' + | 'responsive_creative' + | 'agent_placement' + | 'seller_rendered_stateful_display' + | 'coordinated_placements' + | 'custom'; + params: {}; + /** + * @minLength 1 + */ + format_shape?: string; + format_schema?: PlatformExtensionReference; +} +/** + * Seller-declared locale eligibility for one product or placement format declaration. Each accepted_language_ranges entry is a concrete canonical BCP 47 language range evaluated with RFC 4647 section 3.3.1 Basic Filtering: fr accepts fr, fr-CA, and fr-FR, while fr-CA accepts only fr-CA and its more-specific descendants. Ranges are ORed and wildcards are not supported. Presence is a hard assignment constraint, not a preference: at least one materialized creative locale variant must match. The seller filters the creative to eligible variants before applying buyer-declared strict Lookup, locale_fallbacks, or default behavior; buyer policy can never select a seller-ineligible variant. + */ +export interface CreativeLocalePolicy { + /** + * Concrete canonical BCP 47 language ranges accepted by this format option. RFC 4647 Basic Filtering is directional: seller range fr accepts variant fr-CA, but seller range fr-CA does not accept variant fr or fr-FR. Use zxx explicitly for language-neutral creative; und means unknown and is not a wildcard. + * + * @minItems 1 + * @maxItems 50 + */ + accepted_language_ranges: [LanguageTag, ...LanguageTag[]]; +} +// DELIVERYMETRICAGGREGATE PRIORITY CANONICAL SCHEMA /** * One cross-buy delivery aggregate partitioned by metric scope and qualifier. Row-symmetric with `package.committed_metrics` and delivery `missing_metrics` so buyers can reconcile by `(scope, metric_id, qualifier)`. */ @@ -4114,9 +4275,15 @@ export type DeliveryMetricAggregate = vendor: BrandReference; metric_id: VendorMetricID; /** - * Optional qualifier keys for vendor metrics that need disambiguation (rare today — most vendor methodologies are intrinsic to the metric definition). + * Optional qualifier keys disambiguating this vendor-metric row from sibling rows under the same (vendor, metric_id) — e.g., attribution_window on a vendor outcome metric. Same closed key set as the standard branch; new keys ship explicitly. */ - qualifier?: {}; + qualifier?: { + viewability_standard?: ViewabilityStandard; + completion_source?: CompletionSource; + attribution_methodology?: AttributionMethodology; + attribution_window?: Duration; + lift_dimension?: LiftDimension; + }; /** * Aggregated vendor-attested value. Unit semantics defined by the vendor — see the vendor's measurement-agent metric definition. */ @@ -4127,7 +4294,6 @@ export type DeliveryMetricAggregate = */ measurable_impressions?: number; }; -// CANCELLATIONPOLICY PRIORITY CANONICAL SCHEMA /** * Seller-declared cancellation terms for a product. Declares the minimum notice period required before cancellation takes effect and the cancellation fee for insufficient notice. Buyers accept these terms by creating a media buy against the product. */ @@ -5109,10 +5275,6 @@ export type FrequencyCap = { * Purchased placement selection within the product. This constrains package inventory; it is distinct from creative_assignments[].placement_refs, which only route individual creatives within the purchased set. On create, mode selected supplies the complete selected set and mode default uses the product default. On update, the surrounding targeting_overlay replacement semantics apply. */ export type PlacementSelection = SelectedPlacements | ProductDefaultPlacements; -/** - * A well-formed BCP 47 language tag used by AdCP only as language identity. Script and region may refine that identity; other valid BCP 47 subtags remain part of tag matching but do not make this a general locale-settings object. It does not determine currency, time zone, number/date formatting, market, or legal jurisdiction. The AdCP canonical wire profile requires lower-case language and variants, title-case script, and upper-case region (for example `en-US`, `zh-Hant-TW`, or `x-private`). RFC 5646 comparisons are case-insensitive and its case regularization is optional; AdCP intentionally requires this stricter single wire spelling and receivers MUST reject differently cased tags rather than silently normalizing them. The schema pattern enforces the AdCP casing profile and extension structure for commonly used tags; conforming receivers additionally validate the complete RFC 5646 grammar and registry rules. Every new AdCP field carrying BCP 47 language identity or a concrete language range MUST reference this schema instead of declaring independent string constraints. - */ -export type LanguageTag = string; /** * Assignment of a creative asset to a package with optional rotation and placement routing. Used in create_media_buy and update_media_buy requests. Buyers identify the stored creative with `creative_id` only. A generic `id` alias, if present due to adapter-internal payload reuse, is not an AdCP identifier and sellers MUST ignore it on input. Note: sync_creatives does not support package rotation, placement_refs, or placement_ids - use create/update_media_buy for package-level trafficking controls. */ @@ -5156,6 +5318,7 @@ export type CreativeAssignment = { * Creative asset for upload to library — supports static assets, generative formats, and third-party snippets. Identifies which format this creative conforms to via EITHER a legacy `format_id` (structured `{agent_url, id}`) OR a 3.1+ `format_kind` (canonical format name), with optional `format_option_ref` when the target product needs disambiguation. Mutually exclusive — see the `oneOf` at the schema root. */ export type CreativeAsset = { +} & { /** * Unique identifier for the creative. Stable across legacy named-format and 3.1+ canonical-format paths — a creative registered against `format_id` retains the same `creative_id` when later viewed through a canonical-format flatten. */ @@ -5173,6 +5336,12 @@ export type CreativeAsset = { assets: { [k: string]: AssetVariant | AssetVariant[]; }; + /** + * Component-addressed canonical asset maps for `coordinated_placements`. Keys match coordinated component IDs. This field is preserved by creative-library sync and list readback; it MUST be absent for every other format kind. + */ + component_assets?: { + [k: string]: CreativeAssets | undefined; + }; /** * Preview contexts for generative formats - defines what scenarios to generate previews for */ @@ -5242,6 +5411,8 @@ export type CanonicalFormatKind = | 'native_in_feed' | 'responsive_creative' | 'agent_placement' + | 'seller_rendered_stateful_display' + | 'coordinated_placements' | 'custom'; /** * 3.1+ format-option path, optional. Structured format option reference matching one of the target product's `format_options[]` declarations. Publisher-catalog-backed options match by `{ scope: "publisher", publisher_domain, format_option_id }`; product-local options match by `{ scope: "product", format_option_id }`. Required when the target product has multiple `format_options` entries sharing the same `format_kind`; optional when `format_kind` alone routes the creative to a single declaration. Product-scoped refs require an enclosing target product/package context. @@ -6676,6 +6847,10 @@ export interface URLAsset { * Description of what this URL points to */ description?: string; + /** + * Binding used only when this URL populates a `seller_rendered_stateful_display` `state_click_urls` slot. It MUST match one declared `states[].state_id` (semantic validators resolve it); at most one entry per state. Omit for ordinary URL slots. + */ + state_id?: string; provenance?: Provenance; } /** @@ -6925,6 +7100,14 @@ export interface CardAsset { platform_extensions?: PlatformExtensionReference[]; provenance?: Provenance; } +/** + * Map of canonical asset-group or legacy asset identifiers to supplied creative assets. Values are either a single discriminated asset or a non-empty repeatable asset array. + * + * This interface was referenced by `undefined`'s JSON-Schema definition + * via the `patternProperty` "^[a-z][a-z0-9_]*$". + */ +export interface CreativeAssets { +} /** * An industry-standard or market-specific identifier for an advertising creative (e.g., Ad-ID, ISCI, Clearcast clock number, IDcrea). These identifiers are managed by external registries or clearance bodies and used across the supply chain to track and reference specific creative assets. Add a PR to extend creative-identifier-type when another shared identifier scheme needs first-class support. */ @@ -7108,6 +7291,8 @@ export type WarningAffectedResource = * Static image creative format. Slots: `image_main` (image asset, file or hosted URL), optional `headline` (text), `body_text` (text), `cta` (text/enum), `landing_page_url` (url). Tracking model: impression pixel + click URL via universal_macros, with optional viewability pixel. Distinct from `html5` (interactive bundles) and `display_tag` (third-party served). AR/dimensions narrow to specific sizes via product parameters — covers IAB display sizes (300x250, 728x90, 970x250, etc.) without a separate iab_size enum. */ export type CanonicalFormatImage = SizeModeMutex & { + motion_level?: 'static' | 'limited_motion'; +} & { /** * When true, this canonical or seller narrowing may not work as declared. Adopters SHOULD preflight it with validate_input or in a sandbox and SHOULD NOT route production budget without testing; experimental status never makes the deprecated v1 path preferable. Drivers include unsettled spec shape, an adopter runtime gap, and custom shapes awaiting promotion. This replaces the earlier status plus runtime_status axes. Sellers SHOULD set experimental whenever a canonical or declaration is not production-ready. */ @@ -7119,7 +7304,7 @@ export type CanonicalFormatImage = SizeModeMutex & { /** * Whether this canonical has any v1 named-format equivalent. `true` (default) — the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1→v2 projection via `v1-canonical-mapping.json` is meaningful. `false` — the canonical is inherently new in v2 and has no v1 form; v1's `list_creative_formats` couldn't express it because the underlying concept (algorithmic surface composition, AI-surface mentions, retail-media catalog placements, multi-card carousels) didn't exist as a v1 named-format archetype. * - * Lets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The 4 inherently-v2 canonicals at 3.1 GA: `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`. + * Lets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The six inherently-v2 canonicals in 3.2 are `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`, `seller_rendered_stateful_display`, and `coordinated_placements`. */ v1_translatable?: boolean; /** @@ -7337,6 +7522,12 @@ export type CanonicalFormatImage = SizeModeMutex & { * Whether the product accepts buyer-uploaded assets. When `rejected`, the buyer cannot ship pre-rendered bytes directly — they must use build_creative (or sync_creatives with brief inputs or reference assets) so the seller produces or resolves the asset. Combined with `asset_source`, lets a product declare 'I produce assets from briefs and refuse buyer uploads' (asset_source=`seller_pre_rendered_from_brief`, buyer_asset_acceptance=`rejected`) or 'I accept existing post references, not uploaded bytes' (asset_source=`publisher_owned_reference`, buyer_asset_acceptance=`rejected`). */ buyer_asset_acceptance?: 'accepted' | 'rejected'; + ctv_ad_experience?: CTVAdExperience; + motion_level?: CreativeMotionLevel; + /** + * Viewer activation mechanisms this option offers (e.g. `qr_code` on a pause frame). Activations are engagement events, not impressions. + */ + activation_methods?: CreativeActivationMethod[]; }; /** * Exactly one of: (a) fixed (`width` + `height` both set), (b) multi-size (`sizes` set), (c) responsive (any of `min_width`/`max_width`/`min_height`/`max_height` set), (d) none (no size constraint declared — accepts any dimensions). Combining modes (e.g., `width` + `sizes`) is rejected at schema layer; same rule on `html5` and `display_tag` canonicals. @@ -7413,7 +7604,6 @@ export type DownstreamConnectionRequirement = { */ expires_at?: string; }; - export interface Fixed { /** * Logical render width in pixels — use for fixed-size slots (e.g., a 300×250 IAB MREC). When `pixel_ratios` is absent, the required image asset width is the same value (1x). When `pixel_ratios` is present, an accepted asset's intrinsic width is `width × pixel_ratio`. For multi-size flexible slots, use `sizes[]`; for responsive slots, use the min/max fields. The three size modes are mutually exclusive. @@ -7480,7 +7670,7 @@ export type CanonicalFormatHTML5Banner = SizeModeMutex & { /** * Whether this canonical has any v1 named-format equivalent. `true` (default) — the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1→v2 projection via `v1-canonical-mapping.json` is meaningful. `false` — the canonical is inherently new in v2 and has no v1 form; v1's `list_creative_formats` couldn't express it because the underlying concept (algorithmic surface composition, AI-surface mentions, retail-media catalog placements, multi-card carousels) didn't exist as a v1 named-format archetype. * - * Lets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The 4 inherently-v2 canonicals at 3.1 GA: `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`. + * Lets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The six inherently-v2 canonicals in 3.2 are `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`, `seller_rendered_stateful_display`, and `coordinated_placements`. */ v1_translatable?: boolean; /** @@ -7716,7 +7906,7 @@ export type CanonicalFormatDisplayTag = SizeModeMutex & { /** * Whether this canonical has any v1 named-format equivalent. `true` (default) — the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1→v2 projection via `v1-canonical-mapping.json` is meaningful. `false` — the canonical is inherently new in v2 and has no v1 form; v1's `list_creative_formats` couldn't express it because the underlying concept (algorithmic surface composition, AI-surface mentions, retail-media catalog placements, multi-card carousels) didn't exist as a v1 named-format archetype. * - * Lets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The 4 inherently-v2 canonicals at 3.1 GA: `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`. + * Lets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The six inherently-v2 canonicals in 3.2 are `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`, `seller_rendered_stateful_display`, and `coordinated_placements`. */ v1_translatable?: boolean; /** @@ -8124,7 +8314,7 @@ export interface CanonicalFormatHostedVideo { /** * Whether this canonical has any v1 named-format equivalent. `true` (default) — the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1→v2 projection via `v1-canonical-mapping.json` is meaningful. `false` — the canonical is inherently new in v2 and has no v1 form; v1's `list_creative_formats` couldn't express it because the underlying concept (algorithmic surface composition, AI-surface mentions, retail-media catalog placements, multi-card carousels) didn't exist as a v1 named-format archetype. * - * Lets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The 4 inherently-v2 canonicals at 3.1 GA: `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`. + * Lets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The six inherently-v2 canonicals in 3.2 are `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`, `seller_rendered_stateful_display`, and `coordinated_placements`. */ v1_translatable?: boolean; /** @@ -8333,10 +8523,11 @@ export interface CanonicalFormatHostedVideo { * Whether the product accepts buyer-uploaded video. When `rejected`, the buyer cannot ship a video asset directly — they must use build_creative, sync_creatives with brief inputs, or sync_creatives with an accepted reference asset so the seller produces or resolves the video. */ buyer_asset_acceptance?: 'accepted' | 'rejected'; + ctv_ad_experience?: CTVAdExperience; } // CANONICALFORMATVASTVIDEO PRIORITY CANONICAL SCHEMA /** - * VAST-tag-delivered video creative. Slot: `vast_tag` (vast asset, URL or inline XML, VAST 2.x-4.x). Tracking model: VAST events inherent to the spec — `impression`, `firstQuartile`, `midpoint`, `thirdQuartile`, `complete`, `start`, `pause`, `resume`, `mute`, `unmute`, `expand`, `collapse`, `fullscreen`, `creativeView`, `clickTracking`, `error`. VPAID interactivity via `vpaid_enabled: true` flag. SIMID extensions for interactive video supported as VAST extensions. Orientation is a parameter (vertical / horizontal / square). Distinct from `video_hosted` (direct file with external tracking). + * VAST-tag-delivered video creative. Slot: `vast_tag` (vast asset, URL or inline XML, VAST 2.x-4.x). Tracking model: VAST events inherent to the spec — `impression`, `firstQuartile`, `midpoint`, `thirdQuartile`, `complete`, `start`, `pause`, `resume`, `mute`, `unmute`, `expand`, `collapse`, `fullscreen`, `creativeView`, `clickTracking`, `error`. VPAID interactivity via `vpaid_enabled: true` flag. SIMID is carried by the first-class VAST 4.1+ `` element on Linear creatives. Orientation is a parameter (vertical / horizontal / square). Distinct from `video_hosted` (direct file with external tracking). */ export interface CanonicalFormatVASTVideo { /** @@ -8350,7 +8541,7 @@ export interface CanonicalFormatVASTVideo { /** * Whether this canonical has any v1 named-format equivalent. `true` (default) — the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1→v2 projection via `v1-canonical-mapping.json` is meaningful. `false` — the canonical is inherently new in v2 and has no v1 form; v1's `list_creative_formats` couldn't express it because the underlying concept (algorithmic surface composition, AI-surface mentions, retail-media catalog placements, multi-card carousels) didn't exist as a v1 named-format archetype. * - * Lets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The 4 inherently-v2 canonicals at 3.1 GA: `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`. + * Lets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The six inherently-v2 canonicals in 3.2 are `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`, `seller_rendered_stateful_display`, and `coordinated_placements`. */ v1_translatable?: boolean; /** @@ -8487,7 +8678,7 @@ export interface CanonicalFormatVASTVideo { vpaid_enabled?: boolean; vpaid_version?: '1.0' | '2.0'; /** - * Whether IAB SIMID interactive video extensions are supported. + * Whether the seller accepts IAB SIMID through `` on a Linear VAST creative. SIMID is not a generic VAST extension and cannot be serialized under NonLinearAds; every `ctv_ad_experience` profile therefore forbids `true`. */ simid_supported?: boolean; /** @@ -8516,7 +8707,17 @@ export interface CanonicalFormatVASTVideo { */ max_height?: number; /** - * Whether the VAST creative must be linear (non-skippable in-stream). + * Required VAST creative class: `linear` (in-stream Linear), `nonlinear` (NonLinearAds overlay-class), or `either`. Supersedes `linear_required`; when both are present `creative_type` wins, and validators treat `linear_required: true` with no `creative_type` as `linear`. + */ + creative_type?: 'linear' | 'nonlinear' | 'either'; + ctv_ad_experience?: CTVAdExperience; + motion_level?: CreativeMotionLevel; + /** + * Viewer activation mechanisms this option offers. Activations are engagement events, not impressions. + */ + activation_methods?: CreativeActivationMethod[]; + /** + * Whether the VAST creative must be linear (non-skippable in-stream). Superseded by `creative_type`; retained for pre-3.2 declarations. */ linear_required?: boolean; /** @@ -8547,7 +8748,7 @@ export interface CanonicalFormatHostedAudio { /** * Whether this canonical has any v1 named-format equivalent. `true` (default) — the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1→v2 projection via `v1-canonical-mapping.json` is meaningful. `false` — the canonical is inherently new in v2 and has no v1 form; v1's `list_creative_formats` couldn't express it because the underlying concept (algorithmic surface composition, AI-surface mentions, retail-media catalog placements, multi-card carousels) didn't exist as a v1 named-format archetype. * - * Lets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The 4 inherently-v2 canonicals at 3.1 GA: `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`. + * Lets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The six inherently-v2 canonicals in 3.2 are `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`, `seller_rendered_stateful_display`, and `coordinated_placements`. */ v1_translatable?: boolean; /** @@ -8750,7 +8951,7 @@ export interface CanonicalFormatDAASTAudio { /** * Whether this canonical has any v1 named-format equivalent. `true` (default) — the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1→v2 projection via `v1-canonical-mapping.json` is meaningful. `false` — the canonical is inherently new in v2 and has no v1 form; v1's `list_creative_formats` couldn't express it because the underlying concept (algorithmic surface composition, AI-surface mentions, retail-media catalog placements, multi-card carousels) didn't exist as a v1 named-format archetype. * - * Lets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The 4 inherently-v2 canonicals at 3.1 GA: `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`. + * Lets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The six inherently-v2 canonicals in 3.2 are `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`, `seller_rendered_stateful_display`, and `coordinated_placements`. */ v1_translatable?: boolean; /** @@ -9092,6 +9293,7 @@ export interface CanonicalFormatSponsoredPlacementRetailMediaCatalogDriven { | 'seller_pre_rendered_from_brief' | 'seller_human_designed' | 'agent_synthesized'; + ctv_ad_experience?: CTVAdExperience; } // CANONICALFORMATNATIVEINFEED PRIORITY CANONICAL SCHEMA /** @@ -9107,7 +9309,7 @@ export interface CanonicalFormatSponsoredPlacementRetailMediaCatalogDriven { * - Catalog-driven retail-media (Amazon SP, Criteo SP, CitrusAd SP) — use `sponsored_placement` (requires `source_catalog`). * - Algorithmic surface that picks from a buyer-supplied asset pool (Google PMax, Meta Advantage+) — use `responsive_creative`. * - Multi-card carousel — use `image_carousel`. - * - Video-first native units where the asset is a hosted video file — use `video_hosted` with `applies_to_channels: ["native"]`. + * - Video-first native units where the asset is a hosted video file — use `video_hosted` with `applies_to_channels: ["native"]`. (Distinct from the CTV menu profile: a menu hero remains this canonical because the platform assembles the full asset bundle and the video rides the Native 1.2 `vasttag` video asset, playing on focus rather than being the unit itself.) * * Distinct from `sponsored_placement` along the catalog axis: native_in_feed is asset-bundle composition; sponsored_placement is catalog-row composition. A buyer agent reading `format_kind: native_in_feed` knows to assemble title + image + body + CTA; reading `format_kind: sponsored_placement` knows to attach a catalog feed. */ @@ -9153,7 +9355,7 @@ export interface CanonicalFormatNativeInFeed { */ synthesis_nondeterministic?: boolean; /** - * Default slot shape for native_in_feed. Mirrors IAB OpenRTB Native 1.2 asset types. Products MAY override (`slots_override` on the projection ref) to narrow per-slot limits (`max_chars` on title/body) or remove unused slots (a content-recommendation slot that doesn't display an icon). + * Default slot shape for native_in_feed. Mirrors IAB OpenRTB Native 1.2 asset types, including the Native video asset: `video` carries a VAST document (the Native 1.2 `vasttag` field) for video-bearing native units such as CTV menu heroes with focus-triggered playback. Products MAY override (`slots_override` on the projection ref) to narrow per-slot limits (`max_chars` on title/body) or remove unused slots (a content-recommendation slot that doesn't display an icon). */ slots?: { /** @@ -9246,6 +9448,20 @@ export interface CanonicalFormatNativeInFeed { * Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative). */ production_window_business_days?: number; + ctv_ad_experience?: CTVAdExperience; + /** + * Menu surface variant, mapping to OpenRTB Native `plcmttype` 1 (tile/feed) and 3 (headline banner). Valid only with `ctv_ad_experience: "menu"`. + */ + menu_placement?: 'tile' | 'headline_banner'; + /** + * What happens when the viewer's remote focus lands on the unit. `autoplay_*` requires a `video` asset; playback method maps to AdCOM playbackmethod on OpenRTB bridges. Valid only with `ctv_ad_experience: "menu"`. + */ + focus_behavior?: 'none' | 'autoplay_muted' | 'autoplay_sound'; + motion_level?: CreativeMotionLevel; + /** + * Viewer activation mechanisms this option offers (QR, deep link, send-to-device). Activations are engagement events, not impressions. + */ + activation_methods?: CreativeActivationMethod[]; /** * Maximum character length for the title slot. IAB native typical: 25 (short) to 90 (long). Buyer agents SHOULD validate ship-time title length against this. * @minimum 1 @@ -9728,128 +9944,1063 @@ export interface CanonicalFormatAgentPlacementAISurfaceSponsoredPlacement { */ disclosure_required?: boolean; } -// VALIDATEPROPERTYDELIVERYREQUEST PRIORITY CANONICAL SCHEMA -/** - * Account that owns the list. Required when the authenticated agent has access to multiple accounts; optional otherwise. - */ -export type AccountReference = - | { - /** - * Seller-assigned account identifier. For upstream-managed account namespaces, this value comes from list_accounts; for seller-defined namespaces without a list_accounts surface, it is supplied out-of-band. Buyer-declared account sellers MAY echo account_id from sync_accounts as an internal handle, but they MUST continue accepting the account's current natural-key AccountRef on subsequent calls. A former key tombstoned by identity reconciliation returns ACCOUNT_MOVED to authorized callers. - */ - account_id: string; - } - | { - brand: BrandReference; - /** - * Domain of the entity operating on the brand's behalf. When the brand operates directly, this is the brand's domain. - */ - operator: string; - operator_unit?: OperatorUnit; - /** - * Immutable ISO 4217 transaction currency when the seller's advertiser object is currency-bound. When present, this is part of the natural account key and media buys on the account MUST use it. Omit when currency is selected independently per media buy. - */ - currency?: string; - /** - * Immutable account timezone. Include it in the natural key when get_adcp_capabilities.account.timezone declares account_fixed with buyer_selected; omit it for seller_fixed or seller_assigned accounts. - */ - timezone?: string; - /** - * When true, references the sandbox account for this brand/operator pair. Defaults to false (production account). - */ - sandbox?: boolean; - }; +// CANONICALFORMATSELLERRENDEREDSTATEFULDISPLAY PRIORITY CANONICAL SCHEMA /** - * Request payload for validate_property_delivery task. Validates delivery records against a property list to determine compliance. + * Seller-rendered display unit whose declaration is an executable template contract: buyer-known visual states, explicit transitions, breakpoint canvases, and per-state slot bindings. The seller owns the runtime; `supply_mode` declares which end the buyer feeds. For machine-rendered `components` and `rendered_canvases` supply, sellers MUST support `preview_creative` returning every state × breakpoint from a candidate manifest. `layered_source` instead follows the asynchronous seller-production preview path after the declared production window. `composition_model: deterministic` describes serving the finished states, not instant derivation from layered source. Buyer-executable HTML/MRAID is `html5`, a buyer-delivered tag is `display_tag`, arbitrary games/hotspots/scripts remain `custom`, and per-impression algorithmic assembly is `responsive_creative`. */ -export interface ValidatePropertyDeliveryRequest { - /** - * Release-precision AdCP version (VERSION.RELEASE, e.g. "3.0", "3.1", "3.1-beta"). On a request: the buyer's release pin — the seller validates against its supported_versions and returns VERSION_UNSUPPORTED on cross-major mismatch, or downshifts to the highest supported release within the same major. On a response: the release the seller actually served — clients SHOULD validate the response against that release's schema, not against their pin. Patches are not negotiated; surface them as build_version on capabilities for operational visibility. When omitted, falls back to adcp_major_version (deprecated) or server default. Buyers SHOULD emit both adcp_version and adcp_major_version through 3.x to remain compatible with sellers that only read the legacy field. NORMALIZATION: SDKs that read full-semver values from bundle metadata (e.g. ComplianceIndex.published_version = "3.1.0-beta.1") MUST normalize to release-precision ("3.1-beta.1") before emitting on the wire — meta-field values are NOT valid wire values. - */ - adcp_version?: string; - /** - * @deprecated - * DEPRECATED in favor of adcp_version (release-precision string). Servers MUST continue to honor this field through 3.x. Removed in 4.0. Original semantics: the AdCP major version the buyer's payloads conform to. Sellers validate against their supported major_versions and return VERSION_UNSUPPORTED if unsupported. When omitted, the seller assumes its highest supported version. - */ - adcp_major_version?: number; - /** - * ID of the property list to validate against - */ - list_id: string; - account?: AccountReference; - /** - * Delivery records to validate. Each record represents impressions delivered to a property identifier. - */ - records: DeliveryRecord[]; +export interface CanonicalFormatSellerRenderedStatefulDisplay { /** - * Include compliant records in results (default: only return non_compliant, unmodeled, and unidentified) + * Experimental in AdCP 3.2 while the creative working group gathers implementation evidence across premium web and mobile/app sellers. */ - include_compliant?: boolean; - context?: ContextObject; - ext?: ExtensionObject; -} -/** - * Optional operator-owned business unit, agency seat, or platform account. Only id participates in the natural account key; name is mutable display metadata. - */ -export interface OperatorUnit { + experimental?: boolean; /** - * Stable identifier assigned by the operator. Numeric platform IDs and durable slugs are both valid. Scoped by the enclosing operator domain. + * When true, this canonical (or a seller's specific narrowing of it) is going away. Existing adopters are supported through the deprecation cycle; new adoption is discouraged. Pair with `migration_target_version` to indicate when the canonical is expected to be removed. Distinct from `experimental`: an experimental canonical may stabilize and stop being experimental; a deprecated canonical is on a sunset path. */ - id: string; + deprecated?: boolean; /** - * Human-readable seat or business-unit name, such as Nova EMEA. This label may change and is not part of the natural account key. + * No v1 named-format equivalent can express multiple seller-rendered states and their breakpoint bindings. */ - name?: string; -} -/** - * A single delivery record representing impressions served to a property identifier. Used as input to validate_property_delivery. - */ -export interface DeliveryRecord { - identifier: Identifier; + v1_translatable?: boolean; /** - * Number of impressions delivered to this identifier + * AdCP MAJOR.MINOR version that introduced this canonical (e.g., '3.1', '3.2'). Lets adopters reason about minimum protocol version requirements when consuming a format declaration. Patch precision is intentionally rejected — canonicals are introduced at minor-version boundaries. */ - impressions: number; + since_version?: string; /** - * Optional client-provided ID for correlating results back to source data + * AdCP MAJOR.MINOR version by which the working group expects this canonical to stabilize, surface a breaking revision, or (when `deprecated: true`) be removed. Patch precision is intentionally rejected — canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown'). */ - record_id?: string; + migration_target_version?: string; /** - * URL of the sales agent that sold this inventory. If provided, authorization is validated against the property's adagents.json. + * Whether the surface composes deterministically (buyer can predict per-slot rendering — sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing — responsive_creative, agent_placement). */ - sales_agent_url?: string; - ext?: ExtensionObject; -} -/** - * The property identifier where impressions were delivered - */ -export interface Identifier { - type: PropertyIdentifierTypes; + composition_model?: 'deterministic' | 'algorithmic'; /** - * The identifier value. For domain type: 'example.com' matches base domain plus www and m subdomains; 'edition.example.com' matches that specific subdomain; '*.example.com' matches ALL subdomains but NOT base domain + * When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent. */ - value: string; -} -// VALIDATEPROPERTYDELIVERYRESPONSE PRIORITY CANONICAL SCHEMA -/** - * Response payload for validate_property_delivery task. Returns aggregate compliance statistics and per-record validation results. - */ -export interface ValidatePropertyDeliveryResponse { + provenance_required?: boolean; /** - * Session/conversation identifier for tracking related operations across multiple task invocations. Managed by the protocol layer to maintain conversational context. Distinct from `context` (per-request opaque echo, see below). + * Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly. + * + * **Collision precedence (normative).** When two or more `platform_extensions[]` entries on the same declaration extend the same target (e.g., both extend `tracking`) with overlapping field names, **array order is authoritative — later entries override earlier ones on a per-field basis** (last-in-array-wins). SDKs MUST surface the overlap via the `errors[]` array on the `get_products` response with a structured code (`FORMAT_DECLARATION_DIVERGENT` is appropriate when the overlap appears across dual-emitted shapes; a producer-self-emitted overlap on a single declaration SHOULD use the same code with `error.details: { collision_kind: "platform_extension_field", target, overlapping_fields, winning_extension_uri }`). Producers SHOULD avoid the collision by emitting one extension per target or by partitioning fields across extensions; the deterministic precedence is for last-resort consistency across SDK implementations, not a sanctioned merging strategy. */ - context_id?: string; - context?: ContextObject; + platform_extensions?: PlatformExtensionReference[]; /** - * Unique identifier for tracking asynchronous operations. Present when a task requires extended processing time. Used to query task status and retrieve results when complete. + * When true, the format's production pipeline is genuinely nondeterministic — the platform cannot guarantee that synthesis from a given input set produces in-spec output. Veo / Sora / Runway-class generative video, and other AI-synthesis flows where output dimensions, duration, or quality vary per run. Implies a different validation contract: predictive `validate_input` is impossible; the platform's own post-synthesis QA loop applies; if the QA loop exhausts without producing a valid artifact, `build_creative` returns task_failed with a synthesis_failed reason. Distinct from `composition_model` (which describes how the surface composes per-slot rendering, not whether synthesis is deterministic). When false or absent, the format's production is predictable enough that `validate_input` can predict output properties from input properties. + * + * **Compatibility with `asset_source` / `item_production_model`**: `synthesis_nondeterministic: true` MAY pair with any of `seller_pre_rendered_from_brief`, `seller_human_designed`, or `agent_synthesized` (the QA loop is concept-level, not source-specific — 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error. */ - task_id?: string; - status: TaskStatus; + synthesis_nondeterministic?: boolean; /** - * Human-readable summary of the task result. Provides natural language explanation of what happened, suitable for display to end users or for AI agent comprehension. Generated by the protocol layer based on the task response. + * Default manifest slots; which are consumed depends on `supply_mode`. `state_canvases` images MUST carry `state_id` and `breakpoint_id`, and `state_click_urls` entries MUST carry `state_id` (semantic validators resolve the bindings). Component images SHOULD carry `focal_point` for deterministic seller cropping. `landing_page_url` is the default destination (see `clickthrough`). `font_files` MUST contain only buyer-licensed fonts; publisher-proprietary fonts never travel in manifests. Only image, video, text, url, zip, and pixel_tracker slot asset types are accepted — executable types (javascript, html, css, webhook) are rejected even via `slots` overrides. */ - message?: string; - /** + slots?: { + /** + * Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings. + */ + asset_group_id: string; + /** + * Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `published_post` is an existing-post reference asset, not uploaded media bytes and not a catalog row. `card` is the multi-card carousel element type (see card-asset.json). `pixel_tracker` / `vast_tracker` / `daast_tracker` are the renderer-fired measurement-tracker primitives — see `/schemas/core/assets/pixel-tracker-asset.json` and the VAST / DAAST tracker schemas. `object` is a last-resort fallback for structured non-asset inputs that don't fit any primitive asset_type — prefer specific types whenever possible. + */ + asset_type: + | 'image' + | 'video' + | 'audio' + | 'text' + | 'markdown' + | 'url' + | 'html' + | 'css' + | 'javascript' + | 'vast' + | 'daast' + | 'webhook' + | 'brief' + | 'catalog' + | 'published_post' + | 'zip' + | 'card' + | 'object' + | 'pixel_tracker' + | 'vast_tracker' + | 'daast_tracker'; + /** + * Whether this slot is required for a valid manifest. + */ + required?: boolean; + /** + * Minimum count for repeatable / pool slots. + */ + min?: number; + /** + * Maximum count for repeatable / pool slots. + */ + max?: number; + /** + * Per-slot character limit. Valid only when `asset_type` is `text`, `markdown`, or `brief`. Mutually exclusive with `max_size_kb` (which applies to binary asset types). Schema enforces via if/then so a producer can't set both on the same slot. + */ + max_chars?: number; + /** + * Per-slot file size limit in kilobytes. Valid only when `asset_type` is `image`, `video`, `audio`, or `zip`. Mutually exclusive with `max_chars` (which applies to text asset types). Schema enforces via if/then so a producer can't set both on the same slot. + */ + max_size_kb?: number; + /** + * Accepted intrinsic-pixel densities for this image-bearing slot. Valid when `asset_type` is `image`, and on a `card` slot where it constrains each card's image media (video media is unaffected). This makes density available to every canonical carrying image assets (native, carousel, responsive, companion images, and image itself), not only `format_kind: image`. When the image canonical also declares top-level `params.pixel_ratios`, the effective set is the intersection; an empty intersection is invalid. One matching asset satisfies the slot unless `required_pixel_ratios` requires rendition coverage. + */ + pixel_ratios?: number[]; + /** + * Required density coverage for an image rendition set. Valid only when `asset_type` is `image` and `pixel_ratios` is also declared. Every value MUST appear in the effective accepted set after intersecting any top-level image `params.pixel_ratios`, and the manifest slot value MUST be an array containing exactly one matching image rendition for each required ratio. Other accepted ratios remain optional. For example, `pixel_ratios: [1, 1.5, 2]` with `required_pixel_ratios: [1, 2]` requires the 1x and 2x renditions while making 1.5x optional. SDKs enforce intersection, subset, coverage, and duplicate-ratio rules because JSON Schema draft-07 cannot express them generically. + */ + required_pixel_ratios?: number[]; + /** + * When `asset_group_id` is `logo`, renderer-facing brand.json logo slots acceptable for this format slot. Producers selecting from brand.json SHOULD prefer `logos[]` entries whose `slots[]` intersects this list, then apply `visual_guidelines.logo_usage_rules[]`. + */ + logo_slots?: LogoSlot[]; + /** + * Subset of `logo_slots` for which this format expects explicit logo coverage. A manifest or brand-derived logo pool SHOULD include at least one usable logo for each required slot; if coverage is missing, builders SHOULD surface a validation warning or approval mapping instead of guessing from prose. + */ + required_logo_slots?: LogoSlot[]; + /** + * Human-readable description of what the slot expects from the buyer. + */ + description?: string; + /** + * Dispatch hint for `build_creative` and v1↔v2 wire translators: when `true`, the slot's value is consumed as INPUT to a production step (host-read script, brief copy fed to generative synthesis, catalog feed driving per-SKU rendering) and is not rendered verbatim. When `false` (default), the slot's value is rendered verbatim on the placement (image bytes, video file, display tag). + * + * Motivates the v1↔v2 dispatch table: pre-v2 buyers shipped production-consumed inputs separately in a `inputs` map on the build_creative request; v2 collapses inputs and rendered assets into a single `assets` map keyed by `asset_group_id`. SDK translators between v1 and v2 use this flag per canonical to know which assets in the v2 manifest map back to v1 `inputs` vs v1 `assets`. Without the per-slot flag the dispatch table lives in adopter code and every SDK gets it slightly different. + * + * Producers SHOULD set this explicitly on slots whose consumption pattern isn't obvious (host-read scripts on `audio_hosted`, briefs on generative `video_hosted`, catalog feeds on `sponsored_placement`). For canonicals where every slot is render-verbatim (`image`, `display_tag`, `video_vast`), the default `false` is sufficient and the flag MAY be omitted. + */ + consumed_for_production?: boolean; + }[]; + /** + * Downstream platform connections or grants required to use this format declaration. These are in addition to the single AdCP caller credential. Use this when a platform product requires multiple downstream grants, such as an advertiser account connection plus a publisher identity or post authorization for published-post references. + */ + required_connections?: DownstreamConnectionRequirement[]; + /** + * Policy for formats whose `slots` accept a `published_post` reference. `immutable_snapshot`: seller snapshots the referenced post at approval and later source changes do not change the served creative. `mutable_requires_reapproval`: the source post may change and material changes require review before continued serving. `mutable_auto_recheck`: the source post may change and the seller continuously or periodically rechecks authorization/policy without requiring buyer resubmission. Omit when the format has no `published_post` slot. + */ + reference_mutability?: 'immutable_snapshot' | 'mutable_requires_reapproval' | 'mutable_auto_recheck'; + /** + * Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative). + */ + production_window_business_days?: number; + /** + * Which end of the template contract the buyer feeds. `components`: buyer supplies component slots; seller renders states (no `state_canvases`/`layered_source` assets allowed). `rendered_canvases`: buyer supplies exactly one `state_canvases` image per declared state × breakpoint pair. `layered_source`: buyer ships design source (+ optional `font_files`); seller production derives states (`production_window_business_days` applies) — transitional for sellers without executable templates. + */ + supply_mode?: 'components' | 'rendered_canvases' | 'layered_source'; + /** + * Finite visual states known at buy time; state and breakpoint IDs form the canvas-key matrix. Runtime causes live in `transitions[]`. A single-state unit (topscroll, interscroller, skin) declares one state, no transitions, and typically a `reveal` mechanic. + */ + states: { + /** + * Stable identifier used by `state_canvases[].state_id` and `state_click_urls[].state_id`. + * @pattern ^[a-z][a-z0-9_]*$ + */ + state_id: string; + /** + * `underlay` renders the canvas beneath page content, which scrolls over it (IAB New Ad Portfolio underlay class: skins, reveal units). Transitions into `overlay`/`fullscreen_overlay` states SHOULD be user-initiated; non-user-action entries emit LEAN policy warnings. + */ + anchoring: 'inline' | 'sticky_top' | 'sticky_bottom' | 'overlay' | 'fullscreen_overlay' | 'underlay'; + /** + * Asset group IDs rendered in this state (components mode). Makes the template executable: given components and bindings, assembly is deterministic. Every value MUST resolve to a declared slot. Omitted means all supplied component slots may render. + */ + slot_bindings?: string[]; + /** + * Whether the seller-rendered layout animates within this state (attract loops, load animations). Intra-state animation is seller-rendered; buyer canvases stay static images. + */ + motion?: 'static' | 'animated'; + /** + * Upper bound on intra-state animation duration. Required when `motion` is `animated`. + */ + max_animation_s?: number; + breakpoints: { + }[]; + /** + * Whether this state visibly renders a seller-controlled close affordance. When true, rendering MUST follow IAB New Ad Portfolio close-button guidance (top-right, minimum 50×50 dp, available from state entry). + */ + close_affordance: boolean; + }[]; + /** + * State rendered when the unit first becomes visible. MUST resolve to `states[].state_id`; for a single-state unit it MUST equal the sole state. + * @pattern ^[a-z][a-z0-9_]*$ + */ + initial_state_id: string; + /** + * How the unit enters view, distinct from state changes. `clip_window`: canvas fixed and progressively exposed through a scrolling window (interscroller, topscroll). `scroll_parallax`: canvas moves at a different rate than content. Reveal is presentation of one canvas, not a transition; do not fabricate a second state to express it. + */ + reveal?: 'none' | 'clip_window' | 'scroll_parallax'; + /** + * Bounded seller-rendered transitions between declared visual states. Required when `states` has more than one entry; MUST be omitted for single-state units. Every non-initial state MUST be reachable from `initial_state_id`. Dismissal is terminal unit behavior declared by `user_controls.dismissible`, not a hidden visual state. + */ + transitions?: ( + | { + /** + * Stable transition identifier for preview and reporting. + * @pattern ^[a-z][a-z0-9_]*$ + */ + transition_id: string; + /** + * @pattern ^[a-z][a-z0-9_]*$ + */ + from_state_id: string; + /** + * @pattern ^[a-z][a-z0-9_]*$ + */ + to_state_id: string; + /** + * Cause of the transition. `timer` counts from state entry; `in_view_timer` counts viewable time in the current state (industry auto-collapse is N seconds in view). `media_event` fires on `video_main` playback milestones. The outcome is expressed separately by `to_state_id`. + */ + trigger: 'timer'; + /** + * Scroll direction that arms a `scroll_threshold` transition; enables direction-aware expand/collapse cycles. Re-crossing in the opposite direction does not re-fire this transition. + */ + direction?: 'down' | 'up'; + /** + * `scroll_linked` continuously interpolates the seller-owned layout between the two declared endpoint states; it is valid only with trigger `scroll_progress` and does not permit buyer scripting. + */ + transition_mode: 'instant' | 'animated'; + /** + * For `timer`: delay after `from_state_id` activates (re-entry restarts it). For `in_view_timer`: accumulated viewable milliseconds in `from_state_id`. Timer-class transitions in a state cycle MUST declare at least 1000 (anti-strobe floor). + * @minimum 0 + */ + delay_ms: number; + /** + * Duration of a seller-rendered animated transition. + * @minimum 0 + */ + duration_ms?: number; + /** + * Whether `video_main` continues without restart while the seller changes state. + */ + preserve_playback?: boolean; + } + | { + /** + * Stable transition identifier for preview and reporting. + * @pattern ^[a-z][a-z0-9_]*$ + */ + transition_id: string; + /** + * @pattern ^[a-z][a-z0-9_]*$ + */ + from_state_id: string; + /** + * @pattern ^[a-z][a-z0-9_]*$ + */ + to_state_id: string; + /** + * Cause of the transition. `timer` counts from state entry; `in_view_timer` counts viewable time in the current state (industry auto-collapse is N seconds in view). `media_event` fires on `video_main` playback milestones. The outcome is expressed separately by `to_state_id`. + */ + trigger: 'in_view_timer'; + /** + * Scroll direction that arms a `scroll_threshold` transition; enables direction-aware expand/collapse cycles. Re-crossing in the opposite direction does not re-fire this transition. + */ + direction?: 'down' | 'up'; + /** + * `scroll_linked` continuously interpolates the seller-owned layout between the two declared endpoint states; it is valid only with trigger `scroll_progress` and does not permit buyer scripting. + */ + transition_mode: 'instant' | 'animated'; + /** + * For `timer`: delay after `from_state_id` activates (re-entry restarts it). For `in_view_timer`: accumulated viewable milliseconds in `from_state_id`. Timer-class transitions in a state cycle MUST declare at least 1000 (anti-strobe floor). + * @minimum 0 + */ + delay_ms: number; + /** + * Duration of a seller-rendered animated transition. + * @minimum 0 + */ + duration_ms?: number; + /** + * Whether `video_main` continues without restart while the seller changes state. + */ + preserve_playback?: boolean; + } + | { + /** + * Stable transition identifier for preview and reporting. + * @pattern ^[a-z][a-z0-9_]*$ + */ + transition_id: string; + /** + * @pattern ^[a-z][a-z0-9_]*$ + */ + from_state_id: string; + /** + * @pattern ^[a-z][a-z0-9_]*$ + */ + to_state_id: string; + /** + * Cause of the transition. `timer` counts from state entry; `in_view_timer` counts viewable time in the current state (industry auto-collapse is N seconds in view). `media_event` fires on `video_main` playback milestones. The outcome is expressed separately by `to_state_id`. + */ + trigger: 'scroll_threshold'; + /** + * Input driving a user or scroll transition. `hover` expansion is disallowed by IAB NAP guidance and emits a LEAN policy warning visible to buyers. + */ + input: 'scroll'; + /** + * Scroll direction that arms a `scroll_threshold` transition; enables direction-aware expand/collapse cycles. Re-crossing in the opposite direction does not re-fire this transition. + */ + direction?: 'down' | 'up'; + /** + * `scroll_linked` continuously interpolates the seller-owned layout between the two declared endpoint states; it is valid only with trigger `scroll_progress` and does not permit buyer scripting. + */ + transition_mode: 'instant' | 'animated'; + /** + * Duration of a seller-rendered animated transition. + * @minimum 0 + */ + duration_ms?: number; + /** + * Viewport/page scroll threshold that starts a `scroll_threshold` transition. + * @minimum 0 + * @maximum 100 + */ + scroll_threshold_percent: number; + /** + * Reference frame for scroll percentages. Progress is `scroll_offset / max(scroll_extent - viewport_extent, 1) * 100`, measured on either the page document or the nearest seller-declared containing scroller. + */ + scroll_reference: 'document_progress' | 'containing_scroller_progress'; + /** + * Whether `video_main` continues without restart while the seller changes state. + */ + preserve_playback?: boolean; + } + | { + /** + * Stable transition identifier for preview and reporting. + * @pattern ^[a-z][a-z0-9_]*$ + */ + transition_id: string; + /** + * @pattern ^[a-z][a-z0-9_]*$ + */ + from_state_id: string; + /** + * @pattern ^[a-z][a-z0-9_]*$ + */ + to_state_id: string; + /** + * Cause of the transition. `timer` counts from state entry; `in_view_timer` counts viewable time in the current state (industry auto-collapse is N seconds in view). `media_event` fires on `video_main` playback milestones. The outcome is expressed separately by `to_state_id`. + */ + trigger: 'scroll_progress'; + /** + * Input driving a user or scroll transition. `hover` expansion is disallowed by IAB NAP guidance and emits a LEAN policy warning visible to buyers. + */ + input: 'scroll'; + /** + * `scroll_linked` continuously interpolates the seller-owned layout between the two declared endpoint states; it is valid only with trigger `scroll_progress` and does not permit buyer scripting. + */ + transition_mode: 'scroll_linked'; + /** + * Duration of a seller-rendered animated transition. + * @minimum 0 + */ + duration_ms?: number; + /** + * Reference frame for scroll percentages. Progress is `scroll_offset / max(scroll_extent - viewport_extent, 1) * 100`, measured on either the page document or the nearest seller-declared containing scroller. + */ + scroll_reference: 'document_progress' | 'containing_scroller_progress'; + /** + * Start of the bounded scroll interval for a `scroll_progress` transition. + * @minimum 0 + * @maximum 100 + */ + scroll_start_percent: number; + /** + * End of the bounded scroll interval for a `scroll_progress` transition. MUST be greater than `scroll_start_percent`. + * @minimum 0 + * @maximum 100 + */ + scroll_end_percent: number; + /** + * Whether `video_main` continues without restart while the seller changes state. + */ + preserve_playback?: boolean; + } + | { + /** + * Stable transition identifier for preview and reporting. + * @pattern ^[a-z][a-z0-9_]*$ + */ + transition_id: string; + /** + * @pattern ^[a-z][a-z0-9_]*$ + */ + from_state_id: string; + /** + * @pattern ^[a-z][a-z0-9_]*$ + */ + to_state_id: string; + /** + * Cause of the transition. `timer` counts from state entry; `in_view_timer` counts viewable time in the current state (industry auto-collapse is N seconds in view). `media_event` fires on `video_main` playback milestones. The outcome is expressed separately by `to_state_id`. + */ + trigger: 'user_action'; + /** + * Input driving a user or scroll transition. `hover` expansion is disallowed by IAB NAP guidance and emits a LEAN policy warning visible to buyers. + */ + input: + | 'tap' + | 'hover' + | 'swipe_up' + | 'swipe_down' + | 'swipe_left' + | 'swipe_right' + | 'scroll' + | 'expand_control' + | 'collapse_control'; + /** + * `scroll_linked` continuously interpolates the seller-owned layout between the two declared endpoint states; it is valid only with trigger `scroll_progress` and does not permit buyer scripting. + */ + transition_mode: 'instant' | 'animated'; + /** + * Duration of a seller-rendered animated transition. + * @minimum 0 + */ + duration_ms?: number; + /** + * Whether `video_main` continues without restart while the seller changes state. + */ + preserve_playback?: boolean; + } + | { + /** + * Stable transition identifier for preview and reporting. + * @pattern ^[a-z][a-z0-9_]*$ + */ + transition_id: string; + /** + * @pattern ^[a-z][a-z0-9_]*$ + */ + from_state_id: string; + /** + * @pattern ^[a-z][a-z0-9_]*$ + */ + to_state_id: string; + /** + * Cause of the transition. `timer` counts from state entry; `in_view_timer` counts viewable time in the current state (industry auto-collapse is N seconds in view). `media_event` fires on `video_main` playback milestones. The outcome is expressed separately by `to_state_id`. + */ + trigger: 'media_event'; + /** + * Playback milestone of `video_main` driving a `media_event` transition (endframes, collapse-on-complete). + */ + media_event: 'video_start' | 'video_complete'; + /** + * `scroll_linked` continuously interpolates the seller-owned layout between the two declared endpoint states; it is valid only with trigger `scroll_progress` and does not permit buyer scripting. + */ + transition_mode: 'instant' | 'animated'; + /** + * Duration of a seller-rendered animated transition. + * @minimum 0 + */ + duration_ms?: number; + /** + * Whether `video_main` continues without restart while the seller changes state. + */ + preserve_playback?: boolean; + } + )[]; + /** + * Destination policy. `required` (default): manifest MUST supply `landing_page_url`. `optional`: click-optional units (in-feed brand units) may omit it. `none`: unit is non-clickable; manifests MUST NOT supply `landing_page_url` or `state_click_urls`. Per-state overrides via `state_click_urls` entries carrying `state_id`; `landing_page_url` is the fallback for unlisted states. + */ + clickthrough?: 'required' | 'optional' | 'none'; + /** + * When any state anchors as `overlay` or `fullscreen_overlay`, either `dismissible` MUST be true or that state's `close_affordance` MUST be true (dismissibility floor; semantic validators enforce). + */ + user_controls: { + dismissible: boolean; + user_collapsible: boolean; + }; + /** + * Rectangular areas constraining buyer artwork. Omitted state/breakpoint selectors apply the constraint to every canvas. For fluid or range-sized breakpoints, use percent-unit regions. + */ + canvas_constraints?: CanvasConstraint[]; + /** + * Accepted embedded-video duration [min, max]. `duration_ms_exact` takes precedence when both are present. + */ + duration_ms_range?: (number | null)[]; + /** + * @minimum 1 + */ + duration_ms_exact?: number; + /** + * Embedded-video aspect ratio. + * @pattern ^[0-9]+(\.[0-9]+)?:[0-9]+(\.[0-9]+)?$ + */ + aspect_ratio?: string; + containers?: ('mp4' | 'webm' | 'mov')[]; + video_playback?: 'none' | 'auto_muted' | 'user_initiated'; + /** + * @minimum 1 + */ + max_initial_load_kb?: number; + /** + * Ceiling on assets loaded after the window load event (IAB LEAN subload). Pairs with `max_initial_load_kb` to mirror the New Ad Portfolio initial/subload weight pair. + * @minimum 1 + */ + max_subload_kb?: number; + /** + * When true, non-initial assets load only after the host page's window load event (IAB LEAN subload boundary). + */ + polite_load?: boolean; +} +/** + * Rectangular rule applied to buyer artwork. State and breakpoint selectors are optional so the same shape can constrain a coordinated-placement component or a specific stateful-display canvas. + */ +export interface CanvasConstraint { + constraint: 'safe_area' | 'reserved_region' | 'decoration_only_edge' | 'no_text_or_logos'; + state_id?: string; + breakpoint_id?: string; + region: { + }; +} + +// CANONICALFORMATCOORDINATEDPLACEMENTS PRIORITY CANONICAL SCHEMA +/** + * Re-export of `SizeModeMutex` under the legacy codegen artifact name. + * + * `SizeModeMutex1` is a json-schema-to-typescript under-resolution artifact — + * the bundler inlined the same schema at two call sites and jsts emitted a numbered + * sibling. The body it produced was strictly weaker than `SizeModeMutex` (missing the + * discriminator, canonical wrapper, or named union); aliasing to `SizeModeMutex` + * gives consumers the correctly-discriminated shape that matches the wire format. + * + * @deprecated Use `SizeModeMutex` from `@adcp/sdk/types`. Slated for removal in the next major. + */ +export type SizeModeMutex1 = SizeModeMutex; +/** + * Re-export of `Responsive` under the legacy codegen artifact name. + * + * `Responsive1` is a json-schema-to-typescript under-resolution artifact — + * the bundler inlined the same schema at two call sites and jsts emitted a numbered + * sibling. The body it produced was strictly weaker than `Responsive` (missing the + * discriminator, canonical wrapper, or named union); aliasing to `Responsive` + * gives consumers the correctly-discriminated shape that matches the wire format. + * + * @deprecated Use `Responsive` from `@adcp/sdk/types`. Slated for removal in the next major. + */ +export type Responsive1 = Responsive; +/** + * Re-export of `SizeModeMutex` under the legacy codegen artifact name. + * + * `SizeModeMutex2` is a json-schema-to-typescript under-resolution artifact — + * the bundler inlined the same schema at two call sites and jsts emitted a numbered + * sibling. The body it produced was strictly weaker than `SizeModeMutex` (missing the + * discriminator, canonical wrapper, or named union); aliasing to `SizeModeMutex` + * gives consumers the correctly-discriminated shape that matches the wire format. + * + * @deprecated Use `SizeModeMutex` from `@adcp/sdk/types`. Slated for removal in the next major. + */ +export type SizeModeMutex2 = SizeModeMutex; +/** + * Re-export of `Responsive` under the legacy codegen artifact name. + * + * `Responsive2` is a json-schema-to-typescript under-resolution artifact — + * the bundler inlined the same schema at two call sites and jsts emitted a numbered + * sibling. The body it produced was strictly weaker than `Responsive` (missing the + * discriminator, canonical wrapper, or named union); aliasing to `Responsive` + * gives consumers the correctly-discriminated shape that matches the wire format. + * + * @deprecated Use `Responsive` from `@adcp/sdk/types`. Slated for removal in the next major. + */ +export type Responsive2 = Responsive; +/** + * One creative manifest atomically supplies assets for multiple declared product placements. Each component binds to a public `Product.placements[]` entry and either declares an inline non-custom canonical format or references a sibling format option on the same product. Components cannot nest coordinated placements. The manifest supplies component slots under `component_assets.`; `shared_slots` assets are supplied once at top level. Inventory exclusivity remains `Product.exclusivity`, not a creative-format parameter. Ordinary products whose placements accept independently assigned creatives do not need this canonical. + */ +export interface CanonicalFormatCoordinatedPlacements { + /** + * Experimental in AdCP 3.2 while the creative working group gathers implementation evidence for atomic cross-placement composition. + */ + experimental?: boolean; + /** + * When true, this canonical (or a seller's specific narrowing of it) is going away. Existing adopters are supported through the deprecation cycle; new adoption is discouraged. Pair with `migration_target_version` to indicate when the canonical is expected to be removed. Distinct from `experimental`: an experimental canonical may stabilize and stop being experimental; a deprecated canonical is on a sunset path. + */ + deprecated?: boolean; + /** + * No v1 named-format equivalent can express a coordinated multi-placement buy. + */ + v1_translatable?: boolean; + /** + * AdCP MAJOR.MINOR version that introduced this canonical (e.g., '3.1', '3.2'). Lets adopters reason about minimum protocol version requirements when consuming a format declaration. Patch precision is intentionally rejected — canonicals are introduced at minor-version boundaries. + */ + since_version?: string; + /** + * AdCP MAJOR.MINOR version by which the working group expects this canonical to stabilize, surface a breaking revision, or (when `deprecated: true`) be removed. Patch precision is intentionally rejected — canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown'). + */ + migration_target_version?: string; + /** + * Whether the surface composes deterministically (buyer can predict per-slot rendering — sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing — responsive_creative, agent_placement). + */ + composition_model?: 'deterministic' | 'algorithmic'; + /** + * When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent. + */ + provenance_required?: boolean; + /** + * Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly. + * + * **Collision precedence (normative).** When two or more `platform_extensions[]` entries on the same declaration extend the same target (e.g., both extend `tracking`) with overlapping field names, **array order is authoritative — later entries override earlier ones on a per-field basis** (last-in-array-wins). SDKs MUST surface the overlap via the `errors[]` array on the `get_products` response with a structured code (`FORMAT_DECLARATION_DIVERGENT` is appropriate when the overlap appears across dual-emitted shapes; a producer-self-emitted overlap on a single declaration SHOULD use the same code with `error.details: { collision_kind: "platform_extension_field", target, overlapping_fields, winning_extension_uri }`). Producers SHOULD avoid the collision by emitting one extension per target or by partitioning fields across extensions; the deterministic precedence is for last-resort consistency across SDK implementations, not a sanctioned merging strategy. + */ + platform_extensions?: PlatformExtensionReference[]; + /** + * When true, the format's production pipeline is genuinely nondeterministic — the platform cannot guarantee that synthesis from a given input set produces in-spec output. Veo / Sora / Runway-class generative video, and other AI-synthesis flows where output dimensions, duration, or quality vary per run. Implies a different validation contract: predictive `validate_input` is impossible; the platform's own post-synthesis QA loop applies; if the QA loop exhausts without producing a valid artifact, `build_creative` returns task_failed with a synthesis_failed reason. Distinct from `composition_model` (which describes how the surface composes per-slot rendering, not whether synthesis is deterministic). When false or absent, the format's production is predictable enough that `validate_input` can predict output properties from input properties. + * + * **Compatibility with `asset_source` / `item_production_model`**: `synthesis_nondeterministic: true` MAY pair with any of `seller_pre_rendered_from_brief`, `seller_human_designed`, or `agent_synthesized` (the QA loop is concept-level, not source-specific — 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error. + */ + synthesis_nondeterministic?: boolean; + /** + * Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry. + */ + slots?: { + /** + * Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings. + */ + asset_group_id: string; + /** + * Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `published_post` is an existing-post reference asset, not uploaded media bytes and not a catalog row. `card` is the multi-card carousel element type (see card-asset.json). `pixel_tracker` / `vast_tracker` / `daast_tracker` are the renderer-fired measurement-tracker primitives — see `/schemas/core/assets/pixel-tracker-asset.json` and the VAST / DAAST tracker schemas. `object` is a last-resort fallback for structured non-asset inputs that don't fit any primitive asset_type — prefer specific types whenever possible. + */ + asset_type: + | 'image' + | 'video' + | 'audio' + | 'text' + | 'markdown' + | 'url' + | 'html' + | 'css' + | 'javascript' + | 'vast' + | 'daast' + | 'webhook' + | 'brief' + | 'catalog' + | 'published_post' + | 'zip' + | 'card' + | 'object' + | 'pixel_tracker' + | 'vast_tracker' + | 'daast_tracker'; + /** + * Whether this slot is required for a valid manifest. + */ + required?: boolean; + /** + * Minimum count for repeatable / pool slots. + */ + min?: number; + /** + * Maximum count for repeatable / pool slots. + */ + max?: number; + /** + * Per-slot character limit. Valid only when `asset_type` is `text`, `markdown`, or `brief`. Mutually exclusive with `max_size_kb` (which applies to binary asset types). Schema enforces via if/then so a producer can't set both on the same slot. + */ + max_chars?: number; + /** + * Per-slot file size limit in kilobytes. Valid only when `asset_type` is `image`, `video`, `audio`, or `zip`. Mutually exclusive with `max_chars` (which applies to text asset types). Schema enforces via if/then so a producer can't set both on the same slot. + */ + max_size_kb?: number; + /** + * Accepted intrinsic-pixel densities for this image-bearing slot. Valid when `asset_type` is `image`, and on a `card` slot where it constrains each card's image media (video media is unaffected). This makes density available to every canonical carrying image assets (native, carousel, responsive, companion images, and image itself), not only `format_kind: image`. When the image canonical also declares top-level `params.pixel_ratios`, the effective set is the intersection; an empty intersection is invalid. One matching asset satisfies the slot unless `required_pixel_ratios` requires rendition coverage. + */ + pixel_ratios?: number[]; + /** + * Required density coverage for an image rendition set. Valid only when `asset_type` is `image` and `pixel_ratios` is also declared. Every value MUST appear in the effective accepted set after intersecting any top-level image `params.pixel_ratios`, and the manifest slot value MUST be an array containing exactly one matching image rendition for each required ratio. Other accepted ratios remain optional. For example, `pixel_ratios: [1, 1.5, 2]` with `required_pixel_ratios: [1, 2]` requires the 1x and 2x renditions while making 1.5x optional. SDKs enforce intersection, subset, coverage, and duplicate-ratio rules because JSON Schema draft-07 cannot express them generically. + */ + required_pixel_ratios?: number[]; + /** + * When `asset_group_id` is `logo`, renderer-facing brand.json logo slots acceptable for this format slot. Producers selecting from brand.json SHOULD prefer `logos[]` entries whose `slots[]` intersects this list, then apply `visual_guidelines.logo_usage_rules[]`. + */ + logo_slots?: LogoSlot[]; + /** + * Subset of `logo_slots` for which this format expects explicit logo coverage. A manifest or brand-derived logo pool SHOULD include at least one usable logo for each required slot; if coverage is missing, builders SHOULD surface a validation warning or approval mapping instead of guessing from prose. + */ + required_logo_slots?: LogoSlot[]; + /** + * Human-readable description of what the slot expects from the buyer. + */ + description?: string; + /** + * Dispatch hint for `build_creative` and v1↔v2 wire translators: when `true`, the slot's value is consumed as INPUT to a production step (host-read script, brief copy fed to generative synthesis, catalog feed driving per-SKU rendering) and is not rendered verbatim. When `false` (default), the slot's value is rendered verbatim on the placement (image bytes, video file, display tag). + * + * Motivates the v1↔v2 dispatch table: pre-v2 buyers shipped production-consumed inputs separately in a `inputs` map on the build_creative request; v2 collapses inputs and rendered assets into a single `assets` map keyed by `asset_group_id`. SDK translators between v1 and v2 use this flag per canonical to know which assets in the v2 manifest map back to v1 `inputs` vs v1 `assets`. Without the per-slot flag the dispatch table lives in adopter code and every SDK gets it slightly different. + * + * Producers SHOULD set this explicitly on slots whose consumption pattern isn't obvious (host-read scripts on `audio_hosted`, briefs on generative `video_hosted`, catalog feeds on `sponsored_placement`). For canonicals where every slot is render-verbatim (`image`, `display_tag`, `video_vast`), the default `false` is sufficient and the flag MAY be omitted. + */ + consumed_for_production?: boolean; + }[]; + /** + * Downstream platform connections or grants required to use this format declaration. These are in addition to the single AdCP caller credential. Use this when a platform product requires multiple downstream grants, such as an advertiser account connection plus a publisher identity or post authorization for published-post references. + */ + required_connections?: DownstreamConnectionRequirement[]; + /** + * Policy for formats whose `slots` accept a `published_post` reference. `immutable_snapshot`: seller snapshots the referenced post at approval and later source changes do not change the served creative. `mutable_requires_reapproval`: the source post may change and material changes require review before continued serving. `mutable_auto_recheck`: the source post may change and the seller continuously or periodically rechecks authorization/policy without requiring buyer resubmission. Omit when the format has no `published_post` slot. + */ + reference_mutability?: 'immutable_snapshot' | 'mutable_requires_reapproval' | 'mutable_auto_recheck'; + /** + * Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative). + */ + production_window_business_days?: number; + components: ( + | { + } + | ( + | { + format_kind: 'image'; + params?: CanonicalFormatImage; + } + | { + format_kind: 'html5'; + params?: CanonicalFormatHTML5Banner; + } + | { + format_kind: 'display_tag'; + params?: CanonicalFormatDisplayTag; + } + | { + format_kind: 'image_carousel'; + params?: CanonicalFormatImageCarousel; + } + | { + format_kind: 'video_hosted'; + params?: CanonicalFormatHostedVideo; + } + | { + format_kind: 'video_vast'; + params?: CanonicalFormatVASTVideo; + } + | { + format_kind: 'audio_hosted'; + params?: CanonicalFormatHostedAudio; + } + | { + format_kind: 'audio_daast'; + params?: CanonicalFormatDAASTAudio; + } + | { + format_kind: 'sponsored_placement'; + params?: CanonicalFormatSponsoredPlacementRetailMediaCatalogDriven; + } + | { + format_kind: 'native_in_feed'; + params?: CanonicalFormatNativeInFeed; + } + | { + format_kind: 'responsive_creative'; + params?: CanonicalFormatResponsiveCreative; + } + | { + format_kind: 'agent_placement'; + params?: CanonicalFormatAgentPlacementAISurfaceSponsoredPlacement; + } + | { + format_kind: 'seller_rendered_stateful_display'; + params?: CanonicalFormatSellerRenderedStatefulDisplay; + } + ) + )[]; + /** + * Manifest slots supplied once and consumed by one or more coordinated components. + */ + shared_slots?: { + /** + * @pattern ^[a-z0-9_]+$ + */ + asset_group_id: string; + asset_type: + | 'image' + | 'video' + | 'audio' + | 'text' + | 'markdown' + | 'url' + | 'html' + | 'css' + | 'javascript' + | 'vast' + | 'daast' + | 'webhook' + | 'brief' + | 'catalog' + | 'published_post' + | 'zip' + | 'card' + | 'object' + | 'pixel_tracker' + | 'vast_tracker' + | 'daast_tracker'; + required?: boolean; + /** + * @minimum 0 + */ + min?: number; + /** + * @minimum 1 + */ + max?: number; + /** + * Component IDs that consume this shared asset. Every value MUST resolve to `components[].component_id`. + */ + consumed_by: string[]; + }[]; +} +/** + * Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints). + */ +export interface CanonicalFormatBase { + /** + * When true, this canonical or seller narrowing may not work as declared. Adopters SHOULD preflight it with validate_input or in a sandbox and SHOULD NOT route production budget without testing; experimental status never makes the deprecated v1 path preferable. Drivers include unsettled spec shape, an adopter runtime gap, and custom shapes awaiting promotion. This replaces the earlier status plus runtime_status axes. Sellers SHOULD set experimental whenever a canonical or declaration is not production-ready. + */ + experimental?: boolean; + /** + * When true, this canonical (or a seller's specific narrowing of it) is going away. Existing adopters are supported through the deprecation cycle; new adoption is discouraged. Pair with `migration_target_version` to indicate when the canonical is expected to be removed. Distinct from `experimental`: an experimental canonical may stabilize and stop being experimental; a deprecated canonical is on a sunset path. + */ + deprecated?: boolean; + /** + * Whether this canonical has any v1 named-format equivalent. `true` (default) — the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1→v2 projection via `v1-canonical-mapping.json` is meaningful. `false` — the canonical is inherently new in v2 and has no v1 form; v1's `list_creative_formats` couldn't express it because the underlying concept (algorithmic surface composition, AI-surface mentions, retail-media catalog placements, multi-card carousels) didn't exist as a v1 named-format archetype. + * + * Lets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The six inherently-v2 canonicals in 3.2 are `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`, `seller_rendered_stateful_display`, and `coordinated_placements`. + */ + v1_translatable?: boolean; + /** + * AdCP MAJOR.MINOR version that introduced this canonical (e.g., '3.1', '3.2'). Lets adopters reason about minimum protocol version requirements when consuming a format declaration. Patch precision is intentionally rejected — canonicals are introduced at minor-version boundaries. + */ + since_version?: string; + /** + * AdCP MAJOR.MINOR version by which the working group expects this canonical to stabilize, surface a breaking revision, or (when `deprecated: true`) be removed. Patch precision is intentionally rejected — canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown'). + */ + migration_target_version?: string; + /** + * Whether the surface composes deterministically (buyer can predict per-slot rendering — sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing — responsive_creative, agent_placement). + */ + composition_model?: 'deterministic' | 'algorithmic'; + /** + * When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent. + */ + provenance_required?: boolean; + /** + * Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly. + * + * **Collision precedence (normative).** When two or more `platform_extensions[]` entries on the same declaration extend the same target (e.g., both extend `tracking`) with overlapping field names, **array order is authoritative — later entries override earlier ones on a per-field basis** (last-in-array-wins). SDKs MUST surface the overlap via the `errors[]` array on the `get_products` response with a structured code (`FORMAT_DECLARATION_DIVERGENT` is appropriate when the overlap appears across dual-emitted shapes; a producer-self-emitted overlap on a single declaration SHOULD use the same code with `error.details: { collision_kind: "platform_extension_field", target, overlapping_fields, winning_extension_uri }`). Producers SHOULD avoid the collision by emitting one extension per target or by partitioning fields across extensions; the deterministic precedence is for last-resort consistency across SDK implementations, not a sanctioned merging strategy. + */ + platform_extensions?: PlatformExtensionReference[]; + /** + * When true, the format's production pipeline is genuinely nondeterministic — the platform cannot guarantee that synthesis from a given input set produces in-spec output. Veo / Sora / Runway-class generative video, and other AI-synthesis flows where output dimensions, duration, or quality vary per run. Implies a different validation contract: predictive `validate_input` is impossible; the platform's own post-synthesis QA loop applies; if the QA loop exhausts without producing a valid artifact, `build_creative` returns task_failed with a synthesis_failed reason. Distinct from `composition_model` (which describes how the surface composes per-slot rendering, not whether synthesis is deterministic). When false or absent, the format's production is predictable enough that `validate_input` can predict output properties from input properties. + * + * **Compatibility with `asset_source` / `item_production_model`**: `synthesis_nondeterministic: true` MAY pair with any of `seller_pre_rendered_from_brief`, `seller_human_designed`, or `agent_synthesized` (the QA loop is concept-level, not source-specific — 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error. + */ + synthesis_nondeterministic?: boolean; + /** + * Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry. + */ + slots?: { + }[]; + /** + * Downstream platform connections or grants required to use this format declaration. These are in addition to the single AdCP caller credential. Use this when a platform product requires multiple downstream grants, such as an advertiser account connection plus a publisher identity or post authorization for published-post references. + */ + required_connections?: DownstreamConnectionRequirement[]; + /** + * Policy for formats whose `slots` accept a `published_post` reference. `immutable_snapshot`: seller snapshots the referenced post at approval and later source changes do not change the served creative. `mutable_requires_reapproval`: the source post may change and material changes require review before continued serving. `mutable_auto_recheck`: the source post may change and the seller continuously or periodically rechecks authorization/policy without requiring buyer resubmission. Omit when the format has no `published_post` slot. + */ + reference_mutability?: 'immutable_snapshot' | 'mutable_requires_reapproval' | 'mutable_auto_recheck'; + /** + * Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative). + */ + production_window_business_days?: number; +} +/** + * Re-export of `Fixed` under the legacy codegen artifact name. + * + * `Fixed1` is a json-schema-to-typescript under-resolution artifact — + * the bundler inlined the same schema at two call sites and jsts emitted a numbered + * sibling. The body it produced was strictly weaker than `Fixed` (missing the + * discriminator, canonical wrapper, or named union); aliasing to `Fixed` + * gives consumers the correctly-discriminated shape that matches the wire format. + * + * @deprecated Use `Fixed` from `@adcp/sdk/types`. Slated for removal in the next major. + */ +export type Fixed1 = Fixed; +/** + * Re-export of `MultiSize` under the legacy codegen artifact name. + * + * `MultiSize1` is a json-schema-to-typescript under-resolution artifact — + * the bundler inlined the same schema at two call sites and jsts emitted a numbered + * sibling. The body it produced was strictly weaker than `MultiSize` (missing the + * discriminator, canonical wrapper, or named union); aliasing to `MultiSize` + * gives consumers the correctly-discriminated shape that matches the wire format. + * + * @deprecated Use `MultiSize` from `@adcp/sdk/types`. Slated for removal in the next major. + */ +export type MultiSize1 = MultiSize; +/** + * Re-export of `None` under the legacy codegen artifact name. + * + * `None1` is a json-schema-to-typescript under-resolution artifact — + * the bundler inlined the same schema at two call sites and jsts emitted a numbered + * sibling. The body it produced was strictly weaker than `None` (missing the + * discriminator, canonical wrapper, or named union); aliasing to `None` + * gives consumers the correctly-discriminated shape that matches the wire format. + * + * @deprecated Use `None` from `@adcp/sdk/types`. Slated for removal in the next major. + */ +export type None1 = None; +/** + * Re-export of `Fixed` under the legacy codegen artifact name. + * + * `Fixed2` is a json-schema-to-typescript under-resolution artifact — + * the bundler inlined the same schema at two call sites and jsts emitted a numbered + * sibling. The body it produced was strictly weaker than `Fixed` (missing the + * discriminator, canonical wrapper, or named union); aliasing to `Fixed` + * gives consumers the correctly-discriminated shape that matches the wire format. + * + * @deprecated Use `Fixed` from `@adcp/sdk/types`. Slated for removal in the next major. + */ +export type Fixed2 = Fixed; +/** + * Re-export of `MultiSize` under the legacy codegen artifact name. + * + * `MultiSize2` is a json-schema-to-typescript under-resolution artifact — + * the bundler inlined the same schema at two call sites and jsts emitted a numbered + * sibling. The body it produced was strictly weaker than `MultiSize` (missing the + * discriminator, canonical wrapper, or named union); aliasing to `MultiSize` + * gives consumers the correctly-discriminated shape that matches the wire format. + * + * @deprecated Use `MultiSize` from `@adcp/sdk/types`. Slated for removal in the next major. + */ +export type MultiSize2 = MultiSize; +/** + * Re-export of `None` under the legacy codegen artifact name. + * + * `None2` is a json-schema-to-typescript under-resolution artifact — + * the bundler inlined the same schema at two call sites and jsts emitted a numbered + * sibling. The body it produced was strictly weaker than `None` (missing the + * discriminator, canonical wrapper, or named union); aliasing to `None` + * gives consumers the correctly-discriminated shape that matches the wire format. + * + * @deprecated Use `None` from `@adcp/sdk/types`. Slated for removal in the next major. + */ +export type None2 = None; +// VALIDATEPROPERTYDELIVERYREQUEST PRIORITY CANONICAL SCHEMA +/** + * Account that owns the list. Required when the authenticated agent has access to multiple accounts; optional otherwise. + */ +export type AccountReference = + | { + /** + * Seller-assigned account identifier. For upstream-managed account namespaces, this value comes from list_accounts; for seller-defined namespaces without a list_accounts surface, it is supplied out-of-band. Buyer-declared account sellers MAY echo account_id from sync_accounts as an internal handle, but they MUST continue accepting the account's current natural-key AccountRef on subsequent calls. A former key tombstoned by identity reconciliation returns ACCOUNT_MOVED to authorized callers. + */ + account_id: string; + } + | { + brand: BrandReference; + /** + * Domain of the entity operating on the brand's behalf. When the brand operates directly, this is the brand's domain. + */ + operator: string; + operator_unit?: OperatorUnit; + /** + * Immutable ISO 4217 transaction currency when the seller's advertiser object is currency-bound. When present, this is part of the natural account key and media buys on the account MUST use it. Omit when currency is selected independently per media buy. + */ + currency?: string; + /** + * Immutable account timezone. Include it in the natural key when get_adcp_capabilities.account.timezone declares account_fixed with buyer_selected; omit it for seller_fixed or seller_assigned accounts. + */ + timezone?: string; + /** + * When true, references the sandbox account for this brand/operator pair. Defaults to false (production account). + */ + sandbox?: boolean; + }; +/** + * Request payload for validate_property_delivery task. Validates delivery records against a property list to determine compliance. + */ +export interface ValidatePropertyDeliveryRequest { + /** + * Release-precision AdCP version (VERSION.RELEASE, e.g. "3.0", "3.1", "3.1-beta"). On a request: the buyer's release pin — the seller validates against its supported_versions and returns VERSION_UNSUPPORTED on cross-major mismatch, or downshifts to the highest supported release within the same major. On a response: the release the seller actually served — clients SHOULD validate the response against that release's schema, not against their pin. Patches are not negotiated; surface them as build_version on capabilities for operational visibility. When omitted, falls back to adcp_major_version (deprecated) or server default. Buyers SHOULD emit both adcp_version and adcp_major_version through 3.x to remain compatible with sellers that only read the legacy field. NORMALIZATION: SDKs that read full-semver values from bundle metadata (e.g. ComplianceIndex.published_version = "3.1.0-beta.1") MUST normalize to release-precision ("3.1-beta.1") before emitting on the wire — meta-field values are NOT valid wire values. + */ + adcp_version?: string; + /** + * @deprecated + * DEPRECATED in favor of adcp_version (release-precision string). Servers MUST continue to honor this field through 3.x. Removed in 4.0. Original semantics: the AdCP major version the buyer's payloads conform to. Sellers validate against their supported major_versions and return VERSION_UNSUPPORTED if unsupported. When omitted, the seller assumes its highest supported version. + */ + adcp_major_version?: number; + /** + * ID of the property list to validate against + */ + list_id: string; + account?: AccountReference; + /** + * Delivery records to validate. Each record represents impressions delivered to a property identifier. + */ + records: DeliveryRecord[]; + /** + * Include compliant records in results (default: only return non_compliant, unmodeled, and unidentified) + */ + include_compliant?: boolean; + context?: ContextObject; + ext?: ExtensionObject; +} +/** + * Optional operator-owned business unit, agency seat, or platform account. Only id participates in the natural account key; name is mutable display metadata. + */ +export interface OperatorUnit { + /** + * Stable identifier assigned by the operator. Numeric platform IDs and durable slugs are both valid. Scoped by the enclosing operator domain. + */ + id: string; + /** + * Human-readable seat or business-unit name, such as Nova EMEA. This label may change and is not part of the natural account key. + */ + name?: string; +} +/** + * A single delivery record representing impressions served to a property identifier. Used as input to validate_property_delivery. + */ +export interface DeliveryRecord { + identifier: Identifier; + /** + * Number of impressions delivered to this identifier + */ + impressions: number; + /** + * Optional client-provided ID for correlating results back to source data + */ + record_id?: string; + /** + * URL of the sales agent that sold this inventory. If provided, authorization is validated against the property's adagents.json. + */ + sales_agent_url?: string; + ext?: ExtensionObject; +} +/** + * The property identifier where impressions were delivered + */ +export interface Identifier { + type: PropertyIdentifierTypes; + /** + * The identifier value. For domain type: 'example.com' matches base domain plus www and m subdomains; 'edition.example.com' matches that specific subdomain; '*.example.com' matches ALL subdomains but NOT base domain + */ + value: string; +} +// VALIDATEPROPERTYDELIVERYRESPONSE PRIORITY CANONICAL SCHEMA +/** + * Response payload for validate_property_delivery task. Returns aggregate compliance statistics and per-record validation results. + */ +export interface ValidatePropertyDeliveryResponse { + /** + * Session/conversation identifier for tracking related operations across multiple task invocations. Managed by the protocol layer to maintain conversational context. Distinct from `context` (per-request opaque echo, see below). + */ + context_id?: string; + context?: ContextObject; + /** + * Unique identifier for tracking asynchronous operations. Present when a task requires extended processing time. Used to query task status and retrieve results when complete. + */ + task_id?: string; + status: TaskStatus; + /** + * Human-readable summary of the task result. Provides natural language explanation of what happened, suitable for display to end users or for AI agent comprehension. Generated by the protocol layer based on the task response. + */ + message?: string; + /** * ISO 8601 timestamp when the response was generated. Useful for debugging, logging, cache validation, and tracking async operation progress. */ timestamp?: string; @@ -12764,6 +13915,16 @@ export type CommittedMetric = scope: 'vendor'; vendor: BrandReference6; metric_id: VendorMetricID; + /** + * Optional qualifier disambiguating commitments to the same vendor metric measured under different methodologies or windows. Same closed key set as standard-scope entries; new keys ship explicitly. + */ + qualifier?: { + viewability_standard?: ViewabilityStandard; + completion_source?: CompletionSource; + attribution_methodology?: AttributionMethodology; + attribution_window?: Duration; + lift_dimension?: LiftDimension; + }; /** * ISO 8601 timestamp when this vendor metric became part of the contract. */ @@ -12776,7 +13937,7 @@ export type CommittedMetric = * * **Format matching vs satisfaction (normative).** Legacy named formats MUST be normalized to canonical declarations before comparison; do not exact-match raw `(agent_url, id)` pairs once a `format_id` has been projected through `canonical`, `v1_format_ref`, or the canonical mapping registry. Equivalence matching can treat a legacy fixed-size display ID and `format_kind: "image"` with matching `width`/`height` as the same underlying shape. Product satisfaction is stricter and directional: when this declaration specifies fixed constraints such as `width`, `height`, `duration_ms_exact`, or `duration_ms_range`, a buyer request or creative manifest MUST declare and satisfy those constraints. A broad request with no dimensions or duration does not satisfy a fixed-size or fixed-duration product; a broad product MAY accept a more specific creative unless another product constraint excludes it. Duration precedence is `duration_ms_exact` > `duration_ms_range`. Range constraints use containment: a range-based request satisfies this declaration only when every value it permits falls within this declaration's accepted range; overlap alone is insufficient. An exact value satisfies a range when the exact value falls inside the accepted interval. For hosted audio/video, a null range endpoint is unbounded: [null, 60000] means up to 60s, and [15000, null] means at least 15s; [null, null] is invalid because at least one endpoint must be bounded. * - * **Custom format_kind** (`format_kind: "custom"`): for adopter-defined shapes that don't fit the 12 canonicals (multi-placement takeover, roadblock, branded content, cross-screen sponsorship, sponsorship lockup, newsletter sponsorship, AR lens, playable, live event sponsorship). When `format_kind` is `custom`, the declaration MUST carry `format_shape` (recognized global pattern from the [format-shape vocabulary registry](/schemas/core/format-shape-vocabulary.json)) AND `format_schema` (URI+digest reference to a fetchable schema describing the actual `params` and `slots`). Buyer agents fetch the schema, validate manifests structurally, and reason about manifests without per-seller integration code. See [adcp#3666](https://github.com/adcontextprotocol/adcp/issues/3666) for the canonical promotion queue. + * **Custom format_kind** (`format_kind: "custom"`): for adopter-defined shapes that don't fit the 14 canonicals (branded content, cross-screen sponsorship, sponsorship lockup, newsletter sponsorship, AR lens, playable, live event sponsorship). When `format_kind` is `custom`, the declaration MUST carry `format_shape` (recognized global pattern from the [format-shape vocabulary registry](/schemas/core/format-shape-vocabulary.json)) AND `format_schema` (URI+digest reference to a fetchable schema describing the actual `params` and `slots`). Buyer agents fetch the schema, validate manifests structurally, and reason about manifests without per-seller integration code. See [adcp#3666](https://github.com/adcontextprotocol/adcp/issues/3666) for the canonical promotion queue. */ export type ProductFormatDeclaration = { } & { @@ -12816,7 +13977,7 @@ export type ProductFormatDeclaration = { */ experimental?: boolean; /** - * REQUIRED when `format_kind: "custom"`; otherwise MUST be absent. Recognized global pattern this custom shape is an instance of, drawn from the [format-shape vocabulary registry](/schemas/core/format-shape-vocabulary.json) (`multi_placement_takeover`, `roadblock`, `branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values valid (validators MAY soft-warn) — adopters CAN ship a shape that isn't yet in the registry. Adding entries is a vocabulary PR. Once a `format_shape` entry sees 2+ adopters with substantively similar `format_schema` content for 90+ days, the working group promotes it to a first-class canonical. + * REQUIRED when `format_kind: "custom"`; otherwise MUST be absent. Recognized global pattern this custom shape is an instance of, drawn from the [format-shape vocabulary registry](/schemas/core/format-shape-vocabulary.json) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are valid (validators MAY soft-warn) — adopters CAN ship a shape that isn't yet in the registry. Adding entries is a vocabulary PR. Once a `format_shape` entry sees 2+ adopters with substantively similar `format_schema` content for 90+ days, the working group may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format. */ format_shape?: string; /** @@ -12857,56 +14018,10 @@ export type ProductFormatDeclaration = { | NativeInFeedFormatDeclaration | ResponsiveCreativeFormatDeclaration | AgentPlacementFormatDeclaration + | SellerRenderedStatefulDisplayFormatDeclaration + | CoordinatedPlacementsFormatDeclaration | CustomFormatDeclaration ); -/** - * Re-export of `SizeModeMutex` under the legacy codegen artifact name. - * - * `SizeModeMutex1` is a json-schema-to-typescript under-resolution artifact — - * the bundler inlined the same schema at two call sites and jsts emitted a numbered - * sibling. The body it produced was strictly weaker than `SizeModeMutex` (missing the - * discriminator, canonical wrapper, or named union); aliasing to `SizeModeMutex` - * gives consumers the correctly-discriminated shape that matches the wire format. - * - * @deprecated Use `SizeModeMutex` from `@adcp/sdk/types`. Slated for removal in the next major. - */ -export type SizeModeMutex1 = SizeModeMutex; -/** - * Re-export of `Responsive` under the legacy codegen artifact name. - * - * `Responsive1` is a json-schema-to-typescript under-resolution artifact — - * the bundler inlined the same schema at two call sites and jsts emitted a numbered - * sibling. The body it produced was strictly weaker than `Responsive` (missing the - * discriminator, canonical wrapper, or named union); aliasing to `Responsive` - * gives consumers the correctly-discriminated shape that matches the wire format. - * - * @deprecated Use `Responsive` from `@adcp/sdk/types`. Slated for removal in the next major. - */ -export type Responsive1 = Responsive; -/** - * Re-export of `SizeModeMutex` under the legacy codegen artifact name. - * - * `SizeModeMutex2` is a json-schema-to-typescript under-resolution artifact — - * the bundler inlined the same schema at two call sites and jsts emitted a numbered - * sibling. The body it produced was strictly weaker than `SizeModeMutex` (missing the - * discriminator, canonical wrapper, or named union); aliasing to `SizeModeMutex` - * gives consumers the correctly-discriminated shape that matches the wire format. - * - * @deprecated Use `SizeModeMutex` from `@adcp/sdk/types`. Slated for removal in the next major. - */ -export type SizeModeMutex2 = SizeModeMutex; -/** - * Re-export of `Responsive` under the legacy codegen artifact name. - * - * `Responsive2` is a json-schema-to-typescript under-resolution artifact — - * the bundler inlined the same schema at two call sites and jsts emitted a numbered - * sibling. The body it produced was strictly weaker than `Responsive` (missing the - * discriminator, canonical wrapper, or named union); aliasing to `Responsive` - * gives consumers the correctly-discriminated shape that matches the wire format. - * - * @deprecated Use `Responsive` from `@adcp/sdk/types`. Slated for removal in the next major. - */ -export type Responsive2 = Responsive; /** * Represents a purchased advertising campaign */ @@ -14086,239 +15201,91 @@ export interface PriceAdjustment { beneficiary?: string; } export interface PriceBreakdown { - list_price: number; - adjustments: PriceAdjustment[]; -} -export interface PackageTargetingResolution { - demographics: DemographicTargetingResolution; - ext?: ExtensionObject; -} -/** - * Canonical demographic predicate actually applied by the seller. - */ -export interface DemographicPredicate { - age: DemographicAgeRange; -} -/** - * Re-export of `BrandReference` under the legacy codegen artifact name. - * - * `BrandReference4` is a json-schema-to-typescript under-resolution artifact — - * the bundler inlined the same schema at two call sites and jsts emitted a numbered - * sibling. The body it produced was strictly weaker than `BrandReference` (missing the - * discriminator, canonical wrapper, or named union); aliasing to `BrandReference` - * gives consumers the correctly-discriminated shape that matches the wire format. - * - * @deprecated Use `BrandReference` from `@adcp/sdk/types`. Slated for removal in the next major. - */ -export type BrandReference4 = BrandReference; -/** - * A rate threshold for a performance metric, measured by a specified vendor. The threshold is a floor or ceiling depending on the metric: viewability, completion_rate, brand_safety, and attention_score are floors (must exceed); ivt is a ceiling (must not exceed). - */ -export interface PerformanceStandard { - metric: PerformanceStandardMetric; - /** - * Rate threshold as a decimal (e.g., 0.70 for 70%). Whether this is a floor or ceiling depends on the metric: for viewability, completion_rate, brand_safety, attention_score the actual rate must be >= threshold; for ivt the actual rate must be <= threshold. - */ - threshold: number; - standard?: ViewabilityStandard; - vendor: BrandReference5; -} -/** - * Re-export of `BrandReference` under the legacy codegen artifact name. - * - * `BrandReference5` is a json-schema-to-typescript under-resolution artifact — - * the bundler inlined the same schema at two call sites and jsts emitted a numbered - * sibling. The body it produced was strictly weaker than `BrandReference` (missing the - * discriminator, canonical wrapper, or named union); aliasing to `BrandReference` - * gives consumers the correctly-discriminated shape that matches the wire format. - * - * @deprecated Use `BrandReference` from `@adcp/sdk/types`. Slated for removal in the next major. - */ -export type BrandReference5 = BrandReference; -/** - * Re-export of `BrandReference` under the legacy codegen artifact name. - * - * `BrandReference6` is a json-schema-to-typescript under-resolution artifact — - * the bundler inlined the same schema at two call sites and jsts emitted a numbered - * sibling. The body it produced was strictly weaker than `BrandReference` (missing the - * discriminator, canonical wrapper, or named union); aliasing to `BrandReference` - * gives consumers the correctly-discriminated shape that matches the wire format. - * - * @deprecated Use `BrandReference` from `@adcp/sdk/types`. Slated for removal in the next major. - */ -export type BrandReference6 = BrandReference; -/** - * Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref. - */ -export interface CreativeLocalePolicy { - /** - * Concrete canonical BCP 47 language ranges accepted by this format option. RFC 4647 Basic Filtering is directional: seller range fr accepts variant fr-CA, but seller range fr-CA does not accept variant fr or fr-FR. Use zxx explicitly for language-neutral creative; und means unknown and is not a wildcard. - * - * @minItems 1 - * @maxItems 50 - */ - accepted_language_ranges: [LanguageTag, ...LanguageTag[]]; -} -export interface ImageFormatDeclaration { - format_kind: 'image'; - params: CanonicalFormatImage; -} -/** - * Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints). - */ -export interface CanonicalFormatBase { - /** - * When true, this canonical or seller narrowing may not work as declared. Adopters SHOULD preflight it with validate_input or in a sandbox and SHOULD NOT route production budget without testing; experimental status never makes the deprecated v1 path preferable. Drivers include unsettled spec shape, an adopter runtime gap, and custom shapes awaiting promotion. This replaces the earlier status plus runtime_status axes. Sellers SHOULD set experimental whenever a canonical or declaration is not production-ready. - */ - experimental?: boolean; - /** - * When true, this canonical (or a seller's specific narrowing of it) is going away. Existing adopters are supported through the deprecation cycle; new adoption is discouraged. Pair with `migration_target_version` to indicate when the canonical is expected to be removed. Distinct from `experimental`: an experimental canonical may stabilize and stop being experimental; a deprecated canonical is on a sunset path. - */ - deprecated?: boolean; - /** - * Whether this canonical has any v1 named-format equivalent. `true` (default) — the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1→v2 projection via `v1-canonical-mapping.json` is meaningful. `false` — the canonical is inherently new in v2 and has no v1 form; v1's `list_creative_formats` couldn't express it because the underlying concept (algorithmic surface composition, AI-surface mentions, retail-media catalog placements, multi-card carousels) didn't exist as a v1 named-format archetype. - * - * Lets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The 4 inherently-v2 canonicals at 3.1 GA: `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`. - */ - v1_translatable?: boolean; - /** - * AdCP MAJOR.MINOR version that introduced this canonical (e.g., '3.1', '3.2'). Lets adopters reason about minimum protocol version requirements when consuming a format declaration. Patch precision is intentionally rejected — canonicals are introduced at minor-version boundaries. - */ - since_version?: string; - /** - * AdCP MAJOR.MINOR version by which the working group expects this canonical to stabilize, surface a breaking revision, or (when `deprecated: true`) be removed. Patch precision is intentionally rejected — canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown'). - */ - migration_target_version?: string; - /** - * Whether the surface composes deterministically (buyer can predict per-slot rendering — sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing — responsive_creative, agent_placement). - */ - composition_model?: 'deterministic' | 'algorithmic'; - /** - * When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent. - */ - provenance_required?: boolean; - /** - * Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly. - * - * **Collision precedence (normative).** When two or more `platform_extensions[]` entries on the same declaration extend the same target (e.g., both extend `tracking`) with overlapping field names, **array order is authoritative — later entries override earlier ones on a per-field basis** (last-in-array-wins). SDKs MUST surface the overlap via the `errors[]` array on the `get_products` response with a structured code (`FORMAT_DECLARATION_DIVERGENT` is appropriate when the overlap appears across dual-emitted shapes; a producer-self-emitted overlap on a single declaration SHOULD use the same code with `error.details: { collision_kind: "platform_extension_field", target, overlapping_fields, winning_extension_uri }`). Producers SHOULD avoid the collision by emitting one extension per target or by partitioning fields across extensions; the deterministic precedence is for last-resort consistency across SDK implementations, not a sanctioned merging strategy. - */ - platform_extensions?: PlatformExtensionReference1[]; - /** - * When true, the format's production pipeline is genuinely nondeterministic — the platform cannot guarantee that synthesis from a given input set produces in-spec output. Veo / Sora / Runway-class generative video, and other AI-synthesis flows where output dimensions, duration, or quality vary per run. Implies a different validation contract: predictive `validate_input` is impossible; the platform's own post-synthesis QA loop applies; if the QA loop exhausts without producing a valid artifact, `build_creative` returns task_failed with a synthesis_failed reason. Distinct from `composition_model` (which describes how the surface composes per-slot rendering, not whether synthesis is deterministic). When false or absent, the format's production is predictable enough that `validate_input` can predict output properties from input properties. - * - * **Compatibility with `asset_source` / `item_production_model`**: `synthesis_nondeterministic: true` MAY pair with any of `seller_pre_rendered_from_brief`, `seller_human_designed`, or `agent_synthesized` (the QA loop is concept-level, not source-specific — 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error. - */ - synthesis_nondeterministic?: boolean; - /** - * Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry. - */ - slots?: { - }[]; - /** - * Downstream platform connections or grants required to use this format declaration. These are in addition to the single AdCP caller credential. Use this when a platform product requires multiple downstream grants, such as an advertiser account connection plus a publisher identity or post authorization for published-post references. - */ - required_connections?: DownstreamConnectionRequirement[]; - /** - * Policy for formats whose `slots` accept a `published_post` reference. `immutable_snapshot`: seller snapshots the referenced post at approval and later source changes do not change the served creative. `mutable_requires_reapproval`: the source post may change and material changes require review before continued serving. `mutable_auto_recheck`: the source post may change and the seller continuously or periodically rechecks authorization/policy without requiring buyer resubmission. Omit when the format has no `published_post` slot. - */ - reference_mutability?: 'immutable_snapshot' | 'mutable_requires_reapproval' | 'mutable_auto_recheck'; - /** - * Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative). - */ - production_window_business_days?: number; -} -/** - * Re-export of `PlatformExtensionReference` under the legacy codegen artifact name. - * - * `PlatformExtensionReference1` is a json-schema-to-typescript under-resolution artifact — - * the bundler inlined the same schema at two call sites and jsts emitted a numbered - * sibling. The body it produced was strictly weaker than `PlatformExtensionReference` (missing the - * discriminator, canonical wrapper, or named union); aliasing to `PlatformExtensionReference` - * gives consumers the correctly-discriminated shape that matches the wire format. - * - * @deprecated Use `PlatformExtensionReference` from `@adcp/sdk/types`. Slated for removal in the next major. - */ -export type PlatformExtensionReference1 = PlatformExtensionReference; -export interface HTML5FormatDeclaration { - format_kind: 'html5'; - params: CanonicalFormatHTML5Banner; -} -/** - * Re-export of `Fixed` under the legacy codegen artifact name. - * - * `Fixed1` is a json-schema-to-typescript under-resolution artifact — - * the bundler inlined the same schema at two call sites and jsts emitted a numbered - * sibling. The body it produced was strictly weaker than `Fixed` (missing the - * discriminator, canonical wrapper, or named union); aliasing to `Fixed` - * gives consumers the correctly-discriminated shape that matches the wire format. - * - * @deprecated Use `Fixed` from `@adcp/sdk/types`. Slated for removal in the next major. + list_price: number; + adjustments: PriceAdjustment[]; +} +export interface PackageTargetingResolution { + demographics: DemographicTargetingResolution; + ext?: ExtensionObject; +} +/** + * Canonical demographic predicate actually applied by the seller. */ -export type Fixed1 = Fixed; +export interface DemographicPredicate { + age: DemographicAgeRange; +} /** - * Re-export of `MultiSize` under the legacy codegen artifact name. + * Re-export of `BrandReference` under the legacy codegen artifact name. * - * `MultiSize1` is a json-schema-to-typescript under-resolution artifact — + * `BrandReference4` is a json-schema-to-typescript under-resolution artifact — * the bundler inlined the same schema at two call sites and jsts emitted a numbered - * sibling. The body it produced was strictly weaker than `MultiSize` (missing the - * discriminator, canonical wrapper, or named union); aliasing to `MultiSize` + * sibling. The body it produced was strictly weaker than `BrandReference` (missing the + * discriminator, canonical wrapper, or named union); aliasing to `BrandReference` * gives consumers the correctly-discriminated shape that matches the wire format. * - * @deprecated Use `MultiSize` from `@adcp/sdk/types`. Slated for removal in the next major. + * @deprecated Use `BrandReference` from `@adcp/sdk/types`. Slated for removal in the next major. */ -export type MultiSize1 = MultiSize; +export type BrandReference4 = BrandReference; /** - * Re-export of `None` under the legacy codegen artifact name. - * - * `None1` is a json-schema-to-typescript under-resolution artifact — - * the bundler inlined the same schema at two call sites and jsts emitted a numbered - * sibling. The body it produced was strictly weaker than `None` (missing the - * discriminator, canonical wrapper, or named union); aliasing to `None` - * gives consumers the correctly-discriminated shape that matches the wire format. - * - * @deprecated Use `None` from `@adcp/sdk/types`. Slated for removal in the next major. + * A rate threshold for a performance metric, measured by a specified vendor. The threshold is a floor or ceiling depending on the metric: viewability, completion_rate, brand_safety, and attention_score are floors (must exceed); ivt is a ceiling (must not exceed). */ -export type None1 = None; -export interface DisplayTagFormatDeclaration { - format_kind: 'display_tag'; - params: CanonicalFormatDisplayTag; +export interface PerformanceStandard { + metric: PerformanceStandardMetric; + /** + * Rate threshold as a decimal (e.g., 0.70 for 70%). Whether this is a floor or ceiling depends on the metric: for viewability, completion_rate, brand_safety, attention_score the actual rate must be >= threshold; for ivt the actual rate must be <= threshold. + */ + threshold: number; + standard?: ViewabilityStandard; + vendor: BrandReference5; } /** - * Re-export of `Fixed` under the legacy codegen artifact name. + * Re-export of `BrandReference` under the legacy codegen artifact name. * - * `Fixed2` is a json-schema-to-typescript under-resolution artifact — + * `BrandReference5` is a json-schema-to-typescript under-resolution artifact — * the bundler inlined the same schema at two call sites and jsts emitted a numbered - * sibling. The body it produced was strictly weaker than `Fixed` (missing the - * discriminator, canonical wrapper, or named union); aliasing to `Fixed` + * sibling. The body it produced was strictly weaker than `BrandReference` (missing the + * discriminator, canonical wrapper, or named union); aliasing to `BrandReference` * gives consumers the correctly-discriminated shape that matches the wire format. * - * @deprecated Use `Fixed` from `@adcp/sdk/types`. Slated for removal in the next major. + * @deprecated Use `BrandReference` from `@adcp/sdk/types`. Slated for removal in the next major. */ -export type Fixed2 = Fixed; +export type BrandReference5 = BrandReference; /** - * Re-export of `MultiSize` under the legacy codegen artifact name. + * Re-export of `BrandReference` under the legacy codegen artifact name. * - * `MultiSize2` is a json-schema-to-typescript under-resolution artifact — + * `BrandReference6` is a json-schema-to-typescript under-resolution artifact — * the bundler inlined the same schema at two call sites and jsts emitted a numbered - * sibling. The body it produced was strictly weaker than `MultiSize` (missing the - * discriminator, canonical wrapper, or named union); aliasing to `MultiSize` + * sibling. The body it produced was strictly weaker than `BrandReference` (missing the + * discriminator, canonical wrapper, or named union); aliasing to `BrandReference` * gives consumers the correctly-discriminated shape that matches the wire format. * - * @deprecated Use `MultiSize` from `@adcp/sdk/types`. Slated for removal in the next major. + * @deprecated Use `BrandReference` from `@adcp/sdk/types`. Slated for removal in the next major. */ -export type MultiSize2 = MultiSize; +export type BrandReference6 = BrandReference; +export interface ImageFormatDeclaration { + format_kind: 'image'; + params: CanonicalFormatImage; +} /** - * Re-export of `None` under the legacy codegen artifact name. + * Re-export of `PlatformExtensionReference` under the legacy codegen artifact name. * - * `None2` is a json-schema-to-typescript under-resolution artifact — + * `PlatformExtensionReference1` is a json-schema-to-typescript under-resolution artifact — * the bundler inlined the same schema at two call sites and jsts emitted a numbered - * sibling. The body it produced was strictly weaker than `None` (missing the - * discriminator, canonical wrapper, or named union); aliasing to `None` + * sibling. The body it produced was strictly weaker than `PlatformExtensionReference` (missing the + * discriminator, canonical wrapper, or named union); aliasing to `PlatformExtensionReference` * gives consumers the correctly-discriminated shape that matches the wire format. * - * @deprecated Use `None` from `@adcp/sdk/types`. Slated for removal in the next major. + * @deprecated Use `PlatformExtensionReference` from `@adcp/sdk/types`. Slated for removal in the next major. */ -export type None2 = None; +export type PlatformExtensionReference1 = PlatformExtensionReference; +export interface HTML5FormatDeclaration { + format_kind: 'html5'; + params: CanonicalFormatHTML5Banner; +} +export interface DisplayTagFormatDeclaration { + format_kind: 'display_tag'; + params: CanonicalFormatDisplayTag; +} export interface ImageCarouselFormatDeclaration { format_kind: 'image_carousel'; params: CanonicalFormatImageCarousel; @@ -14355,8 +15322,16 @@ export interface AgentPlacementFormatDeclaration { format_kind: 'agent_placement'; params: CanonicalFormatAgentPlacementAISurfaceSponsoredPlacement; } +export interface SellerRenderedStatefulDisplayFormatDeclaration { + format_kind: 'seller_rendered_stateful_display'; + params: CanonicalFormatSellerRenderedStatefulDisplay; +} +export interface CoordinatedPlacementsFormatDeclaration { + format_kind: 'coordinated_placements'; + params: CanonicalFormatCoordinatedPlacements; +} /** - * Adopter-defined shape that doesn't fit the 12 canonicals. Requires `format_shape` (vocabulary-registered global pattern) and `format_schema` (URI+digest reference to a fetchable schema describing the actual params/slots). `params` shape is governed by the fetched schema rather than baked into AdCP — kept as `type: object` here with `additionalProperties: true` because the canonical schema validates dynamically post-fetch. + * Adopter-defined shape that doesn't fit the 14 canonicals. Requires `format_shape` (vocabulary-registered global pattern) and `format_schema` (URI+digest reference to a fetchable schema describing the actual params/slots). `params` shape is governed by the fetched schema rather than baked into AdCP — kept as `type: object` here with `additionalProperties: true` because the canonical schema validates dynamically post-fetch. */ export interface CustomFormatDeclaration { format_kind: 'custom'; @@ -15714,6 +16689,10 @@ export interface ReportingCapabilities { * Whether this product supports creative-level metric breakdowns in delivery reporting (by_creative within by_package) */ supports_creative_breakdown?: boolean; + /** + * Whether this product supports canonical creative-format breakdowns in GET delivery reporting (by_format within by_package, keyed by format_kind). This is independent from supports_creative_breakdown because a seller may expose aggregate format-grain reporting without exposing individual creative performance. + */ + supports_format_breakdown?: boolean; /** * Whether this product supports keyword-level metric breakdowns in delivery reporting (by_keyword within by_package) */ @@ -17156,135 +18135,6 @@ export type CanonicalReportingCommitment = qualifier?: CanonicalMetricQualifier; effective_at?: string; }; -/** - * Compact canonical creative-format declaration. Legacy named-format links are intentionally absent; params are validated against the canonical schema selected by format_kind without inlining every format union into product discovery. - */ -export type CanonicalFormatOption = { -} & { - format_option_id?: string; - publisher_domain?: string; - display_name?: string; - sample_render_url?: string; - /** - * @minItems 1 - */ - applies_to_channels?: [MediaChannel, ...MediaChannel[]]; - seller_preference?: 'preferred' | 'accepted' | 'discouraged'; - locale_policy?: CreativeLocalePolicy; - canonical_formats_only?: boolean; - experimental?: boolean; - format_kind: - | 'image' - | 'html5' - | 'display_tag' - | 'image_carousel' - | 'video_hosted' - | 'video_vast' - | 'audio_hosted' - | 'audio_daast' - | 'sponsored_placement' - | 'native_in_feed' - | 'responsive_creative' - | 'agent_placement' - | 'custom'; - params: { - }; - format_shape?: string; - format_schema?: PlatformExtensionReference1; -} & { - format_option_id?: string; - publisher_domain?: string; - display_name?: string; - sample_render_url?: string; - /** - * @minItems 1 - */ - applies_to_channels?: [MediaChannel, ...MediaChannel[]]; - seller_preference?: 'preferred' | 'accepted' | 'discouraged'; - locale_policy?: CreativeLocalePolicy; - canonical_formats_only?: boolean; - experimental?: boolean; - format_kind: - | 'image' - | 'html5' - | 'display_tag' - | 'image_carousel' - | 'video_hosted' - | 'video_vast' - | 'audio_hosted' - | 'audio_daast' - | 'sponsored_placement' - | 'native_in_feed' - | 'responsive_creative' - | 'agent_placement' - | 'custom'; - params: { - }; - format_shape?: string; - format_schema?: PlatformExtensionReference1; -} & { - format_option_id?: string; - publisher_domain?: string; - display_name?: string; - sample_render_url?: string; - /** - * @minItems 1 - */ - applies_to_channels?: [MediaChannel, ...MediaChannel[]]; - seller_preference?: 'preferred' | 'accepted' | 'discouraged'; - locale_policy?: CreativeLocalePolicy; - canonical_formats_only?: boolean; - experimental?: boolean; - format_kind: - | 'image' - | 'html5' - | 'display_tag' - | 'image_carousel' - | 'video_hosted' - | 'video_vast' - | 'audio_hosted' - | 'audio_daast' - | 'sponsored_placement' - | 'native_in_feed' - | 'responsive_creative' - | 'agent_placement' - | 'custom'; - params: { - }; - format_shape?: string; - format_schema?: PlatformExtensionReference1; -} & { - format_option_id?: string; - publisher_domain?: string; - display_name?: string; - sample_render_url?: string; - /** - * @minItems 1 - */ - applies_to_channels?: [MediaChannel, ...MediaChannel[]]; - seller_preference?: 'preferred' | 'accepted' | 'discouraged'; - locale_policy?: CreativeLocalePolicy; - canonical_formats_only?: boolean; - experimental?: boolean; - format_kind: - | 'image' - | 'html5' - | 'display_tag' - | 'image_carousel' - | 'video_hosted' - | 'video_vast' - | 'audio_hosted' - | 'audio_daast' - | 'sponsored_placement' - | 'native_in_feed' - | 'responsive_creative' - | 'agent_placement' - | 'custom'; - params: { - }; - format_shape?: string; - format_schema?: PlatformExtensionReference1; -}; /** * Compact product placement with canonical format narrowing only. */ @@ -18066,6 +18916,7 @@ export type BuildCreativeResponse = AdCPVersionEnvelope & * The generated or transformed creative manifest */ export type CreativeManifest = { +} & { format_id?: FormatReferenceStructuredObject; format_kind?: CanonicalFormatKind; format_option_ref?: FormatOptionReference; @@ -18077,6 +18928,12 @@ export type CreativeManifest = { assets: { [k: string]: AssetVariant | AssetVariant[]; }; + /** + * Component-addressed asset maps for `coordinated_placements`. Each key MUST match one `params.components[].component_id`; its value supplies that component's canonical slots. Shared assets remain in top-level `assets` and are injected only into components named by `shared_slots[].consumed_by`. This namespace allows two components to use the same canonical slot name, such as `image_main`, without collision. It MUST be absent for non-`coordinated_placements` manifests. + */ + component_assets?: { + [k: string]: CreativeAssets | undefined; + }; brand?: BrandReference11; /** * Rights constraints attached to this creative. Buyer-carried fields are informational until a serving party evaluates an issuer-bound attestation reference under its own policy. Only a verified, unexpired, unrevoked, digest-matched evaluation can support serving authorization; verification_url is never authority. @@ -19140,6 +19997,7 @@ export interface CanonicalReportingCapabilities { metric_id: VendorMetricID; }[]; supports_creative_breakdown?: boolean; + supports_format_breakdown?: boolean; supports_keyword_breakdown?: boolean; supports_geo_breakdown?: GeographicBreakdownSupport; supports_device_type_breakdown?: boolean; @@ -26858,7 +27716,7 @@ export interface Format { manifest: {}; }; /** - * Metrics this format can produce in delivery reporting. Buyers receive the intersection of format reported_metrics and product available_metrics. If omitted, the format defers entirely to product-level metric declarations. + * Metrics this format can produce in delivery reporting. Buyers receive the intersection of format reported_metrics and product available_metrics. The intersection is evaluated under the container-subsumption rule in `enums/available-metric.json` — a container token (e.g. `viewability`) matches its leaf identities (e.g. `viewable_rate`), so mixed container/leaf declarations do not produce an empty intersection. If omitted, the format defers entirely to product-level metric declarations. */ reported_metrics?: AvailableMetric[]; /** @@ -27685,6 +28543,16 @@ export type MissingMetric = scope: 'vendor'; vendor: BrandReference; metric_id: VendorMetricID; + /** + * Mirrors the qualifier on the corresponding vendor-scope `committed_metrics` entry. MUST match that entry so reconciliation joins on (vendor, metric_id, qualifier). + */ + qualifier?: { + viewability_standard?: ViewabilityStandard; + completion_source?: CompletionSource; + attribution_methodology?: AttributionMethodology; + attribution_window?: Duration; + lift_dimension?: LiftDimension; + }; }; // core/offering.json @@ -27840,6 +28708,16 @@ export type PerformanceFeedbackMetric = scope: 'vendor'; vendor: BrandReference; metric_id: VendorMetricID; + /** + * Optional disambiguator mirroring the vendor-scope qualifier on `committed_metrics` — same closed key set as standard-scope entries. + */ + qualifier?: { + viewability_standard?: ViewabilityStandard; + completion_source?: CompletionSource; + attribution_methodology?: AttributionMethodology; + attribution_window?: Duration; + lift_dimension?: LiftDimension; + }; }; /** * One optimizer-ready assertion about a media buy, package, or creative. This is a compact projection of buyer- or measurement-provider analysis, not a transport for raw exposure logs, attribution models, or complete study datasets. @@ -27997,6 +28875,16 @@ export interface PerformanceFeedback { scope: 'vendor'; vendor: BrandReference; metric_id: VendorMetricID; + /** + * Optional disambiguator mirroring the vendor-scope qualifier on `committed_metrics` — same closed key set as standard-scope entries. + */ + qualifier?: { + viewability_standard?: ViewabilityStandard; + completion_source?: CompletionSource; + attribution_methodology?: AttributionMethodology; + attribution_window?: Duration; + lift_dimension?: LiftDimension; + }; }; feedback_source: FeedbackSource; vendor?: BrandReference1; @@ -28125,7 +29013,7 @@ export type InlineDeclaration = { */ experimental?: boolean; /** - * REQUIRED when `format_kind: "custom"`; otherwise MUST be absent. Recognized global pattern this custom shape is an instance of, drawn from the [format-shape vocabulary registry](/schemas/core/format-shape-vocabulary.json) (`multi_placement_takeover`, `roadblock`, `branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values valid (validators MAY soft-warn) — adopters CAN ship a shape that isn't yet in the registry. Adding entries is a vocabulary PR. Once a `format_shape` entry sees 2+ adopters with substantively similar `format_schema` content for 90+ days, the working group promotes it to a first-class canonical. + * REQUIRED when `format_kind: "custom"`; otherwise MUST be absent. Recognized global pattern this custom shape is an instance of, drawn from the [format-shape vocabulary registry](/schemas/core/format-shape-vocabulary.json) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are valid (validators MAY soft-warn) — adopters CAN ship a shape that isn't yet in the registry. Adding entries is a vocabulary PR. Once a `format_shape` entry sees 2+ adopters with substantively similar `format_schema` content for 90+ days, the working group may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format. */ format_shape?: string; /** @@ -28166,6 +29054,8 @@ export type InlineDeclaration = { | NativeInFeedFormatDeclaration | ResponsiveCreativeFormatDeclaration | AgentPlacementFormatDeclaration + | SellerRenderedStatefulDisplayFormatDeclaration + | CoordinatedPlacementsFormatDeclaration | CustomFormatDeclaration ); /** @@ -28740,7 +29630,7 @@ export interface ProductFilters { */ required_performance_standards?: PerformanceStandard[]; /** - * Filter to products whose `reporting_capabilities.available_metrics` is a superset of these metrics — i.e., products that commit to reporting all listed metrics in delivery responses. Use this for capability-level discovery (e.g., 'I need products that report `completed_views` for a CTV CPCV buy'); guarantee-level requirements with thresholds belong in `required_performance_standards` and `measurement_terms`. Sellers MUST silently exclude products that cannot meet this list (filter-not-fail; do not return an error). The product's declared `available_metrics` becomes the binding reporting contract carried into the resulting media buy — the same metric vocabulary is used to compute `missing_metrics` on `get_media_buy_delivery`. + * Filter to products whose `reporting_capabilities.available_metrics` is a superset of these metrics — i.e., products that commit to reporting all listed metrics in delivery responses. Use this for capability-level discovery (e.g., 'I need products that report `completed_views` for a CTV CPCV buy'); guarantee-level requirements with thresholds belong in `required_performance_standards` and `measurement_terms`. Sellers MUST silently exclude products that cannot meet this list (filter-not-fail; do not return an error). Superset evaluation follows the container-subsumption rule in `enums/available-metric.json` — a product declaring the `viewability` container satisfies a requirement for the `viewable_rate` leaf. The product's declared `available_metrics` becomes the binding reporting contract carried into the resulting media buy — the same metric vocabulary is used to compute `missing_metrics` on `get_media_buy_delivery`. */ required_metrics?: AvailableMetric[]; /** @@ -29577,7 +30467,7 @@ export interface ReportingWebhook { */ reporting_frequency: 'hourly' | 'daily' | 'monthly'; /** - * Optional list of metrics to include in webhook notifications. If omitted, all available metrics are included. Must be subset of product's available_metrics. + * Optional list of metrics to include in webhook notifications. If omitted, all available metrics are included; an empty array has the same meaning as omission (it does not narrow to impressions and spend only). impressions and spend are always included regardless of this list. Must be subset of product's available_metrics. Subset evaluation and leaf resolution follow the container-subsumption rule in `enums/available-metric.json`: requesting a leaf identity selects its canonical carrier object in the payload. Same narrowing semantics as get_media_buy_delivery's requested_metrics (which additionally requires at least one entry when present). */ requested_metrics?: AvailableMetric[]; } @@ -32242,6 +33132,14 @@ export interface ValidateInputResult { */ retry_with?: {}; }[]; + /** + * Non-blocking observations (e.g. LEAN policy advisories such as hover-triggered expansion or non-user-initiated entry into overlay anchoring) that do not affect `result_kind`. MAY be present alongside `validated_pass`, `validated_fail`, or `unvalidatable_nondeterministic`. Same item shape as `violations`. + */ + warnings?: { + rule: string; + expected?: unknown; + predicted?: unknown; + }[]; } diff --git a/src/lib/types/enums.generated.ts b/src/lib/types/enums.generated.ts index 48e8a499a..e7fd126f2 100644 --- a/src/lib/types/enums.generated.ts +++ b/src/lib/types/enums.generated.ts @@ -38,14 +38,14 @@ export const AudioChannelLayoutValues = ["mono", "stereo", "5.1", "7.1"] as cons export const AudioDistributionTypeValues = ["music_streaming_service", "fm_am_broadcast", "podcast", "catch_up_radio", "web_radio", "video_game", "text_to_speech"] as const; export const AuthenticationSchemeValues = ["Bearer", "HMAC-SHA256"] as const; export const AvailabilityStatusValues = ["available", "unavailable"] as const; -export const AvailableMetricValues = ["impressions", "spend", "clicks", "ctr", "views", "completed_views", "completion_rate", "conversions", "conversion_value", "commissionable_value", "roas", "cost_per_acquisition", "new_to_brand_rate", "leads", "reach", "frequency", "grps", "engagements", "engagement_rate", "follows", "saves", "profile_visits", "viewability", "quartile_data", "dooh_metrics", "cost_per_click", "cost_per_completed_view", "cpm", "downloads", "units_sold", "new_to_brand_units", "plays", "incremental_sales_lift", "brand_lift", "foot_traffic", "conversion_lift", "brand_search_lift"] as const; +export const AvailableMetricValues = ["impressions", "spend", "clicks", "ctr", "views", "completed_views", "completion_rate", "conversions", "conversion_value", "commissionable_value", "roas", "cost_per_acquisition", "new_to_brand_rate", "leads", "reach", "frequency", "grps", "engagements", "engagement_rate", "follows", "saves", "profile_visits", "viewability", "viewable_rate", "viewable_impressions", "measurable_impressions", "viewed_seconds", "quartile_data", "quartile_25", "quartile_50", "quartile_75", "quartile_100", "time_based_views", "dooh_metrics", "cost_per_click", "cost_per_completed_view", "cpm", "downloads", "units_sold", "new_to_brand_units", "plays", "incremental_sales_lift", "brand_lift", "foot_traffic", "conversion_lift", "brand_search_lift"] as const; export const BillingPartyValues = ["operator", "agent", "advertiser"] as const; export const BinaryVerdictValues = ["pass", "fail"] as const; export const BrandAgentTypeValues = ["brand", "rights", "measurement", "governance", "creative", "sales", "buying", "signals"] as const; export const BrowserFamilyValues = ["chrome", "safari", "firefox", "edge", "opera", "samsung_internet", "android_webview", "other", "unknown"] as const; export const C2PAWatermarkActionValues = ["c2pa.watermarked.bound", "c2pa.watermarked.unbound"] as const; export const CanceledByValues = ["buyer", "seller"] as const; -export const CanonicalFormatKindValues = ["image", "html5", "display_tag", "image_carousel", "video_hosted", "video_vast", "audio_hosted", "audio_daast", "sponsored_placement", "native_in_feed", "responsive_creative", "agent_placement", "custom"] as const; +export const CanonicalFormatKindValues = ["image", "html5", "display_tag", "image_carousel", "video_hosted", "video_vast", "audio_hosted", "audio_daast", "sponsored_placement", "native_in_feed", "responsive_creative", "agent_placement", "seller_rendered_stateful_display", "coordinated_placements", "custom"] as const; export const CanonicalMediaBuyActionModeValues = ["self_serve", "conditional_self_serve", "requires_approval"] as const; export const CanonicalMediaBuyActionNameValues = ["pause", "resume", "cancel", "extend_flight", "shorten_flight", "update_flight_dates", "increase_budget", "decrease_budget", "reallocate_budget", "update_budget_allocation", "update_targeting", "update_pacing", "update_bidding", "update_frequency_caps", "update_catalog_assignments", "update_keywords", "update_optimization_goals", "update_impression_goal", "update_spend_target", "update_reporting_webhook", "replace_creative", "update_creative_assignments", "remove_creative", "add_packages", "remove_packages"] as const; export const CatalogActionValues = ["created", "updated", "unchanged", "failed", "deleted"] as const; @@ -65,14 +65,17 @@ export const ContentIDTypeValues = ["sku", "gtin", "offering_id", "job_id", "hot export const ContentRatingSystemValues = ["tv_parental", "mpaa", "podcast", "esrb", "bbfc", "fsk", "acb", "chvrs", "csa", "pegi", "custom"] as const; export const CountryFusedPostalCodeSystemValues = ["us_zip", "us_zip_plus_four", "gb_outward", "gb_full", "ca_fsa", "ca_full", "de_plz", "fr_code_postal", "au_postcode", "ch_plz", "at_plz"] as const; export const CreativeActionValues = ["created", "updated", "unchanged", "failed", "deleted"] as const; +export const CreativeActivationMethodValues = ["qr_code", "deep_link", "push_notification", "email", "tune_in", "text_message"] as const; export const CreativeAgentCapabilityValues = ["validation", "assembly", "generation", "preview", "delivery"] as const; export const CreativeApprovalStatusValues = ["pending_review", "approved", "partially_approved", "rejected"] as const; export const CreativeEventReasonCodeValues = ["review_passed", "review_failure", "processing_failure", "seller_rereview", "policy_revocation", "content_drift", "identity_authorization_revoked", "identity_authorization_expired", "source_private", "source_deleted", "takedown_request", "advertiser_request", "seller_archive", "account_closed", "account_suspended", "retention_expired", "legal_erasure"] as const; export const CreativeIdentifierTypeValues = ["ad_id", "isci", "clearcast_clock", "idcrea"] as const; +export const CreativeMotionLevelValues = ["static", "limited_motion", "full_motion"] as const; export const CreativeQualityValues = ["draft", "production"] as const; export const CreativeSelectionStrategyValues = ["audience_relevance", "contextual_fit", "performance", "proximity", "inventory_priority", "random"] as const; export const CreativeSortFieldValues = ["created_date", "updated_date", "name", "status", "assignment_count"] as const; export const CreativeStatusValues = ["processing", "pending_review", "approved", "suspended", "rejected", "archived"] as const; +export const CTVAdExperienceValues = ["menu", "pause", "screensaver", "overlay", "squeezeback", "in_scene"] as const; export const DAASTTrackingEventValues = ["impression", "creativeView", "start", "firstQuartile", "midpoint", "thirdQuartile", "complete", "mute", "unmute", "pause", "resume", "rewind", "skip", "progress", "clickTracking", "customClick", "close", "error", "viewable", "notViewable", "viewUndetermined", "measurableImpression", "viewableImpression"] as const; export const DAASTVersionValues = ["1.0", "1.1"] as const; export const DayOfWeekValues = ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"] as const; @@ -90,7 +93,7 @@ export const DisclosurePositionValues = ["prominent", "footer", "audio", "subtit export const DistanceUnitValues = ["km", "mi", "m"] as const; export const DistributionIdentifierTypeValues = ["apple_podcast_id", "spotify_collection_id", "rss_url", "podcast_guid", "amazon_music_id", "iheart_id", "podcast_index_id", "youtube_channel_id", "youtube_channel_handle", "youtube_channel_url", "youtube_playlist_id", "amazon_title_id", "roku_channel_id", "pluto_channel_id", "tubi_id", "peacock_id", "tiktok_id", "twitch_channel", "imdb_id", "gracenote_id", "eidr_id", "domain", "substack_id"] as const; export const EmbeddedProvenanceMethodValues = ["manifest_wrapper", "provenance_markers"] as const; -export const ErrorCodeValues = ["INVALID_REQUEST", "AUTH_REQUIRED", "AUTH_MISSING", "AUTH_INVALID", "AUTHORIZATION_REQUIRED", "RATE_LIMITED", "SERVICE_UNAVAILABLE", "CONFIGURATION_ERROR", "POLICY_VIOLATION", "PRODUCT_NOT_FOUND", "PRODUCT_UNAVAILABLE", "PROPOSAL_EXPIRED", "BUDGET_TOO_LOW", "CREATIVE_REJECTED", "CREATIVE_LOCALE_NOT_ACCEPTED", "CREATIVE_VALUE_NOT_ALLOWED", "UNSUPPORTED_FEATURE", "UNPRICEABLE_OUTPUT", "UNSUPPORTED_GRANULARITY", "UNSUPPORTED_PROVISIONING", "AUDIENCE_TOO_SMALL", "ACCOUNT_REQUIRED", "ACCOUNT_NOT_FOUND", "ACCOUNT_MOVED", "ACCOUNT_IDENTITY_CONFLICT", "ACCOUNT_SETUP_REQUIRED", "ACCOUNT_AMBIGUOUS", "ACCOUNT_PAYMENT_REQUIRED", "ACCOUNT_SUSPENDED", "COMPLIANCE_UNSATISFIED", "GOVERNANCE_DENIED", "BUDGET_EXHAUSTED", "BUDGET_EXCEEDED", "BUDGET_CAP_REACHED", "CONFLICT", "IDEMPOTENCY_CONFLICT", "IDEMPOTENCY_EXPIRED", "IDEMPOTENCY_IN_FLIGHT", "CREATIVE_DEADLINE_EXCEEDED", "CREATIVE_INACCESSIBLE", "INVALID_STATE", "MEDIA_BUY_NOT_FOUND", "NOT_CANCELLABLE", "PACKAGE_NOT_FOUND", "PLACE_TARGET_UNAVAILABLE", "CREATIVE_NOT_FOUND", "SIGNAL_NOT_FOUND", "SIGNAL_TARGETING_INCOMPATIBLE", "SESSION_NOT_FOUND", "PLAN_NOT_FOUND", "REFERENCE_NOT_FOUND", "SESSION_TERMINATED", "VALIDATION_ERROR", "PRODUCT_EXPIRED", "PROPOSAL_NOT_COMMITTED", "PROPOSAL_NOT_FOUND", "MULTI_FINALIZE_UNSUPPORTED", "IO_REQUIRED", "TERMS_REJECTED", "BIDDING_PLACEMENT_CONFLICT", "AMBIGUOUS_BIDDING_POLICY", "CONFLICTING_SELECTORS", "REQUOTE_REQUIRED", "VERSION_UNSUPPORTED", "CAMPAIGN_SUSPENDED", "GOVERNANCE_UNAVAILABLE", "PERMISSION_DENIED", "SCOPE_INSUFFICIENT", "READ_ONLY_SCOPE", "FIELD_NOT_PERMITTED", "PROVENANCE_REQUIRED", "PROVENANCE_DIGITAL_SOURCE_TYPE_MISSING", "PROVENANCE_SYNTHETIC_DEPICTION_MISSING", "PROVENANCE_DISCLOSURE_MISSING", "PROVENANCE_EMBEDDED_MISSING", "PROVENANCE_VERIFIER_NOT_ACCEPTED", "PROVENANCE_CLAIM_CONTRADICTED", "EVALUATOR_AGENT_NOT_ACCEPTED", "BILLING_NOT_SUPPORTED", "BILLING_NOT_PERMITTED_FOR_AGENT", "BILLING_OUT_OF_BAND", "PAYMENT_TERMS_NOT_SUPPORTED", "BRAND_REQUIRED", "AGENT_SUSPENDED", "AGENT_BLOCKED", "CREDENTIAL_IN_ARGS", "ACTION_NOT_ALLOWED", "PRIVATE_FIELD_IN_PUBLIC_PLACEMENT", "FORMAT_PROJECTION_FAILED", "FORMAT_DECLARATION_DIVERGENT", "FORMAT_DECLARATION_V1_AMBIGUOUS", "FORMAT_OPTION_UNRESOLVED", "FORMAT_DECLARATION_V1_LOSSY_MULTI_SIZE", "FORMAT_NOT_SUPPORTED", "PIXEL_TRACKER_LOSSY_DOWNGRADE", "PIXEL_TRACKER_UPGRADE_INFERRED", "STALE_RESPONSE", "FEED_FETCH_FAILED", "INVALID_FEED_FORMAT", "ITEM_VALIDATION_FAILED", "CATALOG_LIMIT_EXCEEDED", "INVALID_PRICING_OPTION", "INVALID_USAGE_DATA", "SIGNED_RESPONSE_ENVELOPE_EXPIRED", "SIGNED_RESPONSE_REQUEST_HASH_MISMATCH", "SIGNED_RESPONSE_TENANT_MISMATCH", "VAST_PARSE_FAILED", "VAST_VERSION_MISMATCH", "VAST_WRAPPER_DEPTH_EXCEEDED"] as const; +export const ErrorCodeValues = ["INVALID_REQUEST", "AUTH_REQUIRED", "AUTH_MISSING", "AUTH_INVALID", "AUTHORIZATION_REQUIRED", "RATE_LIMITED", "SERVICE_UNAVAILABLE", "CONFIGURATION_ERROR", "POLICY_VIOLATION", "PRODUCT_NOT_FOUND", "PRODUCT_UNAVAILABLE", "PROPOSAL_EXPIRED", "BUDGET_TOO_LOW", "CREATIVE_REJECTED", "CREATIVE_LOCALE_NOT_ACCEPTED", "CREATIVE_VALUE_NOT_ALLOWED", "UNSUPPORTED_FEATURE", "UNPRICEABLE_OUTPUT", "UNSUPPORTED_GRANULARITY", "UNSUPPORTED_PROVISIONING", "AUDIENCE_TOO_SMALL", "ACCOUNT_REQUIRED", "ACCOUNT_NOT_FOUND", "ACCOUNT_MOVED", "ACCOUNT_IDENTITY_CONFLICT", "ACCOUNT_SETUP_REQUIRED", "ACCOUNT_AMBIGUOUS", "ACCOUNT_PAYMENT_REQUIRED", "ACCOUNT_SUSPENDED", "COMPLIANCE_UNSATISFIED", "GOVERNANCE_DENIED", "BUDGET_EXHAUSTED", "BUDGET_EXCEEDED", "BUDGET_CAP_REACHED", "CONFLICT", "IDEMPOTENCY_CONFLICT", "IDEMPOTENCY_EXPIRED", "IDEMPOTENCY_IN_FLIGHT", "CREATIVE_DEADLINE_EXCEEDED", "CREATIVE_INACCESSIBLE", "INVALID_STATE", "MEDIA_BUY_NOT_FOUND", "NOT_CANCELLABLE", "PACKAGE_NOT_FOUND", "PLACE_TARGET_UNAVAILABLE", "CREATIVE_NOT_FOUND", "SIGNAL_NOT_FOUND", "SIGNAL_TARGETING_INCOMPATIBLE", "SESSION_NOT_FOUND", "PLAN_NOT_FOUND", "REFERENCE_NOT_FOUND", "SESSION_TERMINATED", "VALIDATION_ERROR", "PRODUCT_EXPIRED", "PROPOSAL_NOT_COMMITTED", "PROPOSAL_NOT_FOUND", "MULTI_FINALIZE_UNSUPPORTED", "IO_REQUIRED", "TERMS_REJECTED", "BIDDING_PLACEMENT_CONFLICT", "AMBIGUOUS_BIDDING_POLICY", "CONFLICTING_SELECTORS", "REQUOTE_REQUIRED", "VERSION_UNSUPPORTED", "CAMPAIGN_SUSPENDED", "GOVERNANCE_UNAVAILABLE", "PERMISSION_DENIED", "SCOPE_INSUFFICIENT", "READ_ONLY_SCOPE", "FIELD_NOT_PERMITTED", "PROVENANCE_REQUIRED", "PROVENANCE_DIGITAL_SOURCE_TYPE_MISSING", "PROVENANCE_SYNTHETIC_DEPICTION_MISSING", "PROVENANCE_DISCLOSURE_MISSING", "PROVENANCE_EMBEDDED_MISSING", "PROVENANCE_VERIFIER_NOT_ACCEPTED", "PROVENANCE_CLAIM_CONTRADICTED", "EVALUATOR_AGENT_NOT_ACCEPTED", "BILLING_NOT_SUPPORTED", "BILLING_NOT_PERMITTED_FOR_AGENT", "BILLING_OUT_OF_BAND", "PAYMENT_TERMS_NOT_SUPPORTED", "BRAND_REQUIRED", "AGENT_SUSPENDED", "AGENT_BLOCKED", "CREDENTIAL_IN_ARGS", "ACTION_NOT_ALLOWED", "PRIVATE_FIELD_IN_PUBLIC_PLACEMENT", "FORMAT_PROJECTION_FAILED", "FORMAT_DECLARATION_DIVERGENT", "FORMAT_SHAPE_PROMOTED", "FORMAT_DECLARATION_V1_AMBIGUOUS", "FORMAT_OPTION_UNRESOLVED", "FORMAT_DECLARATION_V1_LOSSY_MULTI_SIZE", "FORMAT_NOT_SUPPORTED", "PIXEL_TRACKER_LOSSY_DOWNGRADE", "PIXEL_TRACKER_UPGRADE_INFERRED", "STALE_RESPONSE", "FEED_FETCH_FAILED", "INVALID_FEED_FORMAT", "ITEM_VALIDATION_FAILED", "CATALOG_LIMIT_EXCEEDED", "INVALID_PRICING_OPTION", "INVALID_USAGE_DATA", "SIGNED_RESPONSE_ENVELOPE_EXPIRED", "SIGNED_RESPONSE_REQUEST_HASH_MISMATCH", "SIGNED_RESPONSE_TENANT_MISMATCH", "VAST_PARSE_FAILED", "VAST_VERSION_MISMATCH", "VAST_WRAPPER_DEPTH_EXCEEDED"] as const; export const ErrorScopeValues = ["capability", "account", "agent"] as const; export const EscalationSeverityValues = ["info", "warning", "critical"] as const; export const EventTypeValues = ["page_view", "view_content", "select_content", "select_item", "search", "share", "add_to_cart", "remove_from_cart", "viewed_cart", "add_to_wishlist", "initiate_checkout", "add_payment_info", "purchase", "refund", "lead", "qualify_lead", "close_convert_lead", "disqualify_lead", "complete_registration", "subscribe", "follow", "content_view", "watch_milestone", "start_trial", "app_install", "app_launch", "contact", "schedule", "donate", "submit_application", "custom"] as const; @@ -176,7 +179,7 @@ export const SISessionStatusValues = ["active", "pending_handoff", "complete", " export const SnapshotUnavailableReasonValues = ["SNAPSHOT_UNSUPPORTED", "SNAPSHOT_TEMPORARILY_UNAVAILABLE", "SNAPSHOT_PERMISSION_DENIED"] as const; export const SocialPlacementSurfaceValues = ["feed", "stories", "short_video", "explore", "search"] as const; export const SortDirectionValues = ["asc", "desc"] as const; -export const SortMetricValues = ["impressions", "spend", "clicks", "ctr", "views", "completed_views", "completion_rate", "conversions", "conversion_value", "roas", "cost_per_acquisition", "new_to_brand_rate", "leads", "grps", "reach", "frequency", "engagements", "follows", "saves", "profile_visits", "engagement_rate", "cost_per_click"] as const; +export const SortMetricValues = ["impressions", "spend", "clicks", "ctr", "views", "completed_views", "completion_rate", "conversions", "conversion_value", "roas", "cost_per_acquisition", "new_to_brand_rate", "leads", "grps", "reach", "frequency", "engagements", "follows", "saves", "profile_visits", "engagement_rate", "cost_per_click", "commissionable_value", "plays", "cost_per_completed_view", "cpm", "downloads", "units_sold", "new_to_brand_units", "viewable_rate", "viewable_impressions", "measurable_impressions", "viewed_seconds", "quartile_25", "quartile_50", "quartile_75", "quartile_100"] as const; export const SpecialCategoryValues = ["awards", "championship", "concert", "conference", "election", "festival", "gala", "holiday", "premiere", "product_launch", "reunion", "tribute"] as const; export const SponsoredPlacementTypeValues = ["sponsored_search", "sponsored_display", "sponsored_native"] as const; export const StandardScopeValues = ["attestation_verifier"] as const; @@ -197,6 +200,7 @@ export const VASTVersionValues = ["2.0", "3.0", "4.0", "4.1", "4.2", "4.3"] as c export const VerificationStatusValues = ["owned", "pending_review", "transferring", "disputed", "not_ours", "archived", "licensed_in", "licensed_out", "unknown"] as const; export const VideoPlacementTypeValues = ["instream", "accompanying_content", "interstitial", "standalone"] as const; export const ViewabilityStandardValues = ["mrc", "groupm"] as const; +export const ViewThresholdBasisValues = ["play_time", "in_view"] as const; export const WarningCodeValues = ["inventory_shortfall_forecast", "flight_change_creates_pacing_risk", "fields_ignored_due_to_precedence"] as const; export const WatermarkMediaTypeValues = ["audio", "image", "video", "text"] as const; export const WCAGLevelValues = ["A", "AA", "AAA"] as const; diff --git a/src/lib/types/inline-enums.generated.ts b/src/lib/types/inline-enums.generated.ts index e7ac5ff68..a542c0a88 100644 --- a/src/lib/types/inline-enums.generated.ts +++ b/src/lib/types/inline-enums.generated.ts @@ -170,6 +170,8 @@ export const CanonicalFormatHTML5Banner_MraidVersionValues = ["2.0", "3.0"] as c /** array of | CanonicalFormatImage.image_formats */ export const CanonicalFormatImage_ImageFormatsValues = ["jpg", "jpeg", "png", "gif", "webp", "svg"] as const; +/** single | CanonicalFormatImage.motion_level */ +export const CanonicalFormatImage_MotionLevelValues = ["static", "limited_motion"] as const; // ====== CanonicalFormatImageCarousel ====== @@ -180,8 +182,12 @@ export const CanonicalFormatImageCarousel_AllowedCardAssetTypesValues = ["image" /** single | CanonicalFormatNativeInFeed.asset_source */ export const CanonicalFormatNativeInFeed_AssetSourceValues = ["buyer_uploaded", "seller_pre_rendered_from_brief", "seller_human_designed", "agent_synthesized", "publisher_owned_reference"] as const; +/** single | CanonicalFormatNativeInFeed.focus_behavior */ +export const CanonicalFormatNativeInFeed_FocusBehaviorValues = ["none", "autoplay_muted", "autoplay_sound"] as const; /** array of | CanonicalFormatNativeInFeed.image_formats */ export const CanonicalFormatNativeInFeed_ImageFormatsValues = ["jpg", "jpeg", "png", "gif", "webp"] as const; +/** single | CanonicalFormatNativeInFeed.menu_placement */ +export const CanonicalFormatNativeInFeed_MenuPlacementValues = ["tile", "headline_banner"] as const; // ====== CanonicalFormatOption ====== @@ -199,6 +205,8 @@ export const CanonicalFormatSponsoredPlacementRetailMediaCatalogDriven_Supported // ====== CanonicalFormatVASTVideo ====== +/** single | CanonicalFormatVASTVideo.creative_type */ +export const CanonicalFormatVASTVideo_CreativeTypeValues = ["linear", "nonlinear", "either"] as const; /** single | CanonicalFormatVASTVideo.vpaid_version */ export const CanonicalFormatVASTVideo_VpaidVersionValues = ["1.0", "2.0"] as const; @@ -219,6 +227,11 @@ export const CanonicalProposal_ProposalKindValues = ["new_media_buy", "media_buy /** single | CanonicalReportingCapabilities.date_range_support */ export const CanonicalReportingCapabilities_DateRangeSupportValues = ["date_range", "lifetime_only"] as const; +// ====== CanvasConstraint ====== + +/** single | CanvasConstraint.constraint */ +export const CanvasConstraint_ConstraintValues = ["safe_area", "reserved_region", "decoration_only_edge", "no_text_or_logos"] as const; + // ====== CapabilitiesChangedWebhook ====== /** single | CapabilitiesChangedWebhook.reason */ diff --git a/src/lib/types/manifest.generated.ts b/src/lib/types/manifest.generated.ts index a57c8bdd3..3e1f84cc9 100644 --- a/src/lib/types/manifest.generated.ts +++ b/src/lib/types/manifest.generated.ts @@ -1,8 +1,8 @@ -// AUTO-GENERATED FROM schemas/cache/3.2.0-beta.5/manifest.json — DO NOT EDIT. +// AUTO-GENERATED FROM schemas/cache/3.2.0-beta.6/manifest.json — DO NOT EDIT. // Run `npm run generate-manifest-derived` to regenerate. /** - * Manifest-derived constants for AdCP 3.2.0-beta.5. + * Manifest-derived constants for AdCP 3.2.0-beta.6. * * Single source of truth for tool↔protocol grouping, error-code metadata * (description + recovery + suggestion), and specialism→required-tools @@ -12,8 +12,8 @@ * previously lived in `src/lib/utils/capabilities.ts` and * `src/lib/types/error-codes.ts`. * - * Source: `schemas/cache/3.2.0-beta.5/manifest.json` (adcp_version: 3.2.0-beta.5, generated_at: - * 2026-08-22T14:50:28.243Z). Re-run `npm run sync-schemas` then + * Source: `schemas/cache/3.2.0-beta.6/manifest.json` (adcp_version: 3.2.0-beta.6, generated_at: + * 2026-08-23T20:02:48.262Z). Re-run `npm run sync-schemas` then * `npm run generate-manifest-derived` to refresh after a spec bump. */ @@ -283,6 +283,11 @@ export const STANDARD_ERROR_CODES_FROM_MANIFEST = { recovery: "correctable", suggestion: "advisory — seller-side fix needed: ask the seller to add an explicit `canonical` field on the legacy format declaration, or contribute a registry entry (format_id_glob or structural match) to v1-canonical-mapping.json. Do not auto-retry; the product is still valid on the legacy named-format path" }, + "FORMAT_SHAPE_PROMOTED": { + description: "Non-fatal deprecation advisory raised when a 3.2-aware SDK encounters `format_kind: custom` with a `format_shape` that has been promoted to a first-class canonical. SDKs MUST preserve the declaration during its transition window and SHOULD augment the containing response's `errors[]` with `source: sdk`, `sdk_id`, this code, and `details: { format_shape, promoted_to, promotion_release, transition_end }`. Producers that self-detect their own legacy declaration MAY emit the same advisory with `source: producer`. Recovery is seller-side: dual-emit during the published transition window, migrate consumers, then replace the custom declaration with the promoted canonical.", + recovery: "correctable", + suggestion: "advisory — migrate the legacy custom format_shape to error.details.promoted_to before error.details.transition_end; dual emission is allowed during the transition window. Do not auto-retry" + }, "GOVERNANCE_DENIED": { description: "A registered governance agent denied the transaction. Sellers MUST place the denial in the operation's structured rejection arm when one exists (e.g., `acquire_rights` → `AcquireRightsRejected`, or an `approval_webhook` delivery → `CreativeRejected`); otherwise in `errors[]` + `adcp_error`. Buyers MUST dispatch on the response's discriminated `status` first and fall back to `errors[].code` / `adcp_error.code` only when no rejection arm exists for that operation. The buyer may restructure the buy (e.g., reduce budget, split into smaller transactions), escalate to human spending authority, or contact the governance agent for details. Wire placement (full guidance). Governance denial is a structured business outcome, not a system error — the governance call SUCCEEDED and the agent returned a denial verdict. Two cases: 1. Operation or webhook payload defines a structured rejection arm. The arm IS the canonical denial shape. The seller populates `reason` (human-readable, propagating governance findings) and `suggestions` (optional) and does NOT additionally emit `GOVERNANCE_DENIED` in `errors[]` or `adcp_error`. The rejection arms enforce this at the schema layer: e.g., `AcquireRightsRejected` and `CreativeRejected` both declare `not: { required: [errors] }`, so dual-emission is already a schema violation. The code does not appear on the wire when the rejection arm is used. Transport-level success markers MUST NOT be flipped (HTTP 200, MCP `isError: false`, A2A `succeeded`) — the operation completed successfully and produced a structured response. 2. Operation response has no rejection arm (e.g., `create_media_buy` returns Success / Error / Submitted arms only). The seller populates `errors[].code: GOVERNANCE_DENIED` in the payload AND `adcp_error.code: GOVERNANCE_DENIED` on the envelope per the two-layer model in `error-handling.mdx#envelope-vs-payload-errors-the-two-layer-model`. Transport-level failure markers DO flip in this case (HTTP 4xx, MCP `isError: true`, A2A `failed`) — the task could not produce a success artifact. The rule generalizes to any current or future operation or webhook payload whose response defines a discriminated rejection arm. In either placement, sellers SHOULD propagate governance findings verbatim — buyers' recovery decisions depend on what specifically was rejected. `GOVERNANCE_DENIED` is reserved for verdicts received from a reachable governance agent; if the governance call itself failed (timeout, network, config error), use `GOVERNANCE_UNAVAILABLE` instead.", recovery: "correctable", diff --git a/src/lib/types/schemas.generated.ts b/src/lib/types/schemas.generated.ts index f2c5371c2..b31870edf 100644 --- a/src/lib/types/schemas.generated.ts +++ b/src/lib/types/schemas.generated.ts @@ -1,5 +1,5 @@ // Generated Zod v4 schemas from TypeScript types -// Generated at: 2026-08-22T21:15:11.658Z +// Generated at: 2026-08-24T06:29:43.670Z // Sources: // - core.generated.ts (core types) // - tools.generated.ts (tool types) @@ -9,7 +9,8 @@ // Generated by ts-to-zod import { z } from "zod"; -import type { PreviewCreativeRequest, UpdateMediaBuyRequest, UpdateMediaBuyResponse, BuildCreativeResponse, SyncEventSourcesResponse, CreateMediaBuyRequest, CanonicalProposal } from './tools.generated'; +import type { PreviewCreativeRequest, UpdateMediaBuyRequest, UpdateMediaBuyResponse, BuildCreativeResponse, SyncEventSourcesResponse, CreateMediaBuyRequest, CanonicalProposal, ProductFormatDeclaration, Placement, Transformer, ListCreativeFormatsResponse, PackageStatus, ListTransformersResponseCreativeAgent, GetAdCPCapabilitiesResponse, ListTransformersResponse, CanonicalFormatSellerRenderedStatefulDisplay, CanonicalFormatCoordinatedPlacements } from './tools.generated'; +import type { Format, AvailablePackage } from './core.generated'; import { fullFormats as adcpJsonSchemaFormats } from "ajv-formats/dist/formats.js"; const adcpDateTimeFormat = adcpJsonSchemaFormats["date-time"] as { validate: (value: string) => boolean }; @@ -25,6 +26,8 @@ export const ActionNotAllowedReasonSchema = z.union([z.literal("wrong_status"), export const ActionSourceSchema = z.union([z.literal("website"), z.literal("app"), z.literal("offline"), z.literal("phone_call"), z.literal("chat"), z.literal("email"), z.literal("in_store"), z.literal("system_generated"), z.literal("other")]); +export const CreativeActivationMethodSchema = z.union([z.literal("qr_code"), z.literal("deep_link"), z.literal("push_notification"), z.literal("email"), z.literal("tune_in"), z.literal("text_message")]); + export const AdCPProtocolSchema = z.union([z.literal("media-buy"), z.literal("signals"), z.literal("governance"), z.literal("creative"), z.literal("brand"), z.literal("sponsored-intelligence"), z.literal("measurement")]); export const PriceAdjustmentKindSchema = z.union([z.literal("fee"), z.literal("discount"), z.literal("commission"), z.literal("settlement")]); @@ -63,7 +66,7 @@ export const AuthenticationSchemeSchema = z.union([z.literal("Bearer"), z.litera export const AvailabilityStatusSchema = z.union([z.literal("available"), z.literal("unavailable")]); -export const AvailableMetricSchema = z.union([z.literal("impressions"), z.literal("spend"), z.literal("clicks"), z.literal("ctr"), z.literal("views"), z.literal("completed_views"), z.literal("completion_rate"), z.literal("conversions"), z.literal("conversion_value"), z.literal("commissionable_value"), z.literal("roas"), z.literal("cost_per_acquisition"), z.literal("new_to_brand_rate"), z.literal("leads"), z.literal("reach"), z.literal("frequency"), z.literal("grps"), z.literal("engagements"), z.literal("engagement_rate"), z.literal("follows"), z.literal("saves"), z.literal("profile_visits"), z.literal("viewability"), z.literal("quartile_data"), z.literal("dooh_metrics"), z.literal("cost_per_click"), z.literal("cost_per_completed_view"), z.literal("cpm"), z.literal("downloads"), z.literal("units_sold"), z.literal("new_to_brand_units"), z.literal("plays"), z.literal("incremental_sales_lift"), z.literal("brand_lift"), z.literal("foot_traffic"), z.literal("conversion_lift"), z.literal("brand_search_lift")]); +export const AvailableMetricSchema = z.union([z.literal("impressions"), z.literal("spend"), z.literal("clicks"), z.literal("ctr"), z.literal("views"), z.literal("completed_views"), z.literal("completion_rate"), z.literal("conversions"), z.literal("conversion_value"), z.literal("commissionable_value"), z.literal("roas"), z.literal("cost_per_acquisition"), z.literal("new_to_brand_rate"), z.literal("leads"), z.literal("reach"), z.literal("frequency"), z.literal("grps"), z.literal("engagements"), z.literal("engagement_rate"), z.literal("follows"), z.literal("saves"), z.literal("profile_visits"), z.literal("viewability"), z.literal("viewable_rate"), z.literal("viewable_impressions"), z.literal("measurable_impressions"), z.literal("viewed_seconds"), z.literal("quartile_data"), z.literal("quartile_25"), z.literal("quartile_50"), z.literal("quartile_75"), z.literal("quartile_100"), z.literal("time_based_views"), z.literal("dooh_metrics"), z.literal("cost_per_click"), z.literal("cost_per_completed_view"), z.literal("cpm"), z.literal("downloads"), z.literal("units_sold"), z.literal("new_to_brand_units"), z.literal("plays"), z.literal("incremental_sales_lift"), z.literal("brand_lift"), z.literal("foot_traffic"), z.literal("conversion_lift"), z.literal("brand_search_lift")]); export const BillingPartySchema = z.union([z.literal("operator"), z.literal("agent"), z.literal("advertiser")]); @@ -127,6 +130,8 @@ export const CreativeSortFieldSchema = z.union([z.literal("created_date"), z.lit export const CreativeStatusSchema = z.union([z.literal("processing"), z.literal("pending_review"), z.literal("approved"), z.literal("suspended"), z.literal("rejected"), z.literal("archived")]); +export const CTVAdExperienceSchema = z.union([z.literal("menu"), z.literal("pause"), z.literal("screensaver"), z.literal("overlay"), z.literal("squeezeback"), z.literal("in_scene")]); + export const DAASTTrackingEventSchema = z.union([z.literal("impression"), z.literal("creativeView"), z.literal("start"), z.literal("firstQuartile"), z.literal("midpoint"), z.literal("thirdQuartile"), z.literal("complete"), z.literal("mute"), z.literal("unmute"), z.literal("pause"), z.literal("resume"), z.literal("rewind"), z.literal("skip"), z.literal("progress"), z.literal("clickTracking"), z.literal("customClick"), z.literal("close"), z.literal("error"), z.literal("viewable"), z.literal("notViewable"), z.literal("viewUndetermined"), z.literal("measurableImpression"), z.literal("viewableImpression")]); export const DAASTVersionSchema = z.union([z.literal("1.0"), z.literal("1.1")]); @@ -161,7 +166,7 @@ export const DistributionIdentifierTypeSchema = z.union([z.literal("apple_podcas export const EmbeddedProvenanceMethodSchema = z.union([z.literal("manifest_wrapper"), z.literal("provenance_markers")]); -export const ErrorCodeSchema = z.union([z.literal("INVALID_REQUEST"), z.literal("AUTH_REQUIRED"), z.literal("AUTH_MISSING"), z.literal("AUTH_INVALID"), z.literal("AUTHORIZATION_REQUIRED"), z.literal("RATE_LIMITED"), z.literal("SERVICE_UNAVAILABLE"), z.literal("CONFIGURATION_ERROR"), z.literal("POLICY_VIOLATION"), z.literal("PRODUCT_NOT_FOUND"), z.literal("PRODUCT_UNAVAILABLE"), z.literal("PROPOSAL_EXPIRED"), z.literal("BUDGET_TOO_LOW"), z.literal("CREATIVE_REJECTED"), z.literal("CREATIVE_LOCALE_NOT_ACCEPTED"), z.literal("CREATIVE_VALUE_NOT_ALLOWED"), z.literal("UNSUPPORTED_FEATURE"), z.literal("UNPRICEABLE_OUTPUT"), z.literal("UNSUPPORTED_GRANULARITY"), z.literal("UNSUPPORTED_PROVISIONING"), z.literal("AUDIENCE_TOO_SMALL"), z.literal("ACCOUNT_REQUIRED"), z.literal("ACCOUNT_NOT_FOUND"), z.literal("ACCOUNT_MOVED"), z.literal("ACCOUNT_IDENTITY_CONFLICT"), z.literal("ACCOUNT_SETUP_REQUIRED"), z.literal("ACCOUNT_AMBIGUOUS"), z.literal("ACCOUNT_PAYMENT_REQUIRED"), z.literal("ACCOUNT_SUSPENDED"), z.literal("COMPLIANCE_UNSATISFIED"), z.literal("GOVERNANCE_DENIED"), z.literal("BUDGET_EXHAUSTED"), z.literal("BUDGET_EXCEEDED"), z.literal("BUDGET_CAP_REACHED"), z.literal("CONFLICT"), z.literal("IDEMPOTENCY_CONFLICT"), z.literal("IDEMPOTENCY_EXPIRED"), z.literal("IDEMPOTENCY_IN_FLIGHT"), z.literal("CREATIVE_DEADLINE_EXCEEDED"), z.literal("CREATIVE_INACCESSIBLE"), z.literal("INVALID_STATE"), z.literal("MEDIA_BUY_NOT_FOUND"), z.literal("NOT_CANCELLABLE"), z.literal("PACKAGE_NOT_FOUND"), z.literal("PLACE_TARGET_UNAVAILABLE"), z.literal("CREATIVE_NOT_FOUND"), z.literal("SIGNAL_NOT_FOUND"), z.literal("SIGNAL_TARGETING_INCOMPATIBLE"), z.literal("SESSION_NOT_FOUND"), z.literal("PLAN_NOT_FOUND"), z.literal("REFERENCE_NOT_FOUND"), z.literal("SESSION_TERMINATED"), z.literal("VALIDATION_ERROR"), z.literal("PRODUCT_EXPIRED"), z.literal("PROPOSAL_NOT_COMMITTED"), z.literal("PROPOSAL_NOT_FOUND"), z.literal("MULTI_FINALIZE_UNSUPPORTED"), z.literal("IO_REQUIRED"), z.literal("TERMS_REJECTED"), z.literal("BIDDING_PLACEMENT_CONFLICT"), z.literal("AMBIGUOUS_BIDDING_POLICY"), z.literal("CONFLICTING_SELECTORS"), z.literal("REQUOTE_REQUIRED"), z.literal("VERSION_UNSUPPORTED"), z.literal("CAMPAIGN_SUSPENDED"), z.literal("GOVERNANCE_UNAVAILABLE"), z.literal("PERMISSION_DENIED"), z.literal("SCOPE_INSUFFICIENT"), z.literal("READ_ONLY_SCOPE"), z.literal("FIELD_NOT_PERMITTED"), z.literal("PROVENANCE_REQUIRED"), z.literal("PROVENANCE_DIGITAL_SOURCE_TYPE_MISSING"), z.literal("PROVENANCE_SYNTHETIC_DEPICTION_MISSING"), z.literal("PROVENANCE_DISCLOSURE_MISSING"), z.literal("PROVENANCE_EMBEDDED_MISSING"), z.literal("PROVENANCE_VERIFIER_NOT_ACCEPTED"), z.literal("PROVENANCE_CLAIM_CONTRADICTED"), z.literal("EVALUATOR_AGENT_NOT_ACCEPTED"), z.literal("BILLING_NOT_SUPPORTED"), z.literal("BILLING_NOT_PERMITTED_FOR_AGENT"), z.literal("BILLING_OUT_OF_BAND"), z.literal("PAYMENT_TERMS_NOT_SUPPORTED"), z.literal("BRAND_REQUIRED"), z.literal("AGENT_SUSPENDED"), z.literal("AGENT_BLOCKED"), z.literal("CREDENTIAL_IN_ARGS"), z.literal("ACTION_NOT_ALLOWED"), z.literal("PRIVATE_FIELD_IN_PUBLIC_PLACEMENT"), z.literal("FORMAT_PROJECTION_FAILED"), z.literal("FORMAT_DECLARATION_DIVERGENT"), z.literal("FORMAT_DECLARATION_V1_AMBIGUOUS"), z.literal("FORMAT_OPTION_UNRESOLVED"), z.literal("FORMAT_DECLARATION_V1_LOSSY_MULTI_SIZE"), z.literal("FORMAT_NOT_SUPPORTED"), z.literal("PIXEL_TRACKER_LOSSY_DOWNGRADE"), z.literal("PIXEL_TRACKER_UPGRADE_INFERRED"), z.literal("STALE_RESPONSE"), z.literal("FEED_FETCH_FAILED"), z.literal("INVALID_FEED_FORMAT"), z.literal("ITEM_VALIDATION_FAILED"), z.literal("CATALOG_LIMIT_EXCEEDED"), z.literal("INVALID_PRICING_OPTION"), z.literal("INVALID_USAGE_DATA"), z.literal("SIGNED_RESPONSE_ENVELOPE_EXPIRED"), z.literal("SIGNED_RESPONSE_REQUEST_HASH_MISMATCH"), z.literal("SIGNED_RESPONSE_TENANT_MISMATCH"), z.literal("VAST_PARSE_FAILED"), z.literal("VAST_VERSION_MISMATCH"), z.literal("VAST_WRAPPER_DEPTH_EXCEEDED")]); +export const ErrorCodeSchema = z.union([z.literal("INVALID_REQUEST"), z.literal("AUTH_REQUIRED"), z.literal("AUTH_MISSING"), z.literal("AUTH_INVALID"), z.literal("AUTHORIZATION_REQUIRED"), z.literal("RATE_LIMITED"), z.literal("SERVICE_UNAVAILABLE"), z.literal("CONFIGURATION_ERROR"), z.literal("POLICY_VIOLATION"), z.literal("PRODUCT_NOT_FOUND"), z.literal("PRODUCT_UNAVAILABLE"), z.literal("PROPOSAL_EXPIRED"), z.literal("BUDGET_TOO_LOW"), z.literal("CREATIVE_REJECTED"), z.literal("CREATIVE_LOCALE_NOT_ACCEPTED"), z.literal("CREATIVE_VALUE_NOT_ALLOWED"), z.literal("UNSUPPORTED_FEATURE"), z.literal("UNPRICEABLE_OUTPUT"), z.literal("UNSUPPORTED_GRANULARITY"), z.literal("UNSUPPORTED_PROVISIONING"), z.literal("AUDIENCE_TOO_SMALL"), z.literal("ACCOUNT_REQUIRED"), z.literal("ACCOUNT_NOT_FOUND"), z.literal("ACCOUNT_MOVED"), z.literal("ACCOUNT_IDENTITY_CONFLICT"), z.literal("ACCOUNT_SETUP_REQUIRED"), z.literal("ACCOUNT_AMBIGUOUS"), z.literal("ACCOUNT_PAYMENT_REQUIRED"), z.literal("ACCOUNT_SUSPENDED"), z.literal("COMPLIANCE_UNSATISFIED"), z.literal("GOVERNANCE_DENIED"), z.literal("BUDGET_EXHAUSTED"), z.literal("BUDGET_EXCEEDED"), z.literal("BUDGET_CAP_REACHED"), z.literal("CONFLICT"), z.literal("IDEMPOTENCY_CONFLICT"), z.literal("IDEMPOTENCY_EXPIRED"), z.literal("IDEMPOTENCY_IN_FLIGHT"), z.literal("CREATIVE_DEADLINE_EXCEEDED"), z.literal("CREATIVE_INACCESSIBLE"), z.literal("INVALID_STATE"), z.literal("MEDIA_BUY_NOT_FOUND"), z.literal("NOT_CANCELLABLE"), z.literal("PACKAGE_NOT_FOUND"), z.literal("PLACE_TARGET_UNAVAILABLE"), z.literal("CREATIVE_NOT_FOUND"), z.literal("SIGNAL_NOT_FOUND"), z.literal("SIGNAL_TARGETING_INCOMPATIBLE"), z.literal("SESSION_NOT_FOUND"), z.literal("PLAN_NOT_FOUND"), z.literal("REFERENCE_NOT_FOUND"), z.literal("SESSION_TERMINATED"), z.literal("VALIDATION_ERROR"), z.literal("PRODUCT_EXPIRED"), z.literal("PROPOSAL_NOT_COMMITTED"), z.literal("PROPOSAL_NOT_FOUND"), z.literal("MULTI_FINALIZE_UNSUPPORTED"), z.literal("IO_REQUIRED"), z.literal("TERMS_REJECTED"), z.literal("BIDDING_PLACEMENT_CONFLICT"), z.literal("AMBIGUOUS_BIDDING_POLICY"), z.literal("CONFLICTING_SELECTORS"), z.literal("REQUOTE_REQUIRED"), z.literal("VERSION_UNSUPPORTED"), z.literal("CAMPAIGN_SUSPENDED"), z.literal("GOVERNANCE_UNAVAILABLE"), z.literal("PERMISSION_DENIED"), z.literal("SCOPE_INSUFFICIENT"), z.literal("READ_ONLY_SCOPE"), z.literal("FIELD_NOT_PERMITTED"), z.literal("PROVENANCE_REQUIRED"), z.literal("PROVENANCE_DIGITAL_SOURCE_TYPE_MISSING"), z.literal("PROVENANCE_SYNTHETIC_DEPICTION_MISSING"), z.literal("PROVENANCE_DISCLOSURE_MISSING"), z.literal("PROVENANCE_EMBEDDED_MISSING"), z.literal("PROVENANCE_VERIFIER_NOT_ACCEPTED"), z.literal("PROVENANCE_CLAIM_CONTRADICTED"), z.literal("EVALUATOR_AGENT_NOT_ACCEPTED"), z.literal("BILLING_NOT_SUPPORTED"), z.literal("BILLING_NOT_PERMITTED_FOR_AGENT"), z.literal("BILLING_OUT_OF_BAND"), z.literal("PAYMENT_TERMS_NOT_SUPPORTED"), z.literal("BRAND_REQUIRED"), z.literal("AGENT_SUSPENDED"), z.literal("AGENT_BLOCKED"), z.literal("CREDENTIAL_IN_ARGS"), z.literal("ACTION_NOT_ALLOWED"), z.literal("PRIVATE_FIELD_IN_PUBLIC_PLACEMENT"), z.literal("FORMAT_PROJECTION_FAILED"), z.literal("FORMAT_DECLARATION_DIVERGENT"), z.literal("FORMAT_SHAPE_PROMOTED"), z.literal("FORMAT_DECLARATION_V1_AMBIGUOUS"), z.literal("FORMAT_OPTION_UNRESOLVED"), z.literal("FORMAT_DECLARATION_V1_LOSSY_MULTI_SIZE"), z.literal("FORMAT_NOT_SUPPORTED"), z.literal("PIXEL_TRACKER_LOSSY_DOWNGRADE"), z.literal("PIXEL_TRACKER_UPGRADE_INFERRED"), z.literal("STALE_RESPONSE"), z.literal("FEED_FETCH_FAILED"), z.literal("INVALID_FEED_FORMAT"), z.literal("ITEM_VALIDATION_FAILED"), z.literal("CATALOG_LIMIT_EXCEEDED"), z.literal("INVALID_PRICING_OPTION"), z.literal("INVALID_USAGE_DATA"), z.literal("SIGNED_RESPONSE_ENVELOPE_EXPIRED"), z.literal("SIGNED_RESPONSE_REQUEST_HASH_MISMATCH"), z.literal("SIGNED_RESPONSE_TENANT_MISMATCH"), z.literal("VAST_PARSE_FAILED"), z.literal("VAST_VERSION_MISMATCH"), z.literal("VAST_WRAPPER_DEPTH_EXCEEDED")]); export const ErrorScopeSchema = z.union([z.literal("capability"), z.literal("account"), z.literal("agent")]); @@ -251,6 +256,8 @@ export const MetroAreaSystemSchema = z.union([z.literal("nielsen_dma"), z.litera export const MoovAtomPositionSchema = z.union([z.literal("start"), z.literal("end")]); +export const CreativeMotionLevelSchema = z.union([z.literal("static"), z.literal("limited_motion"), z.literal("full_motion")]); + export const NotificationTypeSchema = z.union([z.literal("scheduled"), z.literal("final"), z.literal("delayed"), z.literal("adjusted"), z.literal("window_update"), z.literal("impairment"), z.literal("creative.status_changed"), z.literal("creative.assignment_changed"), z.literal("indicators.changed"), z.literal("creative.purged"), z.literal("account.status_changed"), z.literal("product.created"), z.literal("product.updated"), z.literal("product.priced"), z.literal("product.removed"), z.literal("signal.created"), z.literal("signal.updated"), z.literal("signal.priced"), z.literal("signal.removed"), z.literal("wholesale_feed.bulk_change"), z.literal("capabilities.changed")]); export const OfferingAvailabilityStatusSchema = z.union([z.literal("available"), z.literal("limited"), z.literal("sold_out"), z.literal("expired"), z.literal("region_restricted"), z.literal("inactive")]); @@ -321,7 +328,7 @@ export const SocialPlacementSurfaceSchema = z.union([z.literal("feed"), z.litera export const SortDirectionSchema = z.union([z.literal("asc"), z.literal("desc")]); -export const SortMetricSchema = z.union([z.literal("impressions"), z.literal("spend"), z.literal("clicks"), z.literal("ctr"), z.literal("views"), z.literal("completed_views"), z.literal("completion_rate"), z.literal("conversions"), z.literal("conversion_value"), z.literal("roas"), z.literal("cost_per_acquisition"), z.literal("new_to_brand_rate"), z.literal("leads"), z.literal("grps"), z.literal("reach"), z.literal("frequency"), z.literal("engagements"), z.literal("follows"), z.literal("saves"), z.literal("profile_visits"), z.literal("engagement_rate"), z.literal("cost_per_click")]); +export const SortMetricSchema = z.union([z.literal("impressions"), z.literal("spend"), z.literal("clicks"), z.literal("ctr"), z.literal("views"), z.literal("completed_views"), z.literal("completion_rate"), z.literal("conversions"), z.literal("conversion_value"), z.literal("roas"), z.literal("cost_per_acquisition"), z.literal("new_to_brand_rate"), z.literal("leads"), z.literal("grps"), z.literal("reach"), z.literal("frequency"), z.literal("engagements"), z.literal("follows"), z.literal("saves"), z.literal("profile_visits"), z.literal("engagement_rate"), z.literal("cost_per_click"), z.literal("commissionable_value"), z.literal("plays"), z.literal("cost_per_completed_view"), z.literal("cpm"), z.literal("downloads"), z.literal("units_sold"), z.literal("new_to_brand_units"), z.literal("viewable_rate"), z.literal("viewable_impressions"), z.literal("measurable_impressions"), z.literal("viewed_seconds"), z.literal("quartile_25"), z.literal("quartile_50"), z.literal("quartile_75"), z.literal("quartile_100")]); export const SpecialCategorySchema = z.union([z.literal("awards"), z.literal("championship"), z.literal("concert"), z.literal("conference"), z.literal("election"), z.literal("festival"), z.literal("gala"), z.literal("holiday"), z.literal("premiere"), z.literal("product_launch"), z.literal("reunion"), z.literal("tribute")]); @@ -355,6 +362,8 @@ export const VASTVersionSchema = z.union([z.literal("2.0"), z.literal("3.0"), z. export const VideoPlacementTypeSchema = z.union([z.literal("instream"), z.literal("accompanying_content"), z.literal("interstitial"), z.literal("standalone")]); +export const ViewThresholdBasisSchema = z.union([z.literal("play_time"), z.literal("in_view")]); + export const ViewabilityStandardSchema = z.union([z.literal("mrc"), z.literal("groupm")]); export const WarningCodeSchema = z.union([z.literal("inventory_shortfall_forecast"), z.literal("flight_change_creates_pacing_risk"), z.literal("fields_ignored_due_to_precedence")]); @@ -715,6 +724,12 @@ export const CatalogRequirementSchema = z.object({ system_versions: z.array(z.string()).optional() }).passthrough(); +export const LanguageTagSchema = z.string(); + +export const CreativeLocalePolicySchema = z.object({ + accepted_language_ranges: z.array(LanguageTagSchema) +}).passthrough(); + export const CancellationPolicySchema = z.object({ notice_period: DurationSchema, cancellation_fee: z.union([z.object({ @@ -811,8 +826,6 @@ export const CollectionListReferenceSchema = z.object({ auth_token: z.string().optional() }).passthrough(); -export const LanguageTagSchema = z.string(); - export const PostalAreaWithFusedSystemSchema = z.object({ system: CountryFusedPostalCodeSystemSchema, values: z.array(z.string()) @@ -922,7 +935,9 @@ export const FormatReferenceStructuredObjectSchema = z.object({ pixel_ratio: z.number().optional() }).passthrough(); -export const CanonicalFormatKindSchema = z.union([z.literal("image"), z.literal("html5"), z.literal("display_tag"), z.literal("image_carousel"), z.literal("video_hosted"), z.literal("video_vast"), z.literal("audio_hosted"), z.literal("audio_daast"), z.literal("sponsored_placement"), z.literal("native_in_feed"), z.literal("responsive_creative"), z.literal("agent_placement"), z.literal("custom")]); +export const CanonicalFormatKindSchema = z.union([z.literal("image"), z.literal("html5"), z.literal("display_tag"), z.literal("image_carousel"), z.literal("video_hosted"), z.literal("video_vast"), z.literal("audio_hosted"), z.literal("audio_daast"), z.literal("sponsored_placement"), z.literal("native_in_feed"), z.literal("responsive_creative"), z.literal("agent_placement"), z.literal("seller_rendered_stateful_display"), z.literal("coordinated_placements"), z.literal("custom")]); + +export const CreativeAssetsSchema = z.object({}).passthrough(); export const IndustryIdentifierSchema = z.object({ type: CreativeIdentifierTypeSchema, @@ -952,6 +967,9 @@ export const ImageAssetSchema = z.object({ height: z.number(), file_size_bytes: z.number().int().min(1).optional(), pixel_ratio: z.number().optional(), + state_id: z.string().optional(), + breakpoint_id: z.string().optional(), + focal_point: z.tuple([z.number(), z.number()]).optional(), format: z.string().optional(), alt_text: z.string().optional(), provenance: ProvenanceSchema.optional() @@ -1037,6 +1055,7 @@ export const URLAssetSchema = z.object({ url: z.string(), url_type: URLAssetTypeSchema.optional(), description: z.string().optional(), + state_id: z.string().optional(), provenance: ProvenanceSchema.optional() }).passthrough(); @@ -1531,7 +1550,8 @@ export const CanonicalFormatHostedVideoSchema = z.object({ companion_banner_widths: z.array(z.number()).optional(), companion_banner_heights: z.array(z.number()).optional(), asset_source: z.union([z.literal("buyer_uploaded"), z.literal("publisher_host_recorded"), z.literal("seller_pre_rendered_from_brief"), z.literal("seller_human_designed"), z.literal("agent_synthesized"), z.literal("publisher_owned_reference")]).optional(), - buyer_asset_acceptance: z.union([z.literal("accepted"), z.literal("rejected")]).optional() + buyer_asset_acceptance: z.union([z.literal("accepted"), z.literal("rejected")]).optional(), + ctv_ad_experience: CTVAdExperienceSchema.optional() }).passthrough(); export const CanonicalFormatVASTVideoSchema = z.object({ @@ -1574,6 +1594,10 @@ export const CanonicalFormatVASTVideoSchema = z.object({ max_width: z.number().min(1).optional(), min_height: z.number().min(1).optional(), max_height: z.number().min(1).optional(), + creative_type: z.union([z.literal("linear"), z.literal("nonlinear"), z.literal("either")]).optional(), + ctv_ad_experience: CTVAdExperienceSchema.optional(), + motion_level: CreativeMotionLevelSchema.optional(), + activation_methods: z.array(CreativeActivationMethodSchema).optional(), linear_required: z.boolean().optional(), skippable_after_ms: z.number().min(0).optional(), max_wrapper_depth: z.number().min(0).optional(), @@ -1699,7 +1723,8 @@ export const CanonicalFormatSponsoredPlacementRetailMediaCatalogDrivenSchema = z required_catalog_fields: z.array(z.string()).optional(), supported_id_types: z.array(z.union([z.literal("asin"), z.literal("sku"), z.literal("gtin"), z.literal("offering_id"), z.literal("store_id"), z.literal("hotel_id"), z.literal("flight_id"), z.literal("vehicle_id"), z.literal("listing_id"), z.literal("program_id"), z.literal("destination_id"), z.literal("app_id"), z.literal("job_id")])).optional(), hero_asset_supported: z.boolean().optional(), - item_production_model: z.union([z.literal("buyer_uploaded"), z.literal("seller_pre_rendered_from_brief"), z.literal("seller_human_designed"), z.literal("agent_synthesized")]).optional() + item_production_model: z.union([z.literal("buyer_uploaded"), z.literal("seller_pre_rendered_from_brief"), z.literal("seller_human_designed"), z.literal("agent_synthesized")]).optional(), + ctv_ad_experience: CTVAdExperienceSchema.optional() }).passthrough(); export const CanonicalFormatNativeInFeedSchema = z.object({ @@ -1730,6 +1755,11 @@ export const CanonicalFormatNativeInFeedSchema = z.object({ required_connections: z.array(DownstreamConnectionRequirementSchema).optional(), reference_mutability: z.union([z.literal("immutable_snapshot"), z.literal("mutable_requires_reapproval"), z.literal("mutable_auto_recheck")]).optional(), production_window_business_days: z.number().optional(), + ctv_ad_experience: CTVAdExperienceSchema.optional(), + menu_placement: z.union([z.literal("tile"), z.literal("headline_banner")]).optional(), + focus_behavior: z.union([z.literal("none"), z.literal("autoplay_muted"), z.literal("autoplay_sound")]).optional(), + motion_level: CreativeMotionLevelSchema.optional(), + activation_methods: z.array(CreativeActivationMethodSchema).optional(), title_max_chars: z.number().min(1).optional(), body_text_max_chars: z.number().min(1).optional(), cta_max_chars: z.number().min(1).optional(), @@ -1842,6 +1872,226 @@ export const CanonicalFormatAgentPlacementAISurfaceSponsoredPlacementSchema = z. disclosure_required: z.boolean().optional() }).passthrough(); +export const CanvasConstraintSchema = z.object({ + constraint: z.union([z.literal("safe_area"), z.literal("reserved_region"), z.literal("decoration_only_edge"), z.literal("no_text_or_logos")]), + state_id: z.string().optional(), + breakpoint_id: z.string().optional(), + region: z.object({}).passthrough() +}).passthrough(); + +export const SizeModeMutex1Schema = SizeModeMutexSchema; + +export const Responsive1Schema = ResponsiveSchema; + +export const SizeModeMutex2Schema = SizeModeMutexSchema; + +export const Responsive2Schema = ResponsiveSchema; + +export const CanonicalFormatImageSchema = z.object({ + experimental: z.boolean().optional(), + deprecated: z.boolean().optional(), + v1_translatable: z.boolean().optional(), + since_version: z.string().optional(), + migration_target_version: z.string().optional(), + composition_model: z.union([z.literal("deterministic"), z.literal("algorithmic")]).optional(), + provenance_required: z.boolean().optional(), + platform_extensions: z.array(PlatformExtensionReferenceSchema).optional(), + synthesis_nondeterministic: z.boolean().optional(), + slots: z.array(z.object({ + asset_group_id: z.string(), + asset_type: z.union([z.literal("image"), z.literal("video"), z.literal("audio"), z.literal("text"), z.literal("markdown"), z.literal("url"), z.literal("html"), z.literal("css"), z.literal("javascript"), z.literal("vast"), z.literal("daast"), z.literal("webhook"), z.literal("brief"), z.literal("catalog"), z.literal("published_post"), z.literal("zip"), z.literal("card"), z.literal("object"), z.literal("pixel_tracker"), z.literal("vast_tracker"), z.literal("daast_tracker")]), + required: z.boolean().optional(), + min: z.number().optional(), + max: z.number().optional(), + max_chars: z.number().optional(), + max_size_kb: z.number().optional(), + pixel_ratios: z.array(z.number()).optional(), + required_pixel_ratios: z.array(z.number()).optional(), + logo_slots: z.array(LogoSlotSchema).optional(), + required_logo_slots: z.array(LogoSlotSchema).optional(), + description: z.string().optional(), + consumed_for_production: z.boolean().optional() + }).passthrough()).optional(), + required_connections: z.array(DownstreamConnectionRequirementSchema).optional(), + reference_mutability: z.union([z.literal("immutable_snapshot"), z.literal("mutable_requires_reapproval"), z.literal("mutable_auto_recheck")]).optional(), + production_window_business_days: z.number().optional(), + width: z.number().min(1).optional(), + height: z.number().min(1).optional(), + sizes: z.array(z.object({ + width: z.number().min(1), + height: z.number().min(1) + }).passthrough()).optional(), + pixel_ratios: z.array(z.number()).optional(), + min_width: z.number().min(1).optional(), + max_width: z.number().min(1).optional(), + min_height: z.number().min(1).optional(), + max_height: z.number().min(1).optional(), + aspect_ratio: z.string().regex(/^[0-9]+(\.[0-9]+)?:[0-9]+(\.[0-9]+)?$/).optional(), + max_file_size_kb: z.number().min(1).optional(), + image_formats: z.array(z.union([z.literal("jpg"), z.literal("jpeg"), z.literal("png"), z.literal("gif"), z.literal("webp"), z.literal("svg")])).optional(), + ssl_required: z.boolean().optional(), + headline_max_chars: z.number().min(1).optional(), + body_text_max_chars: z.number().min(1).optional(), + cta_values: z.array(z.string()).optional(), + asset_source: z.union([z.literal("buyer_uploaded"), z.literal("publisher_host_recorded"), z.literal("seller_pre_rendered_from_brief"), z.literal("seller_human_designed"), z.literal("agent_synthesized"), z.literal("publisher_owned_reference")]).optional(), + buyer_asset_acceptance: z.union([z.literal("accepted"), z.literal("rejected")]).optional(), + ctv_ad_experience: CTVAdExperienceSchema.optional(), + motion_level: CreativeMotionLevelSchema.optional(), + activation_methods: z.array(CreativeActivationMethodSchema).optional() +}).passthrough().omit({ motion_level: true }).merge(z.object({}).passthrough().merge(z.object({ + motion_level: z.union([z.literal("static"), z.literal("limited_motion")]).optional() +}).passthrough())); + +export const CanonicalFormatHTML5BannerSchema = z.object({}).passthrough().merge(z.object({ + experimental: z.boolean().optional(), + deprecated: z.boolean().optional(), + v1_translatable: z.boolean().optional(), + since_version: z.string().optional(), + migration_target_version: z.string().optional(), + composition_model: z.union([z.literal("deterministic"), z.literal("algorithmic")]).optional(), + provenance_required: z.boolean().optional(), + platform_extensions: z.array(PlatformExtensionReferenceSchema).optional(), + synthesis_nondeterministic: z.boolean().optional(), + slots: z.array(z.object({ + asset_group_id: z.string(), + asset_type: z.union([z.literal("image"), z.literal("video"), z.literal("audio"), z.literal("text"), z.literal("markdown"), z.literal("url"), z.literal("html"), z.literal("css"), z.literal("javascript"), z.literal("vast"), z.literal("daast"), z.literal("webhook"), z.literal("brief"), z.literal("catalog"), z.literal("published_post"), z.literal("zip"), z.literal("card"), z.literal("object"), z.literal("pixel_tracker"), z.literal("vast_tracker"), z.literal("daast_tracker")]), + required: z.boolean().optional(), + min: z.number().optional(), + max: z.number().optional(), + max_chars: z.number().optional(), + max_size_kb: z.number().optional(), + pixel_ratios: z.array(z.number()).optional(), + required_pixel_ratios: z.array(z.number()).optional(), + logo_slots: z.array(LogoSlotSchema).optional(), + required_logo_slots: z.array(LogoSlotSchema).optional(), + description: z.string().optional(), + consumed_for_production: z.boolean().optional() + }).passthrough()).optional(), + required_connections: z.array(DownstreamConnectionRequirementSchema).optional(), + reference_mutability: z.union([z.literal("immutable_snapshot"), z.literal("mutable_requires_reapproval"), z.literal("mutable_auto_recheck")]).optional(), + production_window_business_days: z.number().optional(), + width: z.number().min(1).optional(), + height: z.number().min(1).optional(), + sizes: z.array(z.object({ + width: z.number().min(1), + height: z.number().min(1) + }).passthrough()).optional(), + min_width: z.number().min(1).optional(), + max_width: z.number().min(1).optional(), + min_height: z.number().min(1).optional(), + max_height: z.number().min(1).optional(), + max_initial_load_kb: z.number().min(1).optional(), + max_polite_load_kb: z.number().min(1).optional(), + host_initiated_subload: z.boolean().optional(), + max_animation_duration_ms: z.number().min(0).optional(), + max_cpu_load_percent: z.number().min(1).max(100).optional(), + mraid_required: z.boolean().optional(), + mraid_version: z.union([z.literal("2.0"), z.literal("3.0")]).optional(), + om_sdk_required: z.boolean().optional(), + clicktag_macro: z.union([z.literal("clickTag"), z.literal("clickTAG")]).optional(), + backup_image_required: z.boolean().optional(), + backup_image_max_size_kb: z.number().min(1).optional(), + ssl_required: z.boolean().optional() +}).passthrough()); + +// @ts-ignore -- preserve the public schema type across lossy TS-to-Zod projection details. +export const CanonicalFormatSellerRenderedStatefulDisplaySchema: z.ZodObject<{ [K in keyof CanonicalFormatSellerRenderedStatefulDisplay]-?: z.ZodType }, any> & z.ZodType, CanonicalFormatSellerRenderedStatefulDisplay & Record> = z.object({ "experimental": z.boolean().describe("Experimental in AdCP 3.2 while the creative working group gathers implementation evidence across premium web and mobile/app sellers.").optional(), "deprecated": z.boolean().describe("When true, this canonical (or a seller's specific narrowing of it) is going away. Existing adopters are supported through the deprecation cycle; new adoption is discouraged. Pair with `migration_target_version` to indicate when the canonical is expected to be removed. Distinct from `experimental`: an experimental canonical may stabilize and stop being experimental; a deprecated canonical is on a sunset path.").optional(), "v1_translatable": z.boolean().describe("No v1 named-format equivalent can express multiple seller-rendered states and their breakpoint bindings.").optional(), "since_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version that introduced this canonical (e.g., '3.1', '3.2'). Lets adopters reason about minimum protocol version requirements when consuming a format declaration. Patch precision is intentionally rejected — canonicals are introduced at minor-version boundaries.").optional(), "migration_target_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version by which the working group expects this canonical to stabilize, surface a breaking revision, or (when `deprecated: true`) be removed. Patch precision is intentionally rejected — canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown').").optional(), "composition_model": z.enum(["deterministic","algorithmic"]).describe("Whether the surface composes deterministically (buyer can predict per-slot rendering — sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing — responsive_creative, agent_placement).").optional(), "provenance_required": z.boolean().describe("When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent.").optional(), "platform_extensions": z.array(z.object({ "uri": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL identifying the extension. `https://` is mandatory — `http://`, `file://`, `data:`, and other schemes are rejected at the schema layer (defense-in-depth on top of the fetch-contract normative rules). The URI base is the owning agent's URL; the path identifies the extension within that agent. Example: 'https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel'. The full fetch contract — SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds — is documented on `product-format-declaration.json#format_schema` and applies to ALL fetches of this reference shape regardless of whether the field is named `format_schema` (load-bearing for validation) or `platform_extensions` (informational); the *transport* rules are identical, only the *consumption* semantics differ."), "digest": z.string().regex(new RegExp("^sha256:[a-f0-9]{64}$")).describe("SHA-256 content digest of the extension definition (sha256:). Used to detect drift — if the agent revises the extension, the digest changes and cached definitions become invalid.") }).catchall(z.any()).describe("Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` — divergent digests in one response indicate producer-side error (e.g., concurrent extension revision mid-render). Buyers encountering divergent digests for the same URI MUST fail closed: treat all references to that URI as unresolved and surface a validation error rather than picking one branch silently. **Across responses**, digest divergence is normal — extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.")).describe("Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\n\n**Collision precedence (normative).** When two or more `platform_extensions[]` entries on the same declaration extend the same target (e.g., both extend `tracking`) with overlapping field names, **array order is authoritative — later entries override earlier ones on a per-field basis** (last-in-array-wins). SDKs MUST surface the overlap via the `errors[]` array on the `get_products` response with a structured code (`FORMAT_DECLARATION_DIVERGENT` is appropriate when the overlap appears across dual-emitted shapes; a producer-self-emitted overlap on a single declaration SHOULD use the same code with `error.details: { collision_kind: \"platform_extension_field\", target, overlapping_fields, winning_extension_uri }`). Producers SHOULD avoid the collision by emitting one extension per target or by partitioning fields across extensions; the deterministic precedence is for last-resort consistency across SDK implementations, not a sanctioned merging strategy.").optional(), "synthesis_nondeterministic": z.boolean().describe("When true, the format's production pipeline is genuinely nondeterministic — the platform cannot guarantee that synthesis from a given input set produces in-spec output. Veo / Sora / Runway-class generative video, and other AI-synthesis flows where output dimensions, duration, or quality vary per run. Implies a different validation contract: predictive `validate_input` is impossible; the platform's own post-synthesis QA loop applies; if the QA loop exhausts without producing a valid artifact, `build_creative` returns task_failed with a synthesis_failed reason. Distinct from `composition_model` (which describes how the surface composes per-slot rendering, not whether synthesis is deterministic). When false or absent, the format's production is predictable enough that `validate_input` can predict output properties from input properties.\n\n**Compatibility with `asset_source` / `item_production_model`**: `synthesis_nondeterministic: true` MAY pair with any of `seller_pre_rendered_from_brief`, `seller_human_designed`, or `agent_synthesized` (the QA loop is concept-level, not source-specific — 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.").optional(), "slots": z.array(z.object({ "asset_group_id": z.string().describe("Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings."), "asset_type": z.enum(["image","video","audio","text","markdown","url","html","css","javascript","vast","daast","webhook","brief","catalog","published_post","zip","card","object","pixel_tracker","vast_tracker","daast_tracker"]).describe("Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `published_post` is an existing-post reference asset, not uploaded media bytes and not a catalog row. `card` is the multi-card carousel element type (see card-asset.json). `pixel_tracker` / `vast_tracker` / `daast_tracker` are the renderer-fired measurement-tracker primitives — see `/schemas/core/assets/pixel-tracker-asset.json` and the VAST / DAAST tracker schemas. `object` is a last-resort fallback for structured non-asset inputs that don't fit any primitive asset_type — prefer specific types whenever possible."), "required": z.boolean().describe("Whether this slot is required for a valid manifest.").optional(), "min": z.number().int().gte(0).describe("Minimum count for repeatable / pool slots.").optional(), "max": z.number().int().gte(1).describe("Maximum count for repeatable / pool slots.").optional(), "max_chars": z.number().int().gte(1).describe("Per-slot character limit. Valid only when `asset_type` is `text`, `markdown`, or `brief`. Mutually exclusive with `max_size_kb` (which applies to binary asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "max_size_kb": z.number().int().gte(1).describe("Per-slot file size limit in kilobytes. Valid only when `asset_type` is `image`, `video`, `audio`, or `zip`. Mutually exclusive with `max_chars` (which applies to text asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "pixel_ratios": z.array(z.number().gt(0)).min(1).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Accepted intrinsic-pixel densities for this image-bearing slot. Valid when `asset_type` is `image`, and on a `card` slot where it constrains each card's image media (video media is unaffected). This makes density available to every canonical carrying image assets (native, carousel, responsive, companion images, and image itself), not only `format_kind: image`. When the image canonical also declares top-level `params.pixel_ratios`, the effective set is the intersection; an empty intersection is invalid. One matching asset satisfies the slot unless `required_pixel_ratios` requires rendition coverage.").optional(), "required_pixel_ratios": z.array(z.number().gt(0)).min(1).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Required density coverage for an image rendition set. Valid only when `asset_type` is `image` and `pixel_ratios` is also declared. Every value MUST appear in the effective accepted set after intersecting any top-level image `params.pixel_ratios`, and the manifest slot value MUST be an array containing exactly one matching image rendition for each required ratio. Other accepted ratios remain optional. For example, `pixel_ratios: [1, 1.5, 2]` with `required_pixel_ratios: [1, 2]` requires the 1x and 2x renditions while making 1.5x optional. SDKs enforce intersection, subset, coverage, and duplicate-ratio rules because JSON Schema draft-07 cannot express them generically.").optional(), "logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("When `asset_group_id` is `logo`, renderer-facing brand.json logo slots acceptable for this format slot. Producers selecting from brand.json SHOULD prefer `logos[]` entries whose `slots[]` intersects this list, then apply `visual_guidelines.logo_usage_rules[]`.").optional(), "required_logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Subset of `logo_slots` for which this format expects explicit logo coverage. A manifest or brand-derived logo pool SHOULD include at least one usable logo for each required slot; if coverage is missing, builders SHOULD surface a validation warning or approval mapping instead of guessing from prose.").optional(), "description": z.string().describe("Human-readable description of what the slot expects from the buyer.").optional(), "consumed_for_production": z.boolean().describe("Dispatch hint for `build_creative` and v1↔v2 wire translators: when `true`, the slot's value is consumed as INPUT to a production step (host-read script, brief copy fed to generative synthesis, catalog feed driving per-SKU rendering) and is not rendered verbatim. When `false` (default), the slot's value is rendered verbatim on the placement (image bytes, video file, display tag).\n\nMotivates the v1↔v2 dispatch table: pre-v2 buyers shipped production-consumed inputs separately in a `inputs` map on the build_creative request; v2 collapses inputs and rendered assets into a single `assets` map keyed by `asset_group_id`. SDK translators between v1 and v2 use this flag per canonical to know which assets in the v2 manifest map back to v1 `inputs` vs v1 `assets`. Without the per-slot flag the dispatch table lives in adopter code and every SDK gets it slightly different.\n\nProducers SHOULD set this explicitly on slots whose consumption pattern isn't obvious (host-read scripts on `audio_hosted`, briefs on generative `video_hosted`, catalog feeds on `sponsored_placement`). For canonicals where every slot is render-verbatim (`image`, `display_tag`, `video_vast`), the default `false` is sufficient and the flag MAY be omitted.").optional() }).catchall(z.any()).and(z.intersection(z.intersection(z.union([z.any().refine((value) => !z.object({ "asset_type": z.enum(["text","markdown","brief"]) }).safeParse(value).success, "Invalid input: Should NOT be valid against schema"), z.intersection(z.object({ "asset_type": z.enum(["text","markdown","brief"]) }), z.any().refine((value) => !z.object({ "max_size_kb": z.any().nonoptional() }).passthrough().safeParse(value).success, "Invalid input: Should NOT be valid against schema"))]), z.intersection(z.union([z.any().refine((value) => !z.object({ "asset_type": z.enum(["image","video","audio","zip"]) }).safeParse(value).success, "Invalid input: Should NOT be valid against schema"), z.intersection(z.object({ "asset_type": z.enum(["image","video","audio","zip"]) }), z.any().refine((value) => !z.object({ "max_chars": z.any().nonoptional() }).passthrough().safeParse(value).success, "Invalid input: Should NOT be valid against schema"))]), z.union([z.any().refine((value) => !z.any().refine((value) => !z.object({ "asset_group_id": z.literal("logo") }).safeParse(value).success, "Invalid input: Should NOT be valid against schema").safeParse(value).success, "Invalid input: Should NOT be valid against schema"), z.intersection(z.any().refine((value) => !z.object({ "asset_group_id": z.literal("logo") }).safeParse(value).success, "Invalid input: Should NOT be valid against schema"), z.any().refine((value) => !z.union([z.object({ "logo_slots": z.any().nonoptional() }).passthrough(), z.object({ "required_logo_slots": z.any().nonoptional() }).passthrough()]).safeParse(value).success, "Invalid input: Should NOT be valid against schema"))]))), z.intersection(z.any(), z.intersection(z.union([z.any().refine((value) => !z.object({ "required_pixel_ratios": z.any().nonoptional() }).passthrough().safeParse(value).success, "Invalid input: Should NOT be valid against schema"), z.intersection(z.object({ "required_pixel_ratios": z.any().nonoptional() }).passthrough(), z.object({ "asset_type": z.literal("image").optional() }))]), z.union([z.any().refine((value) => !z.object({ "asset_type": z.enum(["url","catalog","published_post","html","css","javascript","webhook","daast","vast","card","object","pixel_tracker","vast_tracker","daast_tracker"]) }).safeParse(value).success, "Invalid input: Should NOT be valid against schema"), z.intersection(z.object({ "asset_type": z.enum(["url","catalog","published_post","html","css","javascript","webhook","daast","vast","card","object","pixel_tracker","vast_tracker","daast_tracker"]) }), z.any().refine((value) => !z.union([z.object({ "max_chars": z.any().nonoptional() }).passthrough(), z.object({ "max_size_kb": z.any().nonoptional() }).passthrough()]).safeParse(value).success, "Invalid input: Should NOT be valid against schema"))])))))).describe("Default manifest slots; which are consumed depends on `supply_mode`. `state_canvases` images MUST carry `state_id` and `breakpoint_id`, and `state_click_urls` entries MUST carry `state_id` (semantic validators resolve the bindings). Component images SHOULD carry `focal_point` for deterministic seller cropping. `landing_page_url` is the default destination (see `clickthrough`). `font_files` MUST contain only buyer-licensed fonts; publisher-proprietary fonts never travel in manifests. Only image, video, text, url, zip, and pixel_tracker slot asset types are accepted — executable types (javascript, html, css, webhook) are rejected even via `slots` overrides.").optional(), "required_connections": z.array(z.object({ "provider": z.string().describe("Stable provider or platform namespace, preferably lowercase. Examples: `social.example`, `shortvideo.example`, or a seller-defined namespace. Omit only when the requirement is provider-agnostic, or when an `authorization_url` fully routes the human to the correct provider-specific connection flow.").optional(), "connection_type": z.enum(["advertiser_account","publisher_identity","post_authorization"]).describe("Kind of downstream connection required. `advertiser_account` is the platform account used to buy/manage ads. `publisher_identity` is the creator, page, channel, organization, or profile that owns source posts. `post_authorization` is a post-scoped grant when the platform authorizes individual posts instead of, or in addition to, the owning identity."), "required_for": z.array(z.string().min(1)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Concrete AdCP protocol operation names that require this downstream connection. Sellers SHOULD include this in product declarations when the requirement is known ahead of time, and in AUTHORIZATION_REQUIRED details when it explains the failed operation. Prefer specific operation names such as `list_creatives`, `sync_creatives`, `create_media_buy`, `get_media_buy_delivery`, or `get_creative_delivery` over broad category labels such as `reporting`.").optional(), "scope": z.enum(["account","identity","post","unknown"]).describe("Granularity of the downstream grant.").optional(), "status": z.enum(["connected","missing","pending","expired","revoked","not_required","unknown"]).describe("Current seller-observed state for this downstream connection when known. Product declarations MAY omit status or use `unknown`; AUTHORIZATION_REQUIRED details SHOULD use `missing`, `expired`, or `revoked` for the connection that blocked the call.").optional(), "connection_id": z.string().describe("Seller-defined identifier for an already-created downstream connection. Omit when no connection exists yet or when exposing it would leak platform/account state.").optional(), "resource_ref": z.object({ "platform_account_id": z.string().describe("Provider-native advertiser or business account id, when safe to disclose.").optional(), "identity_id": z.string().describe("Provider-native creator, page, channel, organization, or profile id, when safe to disclose.").optional(), "handle": z.string().describe("Provider-native public handle for the owning identity, when available.").optional(), "profile_url": z.string().url().describe("Public URL for the owning identity, when available.").optional(), "post_id": z.string().describe("Provider-native post id, when the grant is post-scoped or the failed request referenced a specific post.").optional(), "post_url": z.string().url().describe("Public URL for the referenced post, when available.").optional() }).catchall(z.any()).describe("Optional opaque provider-native resource hint, such as a platform account id, profile URL, handle, channel id, post id, or post URL. This is a hint for routing authorization, not proof that authorization exists.").optional(), "authorization_url": z.string().url().describe("Seller-hosted or provider-hosted URL where a human can complete or restore this downstream connection.").optional(), "authorization_instructions": z.string().describe("Human-readable instructions for completing or restoring this downstream connection.").optional(), "expires_at": z.string().refine(adcpJsonSchemaDateTime, "Invalid date-time").optional() }).catchall(z.any()).and(z.union([z.any().refine((value) => !z.object({ "status": z.enum(["missing","pending","expired","revoked"]) }).safeParse(value).success, "Invalid input: Should NOT be valid against schema"), z.intersection(z.object({ "status": z.enum(["missing","pending","expired","revoked"]) }), z.union([z.object({ "provider": z.any().nonoptional() }).passthrough(), z.object({ "authorization_url": z.any().nonoptional() }).passthrough()]))])).describe("A seller/platform-side connection or grant required by a product, format, or request. This is not the AdCP caller credential: the AdCP request is still authenticated once, and the seller uses these stored downstream connections to call a platform or service on the buyer's behalf. Use this shape for platforms that require more than one downstream grant, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.")).describe("Downstream platform connections or grants required to use this format declaration. These are in addition to the single AdCP caller credential. Use this when a platform product requires multiple downstream grants, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.").optional(), "reference_mutability": z.enum(["immutable_snapshot","mutable_requires_reapproval","mutable_auto_recheck"]).describe("Policy for formats whose `slots` accept a `published_post` reference. `immutable_snapshot`: seller snapshots the referenced post at approval and later source changes do not change the served creative. `mutable_requires_reapproval`: the source post may change and material changes require review before continued serving. `mutable_auto_recheck`: the source post may change and the seller continuously or periodically rechecks authorization/policy without requiring buyer resubmission. Omit when the format has no `published_post` slot.").optional(), "production_window_business_days": z.number().int().gte(0).describe("Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative).").optional(), "supply_mode": z.enum(["components","rendered_canvases","layered_source"]).describe("Which end of the template contract the buyer feeds. `components`: buyer supplies component slots; seller renders states (no `state_canvases`/`layered_source` assets allowed). `rendered_canvases`: buyer supplies exactly one `state_canvases` image per declared state × breakpoint pair. `layered_source`: buyer ships design source (+ optional `font_files`); seller production derives states (`production_window_business_days` applies) — transitional for sellers without executable templates.").optional(), "states": z.array(z.object({ "state_id": z.string().regex(new RegExp("^[a-z][a-z0-9_]*$")).describe("Stable identifier used by `state_canvases[].state_id` and `state_click_urls[].state_id`."), "anchoring": z.enum(["inline","sticky_top","sticky_bottom","overlay","fullscreen_overlay","underlay"]).describe("`underlay` renders the canvas beneath page content, which scrolls over it (IAB New Ad Portfolio underlay class: skins, reveal units). Transitions into `overlay`/`fullscreen_overlay` states SHOULD be user-initiated; non-user-action entries emit LEAN policy warnings."), "slot_bindings": z.array(z.string().regex(new RegExp("^[a-z0-9_]+$"))).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Asset group IDs rendered in this state (components mode). Makes the template executable: given components and bindings, assembly is deterministic. Every value MUST resolve to a declared slot. Omitted means all supplied component slots may render.").optional(), "motion": z.enum(["static","animated"]).describe("Whether the seller-rendered layout animates within this state (attract loops, load animations). Intra-state animation is seller-rendered; buyer canvases stay static images.").optional(), "max_animation_s": z.number().gt(0).describe("Upper bound on intra-state animation duration. Required when `motion` is `animated`.").optional(), "breakpoints": z.array(z.object({ "breakpoint_id": z.string().regex(new RegExp("^[a-z][a-z0-9_]*$")).describe("Stable identifier used by `state_canvases[].breakpoint_id`."), "width": z.number().int().gte(1).optional(), "width_range": z.array(z.number().int().gte(1)).min(2).max(2).describe("Inclusive [minimum, maximum] accepted canvas width. The first value MUST be no greater than the second; semantic validators enforce ordering.").optional(), "width_mode": z.enum(["full_bleed","gutter_residual"]).describe("Fluid width classes. `full_bleed`: edge-to-edge viewport width (IAB adhesion, topscroll). `gutter_residual`: the margin remaining beside the page content column (desktop skins). Fluid breakpoints cannot be targeted by `rendered_canvases` supply — semantic validation rejects that pairing.").optional(), "height": z.number().int().gte(1).optional(), "height_range": z.array(z.number().int().gte(1)).min(2).max(2).describe("Inclusive [minimum, maximum] accepted canvas height. The first value MUST be no greater than the second; semantic validators enforce ordering.").optional(), "viewport_height_percent": z.number().gt(0).lte(100).describe("Height as a percentage of the viewport (topscroll 80%, mobile skin panel 15%). Fluid; cannot be targeted by `rendered_canvases` supply.").optional(), "canvas_aspect_ratio": z.string().regex(new RegExp("^[0-9]+(\\.[0-9]+)?:[0-9]+(\\.[0-9]+)?$")).describe("Aspect ratio the rendered canvas maintains across the size range (IAB flexible units, ratio-bucket responsive). When both fixed `width` and `height` are declared, the ratio MUST be consistent with them.").optional() }).catchall(z.any()).and(z.intersection(z.union([z.object({ "width": z.any().nonoptional() }).passthrough(), z.object({ "width_range": z.any().nonoptional() }).passthrough(), z.object({ "width_mode": z.any().nonoptional() }).passthrough()]), z.union([z.object({ "height": z.any().nonoptional() }).passthrough(), z.object({ "height_range": z.any().nonoptional() }).passthrough(), z.object({ "viewport_height_percent": z.any().nonoptional() }).passthrough()])))).min(1).superRefine((breakpoints, ctx) => { + breakpoints.forEach((breakpoint, index) => { + const widthKeys = ["width", "width_range", "width_mode"].filter(key => breakpoint[key] !== undefined); + const heightKeys = ["height", "height_range", "viewport_height_percent"].filter(key => breakpoint[key] !== undefined); + if (widthKeys.length !== 1) ctx.addIssue({ code: "custom", path: [index], message: "breakpoint requires exactly one width mode" }); + if (heightKeys.length !== 1) ctx.addIssue({ code: "custom", path: [index], message: "breakpoint requires exactly one height mode" }); + }); + }), "close_affordance": z.boolean().describe("Whether this state visibly renders a seller-controlled close affordance. When true, rendering MUST follow IAB New Ad Portfolio close-button guidance (top-right, minimum 50×50 dp, available from state entry).") }).catchall(z.any())).min(1).describe("Finite visual states known at buy time; state and breakpoint IDs form the canvas-key matrix. Runtime causes live in `transitions[]`. A single-state unit (topscroll, interscroller, skin) declares one state, no transitions, and typically a `reveal` mechanic."), "initial_state_id": z.string().regex(new RegExp("^[a-z][a-z0-9_]*$")).describe("State rendered when the unit first becomes visible. MUST resolve to `states[].state_id`; for a single-state unit it MUST equal the sole state."), "reveal": z.enum(["none","clip_window","scroll_parallax"]).describe("How the unit enters view, distinct from state changes. `clip_window`: canvas fixed and progressively exposed through a scrolling window (interscroller, topscroll). `scroll_parallax`: canvas moves at a different rate than content. Reveal is presentation of one canvas, not a transition; do not fabricate a second state to express it.").optional(), "transitions": z.array(z.object({ "transition_id": z.string().regex(new RegExp("^[a-z][a-z0-9_]*$")).describe("Stable transition identifier for preview and reporting."), "from_state_id": z.string().regex(new RegExp("^[a-z][a-z0-9_]*$")), "to_state_id": z.string().regex(new RegExp("^[a-z][a-z0-9_]*$")), "trigger": z.enum(["timer","in_view_timer","scroll_threshold","scroll_progress","user_action","media_event"]).describe("Cause of the transition. `timer` counts from state entry; `in_view_timer` counts viewable time in the current state (industry auto-collapse is N seconds in view). `media_event` fires on `video_main` playback milestones. The outcome is expressed separately by `to_state_id`."), "input": z.enum(["tap","hover","swipe_up","swipe_down","swipe_left","swipe_right","scroll","expand_control","collapse_control"]).describe("Input driving a user or scroll transition. `hover` expansion is disallowed by IAB NAP guidance and emits a LEAN policy warning visible to buyers.").optional(), "media_event": z.enum(["video_start","video_complete"]).describe("Playback milestone of `video_main` driving a `media_event` transition (endframes, collapse-on-complete).").optional(), "direction": z.enum(["down","up"]).describe("Scroll direction that arms a `scroll_threshold` transition; enables direction-aware expand/collapse cycles. Re-crossing in the opposite direction does not re-fire this transition.").optional(), "transition_mode": z.enum(["instant","animated","scroll_linked"]).describe("`scroll_linked` continuously interpolates the seller-owned layout between the two declared endpoint states; it is valid only with trigger `scroll_progress` and does not permit buyer scripting."), "delay_ms": z.number().int().gte(0).describe("For `timer`: delay after `from_state_id` activates (re-entry restarts it). For `in_view_timer`: accumulated viewable milliseconds in `from_state_id`. Timer-class transitions in a state cycle MUST declare at least 1000 (anti-strobe floor).").optional(), "duration_ms": z.number().int().gte(0).describe("Duration of a seller-rendered animated transition.").optional(), "scroll_threshold_percent": z.number().gte(0).lte(100).describe("Viewport/page scroll threshold that starts a `scroll_threshold` transition.").optional(), "scroll_reference": z.enum(["document_progress","containing_scroller_progress"]).describe("Reference frame for scroll percentages. Progress is `scroll_offset / max(scroll_extent - viewport_extent, 1) * 100`, measured on either the page document or the nearest seller-declared containing scroller.").optional(), "scroll_start_percent": z.number().gte(0).lte(100).describe("Start of the bounded scroll interval for a `scroll_progress` transition.").optional(), "scroll_end_percent": z.number().gte(0).lte(100).describe("End of the bounded scroll interval for a `scroll_progress` transition. MUST be greater than `scroll_start_percent`.").optional(), "preserve_playback": z.boolean().describe("Whether `video_main` continues without restart while the seller changes state.").optional() }).catchall(z.any()).and(z.any().superRefine((x, ctx) => { + const schemas = [z.object({ "trigger": z.literal("timer"), "transition_mode": z.enum(["instant","animated"]).optional() }), z.object({ "trigger": z.literal("in_view_timer"), "transition_mode": z.enum(["instant","animated"]).optional() }), z.object({ "trigger": z.literal("scroll_threshold"), "input": z.literal("scroll"), "transition_mode": z.enum(["instant","animated"]).optional() }), z.object({ "trigger": z.literal("scroll_progress"), "input": z.literal("scroll"), "transition_mode": z.literal("scroll_linked").optional() }), z.object({ "trigger": z.literal("user_action"), "transition_mode": z.enum(["instant","animated"]).optional() }), z.object({ "trigger": z.literal("media_event"), "transition_mode": z.enum(["instant","animated"]).optional() })]; + const { errors, failed } = schemas.reduce<{ + errors: z.core.$ZodIssue[]; + failed: number; + }>( + ({ errors, failed }, schema) => + ((result) => + result.error + ? { + errors: [...errors, ...result.error.issues], + failed: failed + 1, + } + : { errors, failed })( + schema.safeParse(x), + ), + { errors: [], failed: 0 }, + ); + const passed = schemas.length - failed; + if (passed !== 1) { + ctx.addIssue(errors.length ? { + path: [], + code: "invalid_union", + errors: [errors], + message: "Invalid input: Should pass single schema. Passed " + passed, + } : { + path: [], + code: "custom", + errors: [errors], + message: "Invalid input: Should pass single schema. Passed " + passed, + }); + } + }))).min(1).describe("Bounded seller-rendered transitions between declared visual states. Required when `states` has more than one entry; MUST be omitted for single-state units. Every non-initial state MUST be reachable from `initial_state_id`. Dismissal is terminal unit behavior declared by `user_controls.dismissible`, not a hidden visual state.").optional().superRefine((transitions, ctx) => { + if (transitions === undefined) return; + const rules: Record = { + timer: { required: ["delay_ms"], forbidden: ["input", "media_event", "scroll_reference", "scroll_threshold_percent", "scroll_start_percent", "scroll_end_percent"], modes: ["instant", "animated"] }, + in_view_timer: { required: ["delay_ms"], forbidden: ["input", "media_event", "scroll_reference", "scroll_threshold_percent", "scroll_start_percent", "scroll_end_percent"], modes: ["instant", "animated"] }, + scroll_threshold: { required: ["input", "scroll_reference", "scroll_threshold_percent"], forbidden: ["delay_ms", "media_event", "scroll_start_percent", "scroll_end_percent"], modes: ["instant", "animated"] }, + scroll_progress: { required: ["input", "scroll_reference", "scroll_start_percent", "scroll_end_percent"], forbidden: ["delay_ms", "media_event", "scroll_threshold_percent", "direction"], modes: ["scroll_linked"] }, + user_action: { required: ["input"], forbidden: ["delay_ms", "media_event", "scroll_reference", "scroll_threshold_percent", "scroll_start_percent", "scroll_end_percent", "direction"], modes: ["instant", "animated"] }, + media_event: { required: ["media_event"], forbidden: ["input", "delay_ms", "scroll_reference", "scroll_threshold_percent", "scroll_start_percent", "scroll_end_percent", "direction"], modes: ["instant", "animated"] } + }; + transitions.forEach((transition, index) => { + const rule = rules[transition.trigger]; + if (!rule) return; + rule.required.forEach((key: string) => { + if (transition[key] === undefined) ctx.addIssue({ code: "custom", path: [index, key], message: "transition field is required for this trigger" }); + }); + rule.forbidden.forEach((key: string) => { + if (transition[key] !== undefined) ctx.addIssue({ code: "custom", path: [index, key], message: "transition field is forbidden for this trigger" }); + }); + if (!rule.modes.includes(transition.transition_mode)) ctx.addIssue({ code: "custom", path: [index, "transition_mode"], message: "invalid transition_mode for this trigger" }); + if ((transition.trigger === "scroll_threshold" || transition.trigger === "scroll_progress") && transition.input !== "scroll") { + ctx.addIssue({ code: "custom", path: [index, "input"], message: "scroll transitions require input=scroll" }); + } + }); + }), "clickthrough": z.enum(["required","optional","none"]).describe("Destination policy. `required` (default): manifest MUST supply `landing_page_url`. `optional`: click-optional units (in-feed brand units) may omit it. `none`: unit is non-clickable; manifests MUST NOT supply `landing_page_url` or `state_click_urls`. Per-state overrides via `state_click_urls` entries carrying `state_id`; `landing_page_url` is the fallback for unlisted states.").optional(), "user_controls": z.object({ "dismissible": z.boolean(), "user_collapsible": z.boolean() }).catchall(z.any()).describe("When any state anchors as `overlay` or `fullscreen_overlay`, either `dismissible` MUST be true or that state's `close_affordance` MUST be true (dismissibility floor; semantic validators enforce)."), "canvas_constraints": z.array(z.object({ "constraint": z.enum(["safe_area","reserved_region","decoration_only_edge","no_text_or_logos"]), "state_id": z.string().regex(new RegExp("^[a-z][a-z0-9_]*$")).optional(), "breakpoint_id": z.string().regex(new RegExp("^[a-z][a-z0-9_]*$")).optional(), "region": z.object({ "x": z.number().gte(0), "y": z.number().gte(0), "width": z.number().gt(0), "height": z.number().gt(0), "unit": z.enum(["px","percent"]).optional() }).catchall(z.any()).and(z.union([z.any().refine((value) => !z.object({ "unit": z.literal("percent") }).safeParse(value).success, "Invalid input: Should NOT be valid against schema"), z.intersection(z.object({ "unit": z.literal("percent") }), z.object({ "x": z.any().optional(), "y": z.any().optional(), "width": z.any().optional(), "height": z.any().optional() }))])) }).catchall(z.any()).and(z.union([z.any().refine((value) => !z.object({ "breakpoint_id": z.any().nonoptional() }).passthrough().safeParse(value).success, "Invalid input: Should NOT be valid against schema"), z.object({ "breakpoint_id": z.any().nonoptional(), "state_id": z.any().nonoptional() }).passthrough()])).describe("Rectangular rule applied to buyer artwork. State and breakpoint selectors are optional so the same shape can constrain a coordinated-placement component or a specific stateful-display canvas.")).describe("Rectangular areas constraining buyer artwork. Omitted state/breakpoint selectors apply the constraint to every canvas. For fluid or range-sized breakpoints, use percent-unit regions.").optional(), "duration_ms_range": z.array(z.union([z.number().int().gte(0), z.null()])).min(2).max(2).describe("Accepted embedded-video duration [min, max]. `duration_ms_exact` takes precedence when both are present.").optional().superRefine((range, ctx) => { + if (range !== undefined && !range.some(value => value !== null)) { + ctx.addIssue({ code: "custom", path: [], message: "duration_ms_range requires at least one finite bound" }); + } + }), "duration_ms_exact": z.number().int().gte(1).optional(), "aspect_ratio": z.string().regex(new RegExp("^[0-9]+(\\.[0-9]+)?:[0-9]+(\\.[0-9]+)?$")).describe("Embedded-video aspect ratio.").optional(), "containers": z.array(z.enum(["mp4","webm","mov"])).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").optional(), "video_playback": z.enum(["none","auto_muted","user_initiated"]).optional(), "max_initial_load_kb": z.number().int().gte(1).optional(), "max_subload_kb": z.number().int().gte(1).describe("Ceiling on assets loaded after the window load event (IAB LEAN subload). Pairs with `max_initial_load_kb` to mirror the New Ad Portfolio initial/subload weight pair.").optional(), "polite_load": z.boolean().describe("When true, non-initial assets load only after the host page's window load event (IAB LEAN subload boundary).").optional() }).catchall(z.any()).describe("Seller-rendered display unit whose declaration is an executable template contract: buyer-known visual states, explicit transitions, breakpoint canvases, and per-state slot bindings. The seller owns the runtime; `supply_mode` declares which end the buyer feeds. For machine-rendered `components` and `rendered_canvases` supply, sellers MUST support `preview_creative` returning every state × breakpoint from a candidate manifest. `layered_source` instead follows the asynchronous seller-production preview path after the declared production window. `composition_model: deterministic` describes serving the finished states, not instant derivation from layered source. Buyer-executable HTML/MRAID is `html5`, a buyer-delivered tag is `display_tag`, arbitrary games/hotspots/scripts remain `custom`, and per-impression algorithmic assembly is `responsive_creative`."); + +export const CanonicalFormatBaseSchema = z.object({ + experimental: z.boolean().optional(), + deprecated: z.boolean().optional(), + v1_translatable: z.boolean().optional(), + since_version: z.string().optional(), + migration_target_version: z.string().optional(), + composition_model: z.union([z.literal("deterministic"), z.literal("algorithmic")]).optional(), + provenance_required: z.boolean().optional(), + platform_extensions: z.array(PlatformExtensionReferenceSchema).optional(), + synthesis_nondeterministic: z.boolean().optional(), + slots: z.array(z.object({}).passthrough()).optional(), + required_connections: z.array(DownstreamConnectionRequirementSchema).optional(), + reference_mutability: z.union([z.literal("immutable_snapshot"), z.literal("mutable_requires_reapproval"), z.literal("mutable_auto_recheck")]).optional(), + production_window_business_days: z.number().optional() +}).passthrough(); + +export const Fixed1Schema = FixedSchema; + +export const MultiSize1Schema = MultiSizeSchema; + +export const None1Schema = NoneSchema; + +export const Fixed2Schema = FixedSchema; + +export const MultiSize2Schema = MultiSizeSchema; + +export const None2Schema = NoneSchema; + export const OperatorUnitSchema = z.object({ id: z.string(), name: z.string().optional() @@ -2192,6 +2442,13 @@ export const CommittedMetricSchema = z.union([z.object({ scope: z.literal("vendor"), vendor: BrandReferenceSchema, metric_id: VendorMetricIDSchema, + qualifier: z.object({ + viewability_standard: ViewabilityStandardSchema.optional(), + completion_source: CompletionSourceSchema.optional(), + attribution_methodology: AttributionMethodologySchema.optional(), + attribution_window: DurationSchema.optional(), + lift_dimension: LiftDimensionSchema.optional() + }).passthrough().optional(), committed_at: z.string() }).passthrough()]); @@ -2252,8 +2509,14 @@ export const DemographicPredicateSchema = z.object({ export const BrandReference6Schema = BrandReferenceSchema; -export const CreativeLocalePolicySchema = z.object({ - accepted_language_ranges: z.array(LanguageTagSchema) +export const ImageFormatDeclarationSchema = z.object({ + format_kind: z.literal("image"), + params: CanonicalFormatImageSchema +}).passthrough(); + +export const HTML5FormatDeclarationSchema = z.object({ + format_kind: z.literal("html5"), + params: CanonicalFormatHTML5BannerSchema }).passthrough(); export const DisplayTagFormatDeclarationSchema = z.object({ @@ -2306,19 +2569,16 @@ export const AgentPlacementFormatDeclarationSchema = z.object({ params: CanonicalFormatAgentPlacementAISurfaceSponsoredPlacementSchema }).passthrough(); +export const SellerRenderedStatefulDisplayFormatDeclarationSchema = z.object({ + format_kind: z.literal("seller_rendered_stateful_display"), + params: CanonicalFormatSellerRenderedStatefulDisplaySchema +}).passthrough(); + export const CustomFormatDeclarationSchema = z.object({ format_kind: z.literal("custom"), params: z.object({}).passthrough() }).passthrough(); -export const SizeModeMutex1Schema = SizeModeMutexSchema; - -export const Responsive1Schema = ResponsiveSchema; - -export const SizeModeMutex2Schema = SizeModeMutexSchema; - -export const Responsive2Schema = ResponsiveSchema; - export const ImpairmentSchema = z.object({ impairment_id: z.string(), resource_type: z.union([z.literal("audience"), z.literal("creative"), z.literal("catalog_item"), z.literal("event_source"), z.literal("property")]), @@ -2389,150 +2649,108 @@ export const PriceAdjustmentSchema = z.object({ }); export const PriceBreakdownSchema = z.object({ - list_price: z.number(), - adjustments: z.array(PriceAdjustmentSchema).min(1).max(20) -}).passthrough(); - -export const DemographicTargetingResolutionSchema = z.object({}).passthrough().merge(z.object({ - requested: DemographicTargetingIntentSchema, - applied: DemographicPredicateSchema, - equivalent: z.literal(true), - execution: z.union([z.object({ - type: z.literal("continuous_bounds"), - ext: ExtensionObjectSchema.optional() - }).passthrough(), z.object({ - type: z.literal("enumerated_intervals"), - interval_ids: z.array(z.string()), - ext: ExtensionObjectSchema.optional() - }).passthrough(), z.object({ - type: z.literal("signals"), - signal_refs: z.array(SignalRefSchema), - ext: ExtensionObjectSchema.optional() - }).passthrough()]), - applied_bases: z.array(AgeDeterminationBasisSchema).optional(), - applied_verification_methods: z.array(AgeVerificationMethodSchema).optional(), - ext: ExtensionObjectSchema.optional() -}).passthrough()); - -export const BrandReference4Schema = BrandReferenceSchema; - -export const BrandReference5Schema = BrandReferenceSchema; - -export const CanonicalFormatImageSchema = z.object({}).passthrough().merge(z.object({ - experimental: z.boolean().optional(), - deprecated: z.boolean().optional(), - v1_translatable: z.boolean().optional(), - since_version: z.string().optional(), - migration_target_version: z.string().optional(), - composition_model: z.union([z.literal("deterministic"), z.literal("algorithmic")]).optional(), - provenance_required: z.boolean().optional(), - platform_extensions: z.array(PlatformExtensionReferenceSchema).optional(), - synthesis_nondeterministic: z.boolean().optional(), - slots: z.array(z.object({ - asset_group_id: z.string(), - asset_type: z.union([z.literal("image"), z.literal("video"), z.literal("audio"), z.literal("text"), z.literal("markdown"), z.literal("url"), z.literal("html"), z.literal("css"), z.literal("javascript"), z.literal("vast"), z.literal("daast"), z.literal("webhook"), z.literal("brief"), z.literal("catalog"), z.literal("published_post"), z.literal("zip"), z.literal("card"), z.literal("object"), z.literal("pixel_tracker"), z.literal("vast_tracker"), z.literal("daast_tracker")]), - required: z.boolean().optional(), - min: z.number().optional(), - max: z.number().optional(), - max_chars: z.number().optional(), - max_size_kb: z.number().optional(), - pixel_ratios: z.array(z.number()).optional(), - required_pixel_ratios: z.array(z.number()).optional(), - logo_slots: z.array(LogoSlotSchema).optional(), - required_logo_slots: z.array(LogoSlotSchema).optional(), - description: z.string().optional(), - consumed_for_production: z.boolean().optional() - }).passthrough()).optional(), - required_connections: z.array(DownstreamConnectionRequirementSchema).optional(), - reference_mutability: z.union([z.literal("immutable_snapshot"), z.literal("mutable_requires_reapproval"), z.literal("mutable_auto_recheck")]).optional(), - production_window_business_days: z.number().optional(), - width: z.number().min(1).optional(), - height: z.number().min(1).optional(), - sizes: z.array(z.object({ - width: z.number().min(1), - height: z.number().min(1) - }).passthrough()).optional(), - pixel_ratios: z.array(z.number()).optional(), - min_width: z.number().min(1).optional(), - max_width: z.number().min(1).optional(), - min_height: z.number().min(1).optional(), - max_height: z.number().min(1).optional(), - aspect_ratio: z.string().regex(/^[0-9]+(\.[0-9]+)?:[0-9]+(\.[0-9]+)?$/).optional(), - max_file_size_kb: z.number().min(1).optional(), - image_formats: z.array(z.union([z.literal("jpg"), z.literal("jpeg"), z.literal("png"), z.literal("gif"), z.literal("webp"), z.literal("svg")])).optional(), - ssl_required: z.boolean().optional(), - headline_max_chars: z.number().min(1).optional(), - body_text_max_chars: z.number().min(1).optional(), - cta_values: z.array(z.string()).optional(), - asset_source: z.union([z.literal("buyer_uploaded"), z.literal("publisher_host_recorded"), z.literal("seller_pre_rendered_from_brief"), z.literal("seller_human_designed"), z.literal("agent_synthesized"), z.literal("publisher_owned_reference")]).optional(), - buyer_asset_acceptance: z.union([z.literal("accepted"), z.literal("rejected")]).optional() -}).passthrough()); - -export const PlatformExtensionReference1Schema = PlatformExtensionReferenceSchema; - -export const CanonicalFormatHTML5BannerSchema = z.object({}).passthrough().merge(z.object({ - experimental: z.boolean().optional(), - deprecated: z.boolean().optional(), - v1_translatable: z.boolean().optional(), - since_version: z.string().optional(), - migration_target_version: z.string().optional(), - composition_model: z.union([z.literal("deterministic"), z.literal("algorithmic")]).optional(), - provenance_required: z.boolean().optional(), - platform_extensions: z.array(PlatformExtensionReferenceSchema).optional(), - synthesis_nondeterministic: z.boolean().optional(), - slots: z.array(z.object({ - asset_group_id: z.string(), - asset_type: z.union([z.literal("image"), z.literal("video"), z.literal("audio"), z.literal("text"), z.literal("markdown"), z.literal("url"), z.literal("html"), z.literal("css"), z.literal("javascript"), z.literal("vast"), z.literal("daast"), z.literal("webhook"), z.literal("brief"), z.literal("catalog"), z.literal("published_post"), z.literal("zip"), z.literal("card"), z.literal("object"), z.literal("pixel_tracker"), z.literal("vast_tracker"), z.literal("daast_tracker")]), - required: z.boolean().optional(), - min: z.number().optional(), - max: z.number().optional(), - max_chars: z.number().optional(), - max_size_kb: z.number().optional(), - pixel_ratios: z.array(z.number()).optional(), - required_pixel_ratios: z.array(z.number()).optional(), - logo_slots: z.array(LogoSlotSchema).optional(), - required_logo_slots: z.array(LogoSlotSchema).optional(), - description: z.string().optional(), - consumed_for_production: z.boolean().optional() - }).passthrough()).optional(), - required_connections: z.array(DownstreamConnectionRequirementSchema).optional(), - reference_mutability: z.union([z.literal("immutable_snapshot"), z.literal("mutable_requires_reapproval"), z.literal("mutable_auto_recheck")]).optional(), - production_window_business_days: z.number().optional(), - width: z.number().min(1).optional(), - height: z.number().min(1).optional(), - sizes: z.array(z.object({ - width: z.number().min(1), - height: z.number().min(1) - }).passthrough()).optional(), - min_width: z.number().min(1).optional(), - max_width: z.number().min(1).optional(), - min_height: z.number().min(1).optional(), - max_height: z.number().min(1).optional(), - max_initial_load_kb: z.number().min(1).optional(), - max_polite_load_kb: z.number().min(1).optional(), - host_initiated_subload: z.boolean().optional(), - max_animation_duration_ms: z.number().min(0).optional(), - max_cpu_load_percent: z.number().min(1).max(100).optional(), - mraid_required: z.boolean().optional(), - mraid_version: z.union([z.literal("2.0"), z.literal("3.0")]).optional(), - om_sdk_required: z.boolean().optional(), - clicktag_macro: z.union([z.literal("clickTag"), z.literal("clickTAG")]).optional(), - backup_image_required: z.boolean().optional(), - backup_image_max_size_kb: z.number().min(1).optional(), - ssl_required: z.boolean().optional() -}).passthrough()); + list_price: z.number(), + adjustments: z.array(PriceAdjustmentSchema).min(1).max(20) +}).passthrough(); -export const Fixed1Schema = FixedSchema; +export const DemographicTargetingResolutionSchema = z.object({}).passthrough().merge(z.object({ + requested: DemographicTargetingIntentSchema, + applied: DemographicPredicateSchema, + equivalent: z.literal(true), + execution: z.union([z.object({ + type: z.literal("continuous_bounds"), + ext: ExtensionObjectSchema.optional() + }).passthrough(), z.object({ + type: z.literal("enumerated_intervals"), + interval_ids: z.array(z.string()), + ext: ExtensionObjectSchema.optional() + }).passthrough(), z.object({ + type: z.literal("signals"), + signal_refs: z.array(SignalRefSchema), + ext: ExtensionObjectSchema.optional() + }).passthrough()]), + applied_bases: z.array(AgeDeterminationBasisSchema).optional(), + applied_verification_methods: z.array(AgeVerificationMethodSchema).optional(), + ext: ExtensionObjectSchema.optional() +}).passthrough()); -export const MultiSize1Schema = MultiSizeSchema; +export const BrandReference4Schema = BrandReferenceSchema; -export const None1Schema = NoneSchema; +export const BrandReference5Schema = BrandReferenceSchema; -export const Fixed2Schema = FixedSchema; +export const PlatformExtensionReference1Schema = PlatformExtensionReferenceSchema; -export const MultiSize2Schema = MultiSizeSchema; +const CoordinatedPlacementInlineParamsRuntimeSchemas: Record = { + image: CanonicalFormatImageSchema, + html5: CanonicalFormatHTML5BannerSchema, + display_tag: CanonicalFormatDisplayTagSchema, + image_carousel: CanonicalFormatImageCarouselSchema, + video_hosted: CanonicalFormatHostedVideoSchema, + video_vast: CanonicalFormatVASTVideoSchema, + audio_hosted: CanonicalFormatHostedAudioSchema, + audio_daast: CanonicalFormatDAASTAudioSchema, + sponsored_placement: CanonicalFormatSponsoredPlacementRetailMediaCatalogDrivenSchema, + native_in_feed: CanonicalFormatNativeInFeedSchema, + responsive_creative: CanonicalFormatResponsiveCreativeSchema, + agent_placement: CanonicalFormatAgentPlacementAISurfaceSponsoredPlacementSchema, + seller_rendered_stateful_display: CanonicalFormatSellerRenderedStatefulDisplaySchema +}; -export const None2Schema = NoneSchema; +// @ts-ignore -- preserve the public schema type across lossy TS-to-Zod projection details. +export const CanonicalFormatCoordinatedPlacementsSchema: z.ZodObject<{ [K in keyof CanonicalFormatCoordinatedPlacements]-?: z.ZodType }, any> & z.ZodType, CanonicalFormatCoordinatedPlacements & Record> = z.object({ "experimental": z.boolean().describe("Experimental in AdCP 3.2 while the creative working group gathers implementation evidence for atomic cross-placement composition.").optional(), "deprecated": z.boolean().describe("When true, this canonical (or a seller's specific narrowing of it) is going away. Existing adopters are supported through the deprecation cycle; new adoption is discouraged. Pair with `migration_target_version` to indicate when the canonical is expected to be removed. Distinct from `experimental`: an experimental canonical may stabilize and stop being experimental; a deprecated canonical is on a sunset path.").optional(), "v1_translatable": z.boolean().describe("No v1 named-format equivalent can express a coordinated multi-placement buy.").optional(), "since_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version that introduced this canonical (e.g., '3.1', '3.2'). Lets adopters reason about minimum protocol version requirements when consuming a format declaration. Patch precision is intentionally rejected — canonicals are introduced at minor-version boundaries.").optional(), "migration_target_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version by which the working group expects this canonical to stabilize, surface a breaking revision, or (when `deprecated: true`) be removed. Patch precision is intentionally rejected — canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown').").optional(), "composition_model": z.enum(["deterministic","algorithmic"]).describe("Whether the surface composes deterministically (buyer can predict per-slot rendering — sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing — responsive_creative, agent_placement).").optional(), "provenance_required": z.boolean().describe("When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent.").optional(), "platform_extensions": z.array(z.object({ "uri": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL identifying the extension. `https://` is mandatory — `http://`, `file://`, `data:`, and other schemes are rejected at the schema layer (defense-in-depth on top of the fetch-contract normative rules). The URI base is the owning agent's URL; the path identifies the extension within that agent. Example: 'https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel'. The full fetch contract — SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds — is documented on `product-format-declaration.json#format_schema` and applies to ALL fetches of this reference shape regardless of whether the field is named `format_schema` (load-bearing for validation) or `platform_extensions` (informational); the *transport* rules are identical, only the *consumption* semantics differ."), "digest": z.string().regex(new RegExp("^sha256:[a-f0-9]{64}$")).describe("SHA-256 content digest of the extension definition (sha256:). Used to detect drift — if the agent revises the extension, the digest changes and cached definitions become invalid.") }).catchall(z.any()).describe("Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` — divergent digests in one response indicate producer-side error (e.g., concurrent extension revision mid-render). Buyers encountering divergent digests for the same URI MUST fail closed: treat all references to that URI as unresolved and surface a validation error rather than picking one branch silently. **Across responses**, digest divergence is normal — extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.")).describe("Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\n\n**Collision precedence (normative).** When two or more `platform_extensions[]` entries on the same declaration extend the same target (e.g., both extend `tracking`) with overlapping field names, **array order is authoritative — later entries override earlier ones on a per-field basis** (last-in-array-wins). SDKs MUST surface the overlap via the `errors[]` array on the `get_products` response with a structured code (`FORMAT_DECLARATION_DIVERGENT` is appropriate when the overlap appears across dual-emitted shapes; a producer-self-emitted overlap on a single declaration SHOULD use the same code with `error.details: { collision_kind: \"platform_extension_field\", target, overlapping_fields, winning_extension_uri }`). Producers SHOULD avoid the collision by emitting one extension per target or by partitioning fields across extensions; the deterministic precedence is for last-resort consistency across SDK implementations, not a sanctioned merging strategy.").optional(), "synthesis_nondeterministic": z.boolean().describe("When true, the format's production pipeline is genuinely nondeterministic — the platform cannot guarantee that synthesis from a given input set produces in-spec output. Veo / Sora / Runway-class generative video, and other AI-synthesis flows where output dimensions, duration, or quality vary per run. Implies a different validation contract: predictive `validate_input` is impossible; the platform's own post-synthesis QA loop applies; if the QA loop exhausts without producing a valid artifact, `build_creative` returns task_failed with a synthesis_failed reason. Distinct from `composition_model` (which describes how the surface composes per-slot rendering, not whether synthesis is deterministic). When false or absent, the format's production is predictable enough that `validate_input` can predict output properties from input properties.\n\n**Compatibility with `asset_source` / `item_production_model`**: `synthesis_nondeterministic: true` MAY pair with any of `seller_pre_rendered_from_brief`, `seller_human_designed`, or `agent_synthesized` (the QA loop is concept-level, not source-specific — 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.").optional(), "slots": z.array(z.object({ "asset_group_id": z.string().describe("Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings."), "asset_type": z.enum(["image","video","audio","text","markdown","url","html","css","javascript","vast","daast","webhook","brief","catalog","published_post","zip","card","object","pixel_tracker","vast_tracker","daast_tracker"]).describe("Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `published_post` is an existing-post reference asset, not uploaded media bytes and not a catalog row. `card` is the multi-card carousel element type (see card-asset.json). `pixel_tracker` / `vast_tracker` / `daast_tracker` are the renderer-fired measurement-tracker primitives — see `/schemas/core/assets/pixel-tracker-asset.json` and the VAST / DAAST tracker schemas. `object` is a last-resort fallback for structured non-asset inputs that don't fit any primitive asset_type — prefer specific types whenever possible."), "required": z.boolean().describe("Whether this slot is required for a valid manifest.").optional(), "min": z.number().int().gte(0).describe("Minimum count for repeatable / pool slots.").optional(), "max": z.number().int().gte(1).describe("Maximum count for repeatable / pool slots.").optional(), "max_chars": z.number().int().gte(1).describe("Per-slot character limit. Valid only when `asset_type` is `text`, `markdown`, or `brief`. Mutually exclusive with `max_size_kb` (which applies to binary asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "max_size_kb": z.number().int().gte(1).describe("Per-slot file size limit in kilobytes. Valid only when `asset_type` is `image`, `video`, `audio`, or `zip`. Mutually exclusive with `max_chars` (which applies to text asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "pixel_ratios": z.array(z.number().gt(0)).min(1).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Accepted intrinsic-pixel densities for this image-bearing slot. Valid when `asset_type` is `image`, and on a `card` slot where it constrains each card's image media (video media is unaffected). This makes density available to every canonical carrying image assets (native, carousel, responsive, companion images, and image itself), not only `format_kind: image`. When the image canonical also declares top-level `params.pixel_ratios`, the effective set is the intersection; an empty intersection is invalid. One matching asset satisfies the slot unless `required_pixel_ratios` requires rendition coverage.").optional(), "required_pixel_ratios": z.array(z.number().gt(0)).min(1).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Required density coverage for an image rendition set. Valid only when `asset_type` is `image` and `pixel_ratios` is also declared. Every value MUST appear in the effective accepted set after intersecting any top-level image `params.pixel_ratios`, and the manifest slot value MUST be an array containing exactly one matching image rendition for each required ratio. Other accepted ratios remain optional. For example, `pixel_ratios: [1, 1.5, 2]` with `required_pixel_ratios: [1, 2]` requires the 1x and 2x renditions while making 1.5x optional. SDKs enforce intersection, subset, coverage, and duplicate-ratio rules because JSON Schema draft-07 cannot express them generically.").optional(), "logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("When `asset_group_id` is `logo`, renderer-facing brand.json logo slots acceptable for this format slot. Producers selecting from brand.json SHOULD prefer `logos[]` entries whose `slots[]` intersects this list, then apply `visual_guidelines.logo_usage_rules[]`.").optional(), "required_logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Subset of `logo_slots` for which this format expects explicit logo coverage. A manifest or brand-derived logo pool SHOULD include at least one usable logo for each required slot; if coverage is missing, builders SHOULD surface a validation warning or approval mapping instead of guessing from prose.").optional(), "description": z.string().describe("Human-readable description of what the slot expects from the buyer.").optional(), "consumed_for_production": z.boolean().describe("Dispatch hint for `build_creative` and v1↔v2 wire translators: when `true`, the slot's value is consumed as INPUT to a production step (host-read script, brief copy fed to generative synthesis, catalog feed driving per-SKU rendering) and is not rendered verbatim. When `false` (default), the slot's value is rendered verbatim on the placement (image bytes, video file, display tag).\n\nMotivates the v1↔v2 dispatch table: pre-v2 buyers shipped production-consumed inputs separately in a `inputs` map on the build_creative request; v2 collapses inputs and rendered assets into a single `assets` map keyed by `asset_group_id`. SDK translators between v1 and v2 use this flag per canonical to know which assets in the v2 manifest map back to v1 `inputs` vs v1 `assets`. Without the per-slot flag the dispatch table lives in adopter code and every SDK gets it slightly different.\n\nProducers SHOULD set this explicitly on slots whose consumption pattern isn't obvious (host-read scripts on `audio_hosted`, briefs on generative `video_hosted`, catalog feeds on `sponsored_placement`). For canonicals where every slot is render-verbatim (`image`, `display_tag`, `video_vast`), the default `false` is sufficient and the flag MAY be omitted.").optional() }).catchall(z.any()).and(z.intersection(z.intersection(z.union([z.any().refine((value) => !z.object({ "asset_type": z.enum(["text","markdown","brief"]) }).safeParse(value).success, "Invalid input: Should NOT be valid against schema"), z.intersection(z.object({ "asset_type": z.enum(["text","markdown","brief"]) }), z.any().refine((value) => !z.object({ "max_size_kb": z.any().nonoptional() }).passthrough().safeParse(value).success, "Invalid input: Should NOT be valid against schema"))]), z.intersection(z.union([z.any().refine((value) => !z.object({ "asset_type": z.enum(["image","video","audio","zip"]) }).safeParse(value).success, "Invalid input: Should NOT be valid against schema"), z.intersection(z.object({ "asset_type": z.enum(["image","video","audio","zip"]) }), z.any().refine((value) => !z.object({ "max_chars": z.any().nonoptional() }).passthrough().safeParse(value).success, "Invalid input: Should NOT be valid against schema"))]), z.union([z.any().refine((value) => !z.any().refine((value) => !z.object({ "asset_group_id": z.literal("logo") }).safeParse(value).success, "Invalid input: Should NOT be valid against schema").safeParse(value).success, "Invalid input: Should NOT be valid against schema"), z.intersection(z.any().refine((value) => !z.object({ "asset_group_id": z.literal("logo") }).safeParse(value).success, "Invalid input: Should NOT be valid against schema"), z.any().refine((value) => !z.union([z.object({ "logo_slots": z.any().nonoptional() }).passthrough(), z.object({ "required_logo_slots": z.any().nonoptional() }).passthrough()]).safeParse(value).success, "Invalid input: Should NOT be valid against schema"))]))), z.intersection(z.any(), z.intersection(z.union([z.any().refine((value) => !z.object({ "required_pixel_ratios": z.any().nonoptional() }).passthrough().safeParse(value).success, "Invalid input: Should NOT be valid against schema"), z.intersection(z.object({ "required_pixel_ratios": z.any().nonoptional() }).passthrough(), z.object({ "asset_type": z.literal("image").optional() }))]), z.union([z.any().refine((value) => !z.object({ "asset_type": z.enum(["url","catalog","published_post","html","css","javascript","webhook","daast","vast","card","object","pixel_tracker","vast_tracker","daast_tracker"]) }).safeParse(value).success, "Invalid input: Should NOT be valid against schema"), z.intersection(z.object({ "asset_type": z.enum(["url","catalog","published_post","html","css","javascript","webhook","daast","vast","card","object","pixel_tracker","vast_tracker","daast_tracker"]) }), z.any().refine((value) => !z.union([z.object({ "max_chars": z.any().nonoptional() }).passthrough(), z.object({ "max_size_kb": z.any().nonoptional() }).passthrough()]).safeParse(value).success, "Invalid input: Should NOT be valid against schema"))])))))).describe("Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.").optional(), "required_connections": z.array(z.object({ "provider": z.string().describe("Stable provider or platform namespace, preferably lowercase. Examples: `social.example`, `shortvideo.example`, or a seller-defined namespace. Omit only when the requirement is provider-agnostic, or when an `authorization_url` fully routes the human to the correct provider-specific connection flow.").optional(), "connection_type": z.enum(["advertiser_account","publisher_identity","post_authorization"]).describe("Kind of downstream connection required. `advertiser_account` is the platform account used to buy/manage ads. `publisher_identity` is the creator, page, channel, organization, or profile that owns source posts. `post_authorization` is a post-scoped grant when the platform authorizes individual posts instead of, or in addition to, the owning identity."), "required_for": z.array(z.string().min(1)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Concrete AdCP protocol operation names that require this downstream connection. Sellers SHOULD include this in product declarations when the requirement is known ahead of time, and in AUTHORIZATION_REQUIRED details when it explains the failed operation. Prefer specific operation names such as `list_creatives`, `sync_creatives`, `create_media_buy`, `get_media_buy_delivery`, or `get_creative_delivery` over broad category labels such as `reporting`.").optional(), "scope": z.enum(["account","identity","post","unknown"]).describe("Granularity of the downstream grant.").optional(), "status": z.enum(["connected","missing","pending","expired","revoked","not_required","unknown"]).describe("Current seller-observed state for this downstream connection when known. Product declarations MAY omit status or use `unknown`; AUTHORIZATION_REQUIRED details SHOULD use `missing`, `expired`, or `revoked` for the connection that blocked the call.").optional(), "connection_id": z.string().describe("Seller-defined identifier for an already-created downstream connection. Omit when no connection exists yet or when exposing it would leak platform/account state.").optional(), "resource_ref": z.object({ "platform_account_id": z.string().describe("Provider-native advertiser or business account id, when safe to disclose.").optional(), "identity_id": z.string().describe("Provider-native creator, page, channel, organization, or profile id, when safe to disclose.").optional(), "handle": z.string().describe("Provider-native public handle for the owning identity, when available.").optional(), "profile_url": z.string().url().describe("Public URL for the owning identity, when available.").optional(), "post_id": z.string().describe("Provider-native post id, when the grant is post-scoped or the failed request referenced a specific post.").optional(), "post_url": z.string().url().describe("Public URL for the referenced post, when available.").optional() }).catchall(z.any()).describe("Optional opaque provider-native resource hint, such as a platform account id, profile URL, handle, channel id, post id, or post URL. This is a hint for routing authorization, not proof that authorization exists.").optional(), "authorization_url": z.string().url().describe("Seller-hosted or provider-hosted URL where a human can complete or restore this downstream connection.").optional(), "authorization_instructions": z.string().describe("Human-readable instructions for completing or restoring this downstream connection.").optional(), "expires_at": z.string().refine(adcpJsonSchemaDateTime, "Invalid date-time").optional() }).catchall(z.any()).and(z.union([z.any().refine((value) => !z.object({ "status": z.enum(["missing","pending","expired","revoked"]) }).safeParse(value).success, "Invalid input: Should NOT be valid against schema"), z.intersection(z.object({ "status": z.enum(["missing","pending","expired","revoked"]) }), z.union([z.object({ "provider": z.any().nonoptional() }).passthrough(), z.object({ "authorization_url": z.any().nonoptional() }).passthrough()]))])).describe("A seller/platform-side connection or grant required by a product, format, or request. This is not the AdCP caller credential: the AdCP request is still authenticated once, and the seller uses these stored downstream connections to call a platform or service on the buyer's behalf. Use this shape for platforms that require more than one downstream grant, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.")).describe("Downstream platform connections or grants required to use this format declaration. These are in addition to the single AdCP caller credential. Use this when a platform product requires multiple downstream grants, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.").optional(), "reference_mutability": z.enum(["immutable_snapshot","mutable_requires_reapproval","mutable_auto_recheck"]).describe("Policy for formats whose `slots` accept a `published_post` reference. `immutable_snapshot`: seller snapshots the referenced post at approval and later source changes do not change the served creative. `mutable_requires_reapproval`: the source post may change and material changes require review before continued serving. `mutable_auto_recheck`: the source post may change and the seller continuously or periodically rechecks authorization/policy without requiring buyer resubmission. Omit when the format has no `published_post` slot.").optional(), "production_window_business_days": z.number().int().gte(0).describe("Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative).").optional(), "components": z.array(z.object({ "component_id": z.string().regex(new RegExp("^[a-z][a-z0-9_]*$")).describe("Stable coordination-local component identifier. Values MUST be unique within `components[]`."), "placement_ref": z.object({ "publisher_domain": z.string().regex(new RegExp("^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$")).describe("Domain where the adagents.json declaring this placement is hosted. Omitted only for legacy single-publisher seller contexts where the seller agent's own publisher domain is the namespace.").optional(), "placement_id": z.string().describe("Placement ID from the publisher's adagents.json placement catalog, or an inline seller-defined placement ID interpreted within the same publisher namespace.") }).catchall(z.any()).describe("Public product placement this component supplies. MUST resolve against the containing product's `placements[]`."), "required": z.boolean(), "sequence": z.number().int().gte(1).describe("Declared presentation order for sequential-messaging coordinated buys. Components sharing a sequence value present simultaneously; absent means unordered/simultaneous (default). Sequence declares seller-rendered ordering, not buyer-controlled timing.").optional(), "serving_policy": z.enum(["seller_served_only","third_party_allowed"]).describe("Per-component serving/tracking policy. Some sellers restrict specific components (e.g., page skins) to first-party serving while allowing third-party tags on sibling components. Defaults to the product-level policy when absent.").optional(), "canvas_constraints": z.array(z.object({ "constraint": z.enum(["safe_area","reserved_region","decoration_only_edge","no_text_or_logos"]), "state_id": z.string().regex(new RegExp("^[a-z][a-z0-9_]*$")).optional(), "breakpoint_id": z.string().regex(new RegExp("^[a-z][a-z0-9_]*$")).optional(), "region": z.object({ "x": z.number().gte(0), "y": z.number().gte(0), "width": z.number().gt(0), "height": z.number().gt(0), "unit": z.enum(["px","percent"]).optional() }).catchall(z.any()).and(z.union([z.any().refine((value) => !z.object({ "unit": z.literal("percent") }).safeParse(value).success, "Invalid input: Should NOT be valid against schema"), z.intersection(z.object({ "unit": z.literal("percent") }), z.object({ "x": z.any().optional(), "y": z.any().optional(), "width": z.any().optional(), "height": z.any().optional() }))])) }).catchall(z.any()).and(z.union([z.any().refine((value) => !z.object({ "breakpoint_id": z.any().nonoptional() }).passthrough().safeParse(value).success, "Invalid input: Should NOT be valid against schema"), z.object({ "breakpoint_id": z.any().nonoptional(), "state_id": z.any().nonoptional() }).passthrough()])).describe("Rectangular rule applied to buyer artwork. State and breakpoint selectors are optional so the same shape can constrain a coordinated-placement component or a specific stateful-display canvas.")).describe("Artwork constraints applied to this component, including safe areas and seller-reserved regions.").optional(), "format_option_ref": z.record(z.string(), z.any()).and(z.any().superRefine((x, ctx) => { + const schemas = [z.object({ "scope": z.literal("publisher").describe("Reference resolves against the named publisher's adagents.json top-level `formats[]` catalog."), "publisher_domain": z.string().regex(new RegExp("^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$")).describe("Publisher domain where the adagents.json declaring this format option is hosted."), "format_option_id": z.string().describe("Stable format option ID from the publisher's adagents.json top-level `formats[]`, matching a publisher-catalog-backed entry in the target product's `format_options[]`.") }).catchall(z.any()).describe("Selects a publisher-catalog-backed product format option by publisher domain and format option ID."), z.object({ "scope": z.literal("product").describe("Reference resolves only against the target product's inline `format_options[]`."), "format_option_id": z.string().describe("Stable format option ID from the target product's inline `format_options[]`."), "publisher_domain": z.never().optional() }).catchall(z.any()).describe("Selects a product-local format option by ID within the enclosing package/product context. This branch deliberately forbids `publisher_domain` (`publisher_domain: false` in the schema) because product-local references are namespaced by the enclosing product only; include `scope: \"publisher\"` when the selector must cross into a publisher catalog.")]; + const { errors, failed } = schemas.reduce<{ + errors: z.core.$ZodIssue[]; + failed: number; + }>( + ({ errors, failed }, schema) => + ((result) => + result.error + ? { + errors: [...errors, ...result.error.issues], + failed: failed + 1, + } + : { errors, failed })( + schema.safeParse(x), + ), + { errors: [], failed: 0 }, + ); + const passed = schemas.length - failed; + if (passed !== 1) { + ctx.addIssue(errors.length ? { + path: [], + code: "invalid_union", + errors: [errors], + message: "Invalid input: Should pass single schema. Passed " + passed, + } : { + path: [], + code: "custom", + errors: [errors], + message: "Invalid input: Should pass single schema. Passed " + passed, + }); + } + })).describe("Reference to a sibling `format_options[]` entry on the same product. It MUST NOT resolve to `custom` or `coordinated_placements`.").optional(), "format_kind": z.string().optional(), "params": z.record(z.string(), z.any()).optional() }).catchall(z.any()).and(z.union([z.object({ "format_option_ref": z.any().nonoptional() }).passthrough(), z.object({ "format_kind": z.any().nonoptional(), "params": z.any().nonoptional() }).passthrough()]))).min(2).superRefine((components, ctx) => { + if (!components.some(component => component.required === true)) { + ctx.addIssue({ code: "custom", path: [], message: "At least one component must be required" }); + } + components.forEach((component, index) => { + const referenced = component.format_option_ref !== undefined; + const hasKind = component.format_kind !== undefined; + const hasParams = component.params !== undefined; + if (referenced === (hasKind || hasParams) || (!referenced && (!hasKind || !hasParams))) { + ctx.addIssue({ code: "custom", path: [index], message: "Each component must select exactly one referenced or inline format" }); + return; + } + if (!referenced) { + const paramsSchema = CoordinatedPlacementInlineParamsRuntimeSchemas[component.format_kind]; + if (!paramsSchema) { + ctx.addIssue({ code: "custom", path: [index, "format_kind"], message: "Unsupported coordinated placement format_kind" }); + } else if (!paramsSchema.safeParse(component.params).success) { + ctx.addIssue({ code: "custom", path: [index, "params"], message: "params do not match format_kind" }); + } + } + }); + }), "shared_slots": z.array(z.object({ "asset_group_id": z.string().regex(new RegExp("^[a-z0-9_]+$")), "asset_type": z.enum(["image","video","audio","text","markdown","url","html","css","javascript","vast","daast","webhook","brief","catalog","published_post","zip","card","object","pixel_tracker","vast_tracker","daast_tracker"]), "required": z.boolean().optional(), "min": z.number().int().gte(0).optional(), "max": z.number().int().gte(1).optional(), "consumed_by": z.array(z.string()).min(1).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Component IDs that consume this shared asset. Every value MUST resolve to `components[].component_id`.") }).catchall(z.any())).describe("Manifest slots supplied once and consumed by one or more coordinated components.").optional() }).catchall(z.any()).describe("One creative manifest atomically supplies assets for multiple declared product placements. Each component binds to a public `Product.placements[]` entry and either declares an inline non-custom canonical format or references a sibling format option on the same product. Components cannot nest coordinated placements. The manifest supplies component slots under `component_assets.`; `shared_slots` assets are supplied once at top level. Inventory exclusivity remains `Product.exclusivity`, not a creative-format parameter. Ordinary products whose placements accept independently assigned creatives do not need this canonical."); export const NamedFormatProductSchema = z.object({}).passthrough(); @@ -3291,80 +3509,23 @@ export const CanonicalMetricQualifierSchema = z.object({ attribution_methodology: AttributionMethodologySchema.optional(), attribution_window: DurationSchema.optional(), lift_dimension: LiftDimensionSchema.optional() -}).passthrough(); +}).strict(); -export const CanonicalFormatOptionSchema = z.object({}).passthrough().merge(z.object({ - format_option_id: z.string().optional(), - publisher_domain: z.string().optional(), - display_name: z.string().optional(), - sample_render_url: z.string().optional(), - applies_to_channels: z.array(MediaChannelSchema).optional(), - seller_preference: z.union([z.literal("preferred"), z.literal("accepted"), z.literal("discouraged")]).optional(), - locale_policy: CreativeLocalePolicySchema.optional(), - canonical_formats_only: z.boolean().optional(), - experimental: z.boolean().optional(), - format_kind: z.union([z.literal("image"), z.literal("html5"), z.literal("display_tag"), z.literal("image_carousel"), z.literal("video_hosted"), z.literal("video_vast"), z.literal("audio_hosted"), z.literal("audio_daast"), z.literal("sponsored_placement"), z.literal("native_in_feed"), z.literal("responsive_creative"), z.literal("agent_placement"), z.literal("custom")]), - params: z.object({}).passthrough(), - format_shape: z.string().optional(), - format_schema: PlatformExtensionReferenceSchema.optional() -}).passthrough()).merge(z.object({ - format_option_id: z.string().optional(), - publisher_domain: z.string().optional(), - display_name: z.string().optional(), - sample_render_url: z.string().optional(), - applies_to_channels: z.array(MediaChannelSchema).optional(), - seller_preference: z.union([z.literal("preferred"), z.literal("accepted"), z.literal("discouraged")]).optional(), - locale_policy: CreativeLocalePolicySchema.optional(), - canonical_formats_only: z.boolean().optional(), - experimental: z.boolean().optional(), - format_kind: z.union([z.literal("image"), z.literal("html5"), z.literal("display_tag"), z.literal("image_carousel"), z.literal("video_hosted"), z.literal("video_vast"), z.literal("audio_hosted"), z.literal("audio_daast"), z.literal("sponsored_placement"), z.literal("native_in_feed"), z.literal("responsive_creative"), z.literal("agent_placement"), z.literal("custom")]), - params: z.object({}).passthrough(), - format_shape: z.string().optional(), - format_schema: PlatformExtensionReferenceSchema.optional() -}).passthrough()).merge(z.object({ - format_option_id: z.string().optional(), - publisher_domain: z.string().optional(), - display_name: z.string().optional(), - sample_render_url: z.string().optional(), - applies_to_channels: z.array(MediaChannelSchema).optional(), - seller_preference: z.union([z.literal("preferred"), z.literal("accepted"), z.literal("discouraged")]).optional(), - locale_policy: CreativeLocalePolicySchema.optional(), - canonical_formats_only: z.boolean().optional(), - experimental: z.boolean().optional(), - format_kind: z.union([z.literal("image"), z.literal("html5"), z.literal("display_tag"), z.literal("image_carousel"), z.literal("video_hosted"), z.literal("video_vast"), z.literal("audio_hosted"), z.literal("audio_daast"), z.literal("sponsored_placement"), z.literal("native_in_feed"), z.literal("responsive_creative"), z.literal("agent_placement"), z.literal("custom")]), - params: z.object({}).passthrough(), - format_shape: z.string().optional(), - format_schema: PlatformExtensionReferenceSchema.optional() -}).passthrough()).merge(z.object({ - format_option_id: z.string().optional(), - publisher_domain: z.string().optional(), - display_name: z.string().optional(), - sample_render_url: z.string().optional(), +export const CanonicalFormatOptionSchema = z.object({ + format_option_id: z.string().min(1).optional(), + publisher_domain: z.string().regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/).optional(), + display_name: z.string().min(1).optional(), + sample_render_url: z.string().regex(/^https:\/\//).optional(), applies_to_channels: z.array(MediaChannelSchema).optional(), seller_preference: z.union([z.literal("preferred"), z.literal("accepted"), z.literal("discouraged")]).optional(), locale_policy: CreativeLocalePolicySchema.optional(), canonical_formats_only: z.boolean().optional(), experimental: z.boolean().optional(), - format_kind: z.union([z.literal("image"), z.literal("html5"), z.literal("display_tag"), z.literal("image_carousel"), z.literal("video_hosted"), z.literal("video_vast"), z.literal("audio_hosted"), z.literal("audio_daast"), z.literal("sponsored_placement"), z.literal("native_in_feed"), z.literal("responsive_creative"), z.literal("agent_placement"), z.literal("custom")]), + format_kind: z.union([z.literal("image"), z.literal("html5"), z.literal("display_tag"), z.literal("image_carousel"), z.literal("video_hosted"), z.literal("video_vast"), z.literal("audio_hosted"), z.literal("audio_daast"), z.literal("sponsored_placement"), z.literal("native_in_feed"), z.literal("responsive_creative"), z.literal("agent_placement"), z.literal("seller_rendered_stateful_display"), z.literal("coordinated_placements"), z.literal("custom")]), params: z.object({}).passthrough(), - format_shape: z.string().optional(), + format_shape: z.string().min(1).optional(), format_schema: PlatformExtensionReferenceSchema.optional() -}).passthrough()); - -export const CanonicalProductPlacementSchema = z.object({}).passthrough().merge(z.object({ - kind: z.union([z.literal("publisher_ref"), z.literal("seller_inline")]), - placement_id: z.string(), - publisher_domain: z.string().optional(), - name: z.string().optional(), - description: z.string().optional(), - mode: z.union([z.literal("targetable"), z.literal("included")]), - tags: z.array(z.string()).optional(), - format_options: z.array(CanonicalFormatOptionSchema).optional(), - video_placement_types: z.array(VideoPlacementTypeSchema).optional(), - audio_distribution_types: z.array(AudioDistributionTypeSchema).optional(), - sponsored_placement_types: z.array(SponsoredPlacementTypeSchema).optional(), - social_placement_surfaces: z.array(SocialPlacementSurfaceSchema).optional() -}).passthrough()); +}).passthrough(); export const CanonicalAudienceEvidenceSchema = z.object({}).passthrough().merge(z.object({}).passthrough()).merge(z.object({}).passthrough()).merge(z.object({ evidence_id: z.string(), @@ -3725,6 +3886,21 @@ export const CanonicalForecastVendorMetricValueSchema = z.object({ breakdown: z.object({}).passthrough().optional() }).passthrough(); +export const CanonicalProductPlacementSchema = z.object({}).passthrough().merge(z.object({ + kind: z.union([z.literal("publisher_ref"), z.literal("seller_inline")]), + placement_id: z.string(), + publisher_domain: z.string().optional(), + name: z.string().optional(), + description: z.string().optional(), + mode: z.union([z.literal("targetable"), z.literal("included")]), + tags: z.array(z.string()).optional(), + format_options: z.array(CanonicalFormatOptionSchema).optional(), + video_placement_types: z.array(VideoPlacementTypeSchema).optional(), + audio_distribution_types: z.array(AudioDistributionTypeSchema).optional(), + sponsored_placement_types: z.array(SponsoredPlacementTypeSchema).optional(), + social_placement_surfaces: z.array(SocialPlacementSurfaceSchema).optional() +}).passthrough()); + export const CanonicalReportingCapabilitiesSchema = z.object({ available_reporting_frequencies: z.array(ReportingFrequencySchema), expected_delay_minutes: z.number(), @@ -3736,6 +3912,7 @@ export const CanonicalReportingCapabilitiesSchema = z.object({ metric_id: VendorMetricIDSchema }).passthrough()).optional(), supports_creative_breakdown: z.boolean().optional(), + supports_format_breakdown: z.boolean().optional(), supports_keyword_breakdown: z.boolean().optional(), supports_geo_breakdown: GeographicBreakdownSupportSchema.optional(), supports_device_type_breakdown: z.boolean().optional(), @@ -5794,7 +5971,14 @@ export const MissingMetricSchema = z.union([z.object({ }).passthrough(), z.object({ scope: z.literal("vendor"), vendor: BrandReferenceSchema, - metric_id: VendorMetricIDSchema + metric_id: VendorMetricIDSchema, + qualifier: z.object({ + viewability_standard: ViewabilityStandardSchema.optional(), + completion_source: CompletionSourceSchema.optional(), + attribution_methodology: AttributionMethodologySchema.optional(), + attribution_window: DurationSchema.optional(), + lift_dimension: LiftDimensionSchema.optional() + }).passthrough().optional() }).passthrough()]); export const OpportunityContextSchema = z.object({}).passthrough().merge(z.object({ @@ -5821,7 +6005,14 @@ export const PerformanceFeedbackMetricSchema = z.union([z.object({ }).passthrough(), z.object({ scope: z.literal("vendor"), vendor: BrandReferenceSchema, - metric_id: VendorMetricIDSchema + metric_id: VendorMetricIDSchema, + qualifier: z.object({ + viewability_standard: ViewabilityStandardSchema.optional(), + completion_source: CompletionSourceSchema.optional(), + attribution_methodology: AttributionMethodologySchema.optional(), + attribution_window: DurationSchema.optional(), + lift_dimension: LiftDimensionSchema.optional() + }).passthrough().optional() }).passthrough()]); export const PerformanceFeedbackAssertionSchema = z.object({ @@ -5877,7 +6068,14 @@ export const PerformanceFeedbackSchema = z.object({ }).passthrough(), z.object({ scope: z.literal("vendor"), vendor: BrandReferenceSchema, - metric_id: VendorMetricIDSchema + metric_id: VendorMetricIDSchema, + qualifier: z.object({ + viewability_standard: ViewabilityStandardSchema.optional(), + completion_source: CompletionSourceSchema.optional(), + attribution_methodology: AttributionMethodologySchema.optional(), + attribution_window: DurationSchema.optional(), + lift_dimension: LiftDimensionSchema.optional() + }).passthrough().optional() }).passthrough()]).optional(), feedback_source: FeedbackSourceSchema, vendor: BrandReference1Schema.optional(), @@ -5907,14 +6105,9 @@ export const PublisherDesignatedPreviewProviderSchema = z.object({ }).passthrough()) }).passthrough(); -export const ImageFormatDeclarationSchema = z.object({ - format_kind: z.literal("image"), - params: CanonicalFormatImageSchema -}).passthrough(); - -export const HTML5FormatDeclarationSchema = z.object({ - format_kind: z.literal("html5"), - params: CanonicalFormatHTML5BannerSchema +export const CoordinatedPlacementsFormatDeclarationSchema = z.object({ + format_kind: z.literal("coordinated_placements"), + params: CanonicalFormatCoordinatedPlacementsSchema }).passthrough(); export const LayerSchema = z.union([z.literal("behind_creative"), z.literal("in_front_of_creative")]); @@ -6453,7 +6646,8 @@ export const CanonicalTransformerOutputsSchema = z.object({}).passthrough(); export const NamedFormatTransformerOutputsSchema = z.object({}).passthrough(); -export const ProductFormatDeclarationSchema = z.object({}).passthrough().merge(z.object({}).passthrough()).merge(z.object({}).passthrough()).merge(z.object({}).passthrough()).merge(z.object({}).passthrough()).merge(z.object({ +// @ts-ignore -- preserve the public schema type across lossy TS-to-Zod projection details. +export const ProductFormatDeclarationSchema: z.ZodObject<{ [K in keyof ProductFormatDeclaration]-?: z.ZodType }, any> & z.ZodType, ProductFormatDeclaration & Record> = z.object({}).passthrough().merge(z.object({}).passthrough()).merge(z.object({}).passthrough()).merge(z.object({}).passthrough()).merge(z.object({}).passthrough()).merge(z.object({ format_option_id: z.string().optional(), publisher_domain: z.string().optional(), display_name: z.string().optional(), @@ -6466,7 +6660,7 @@ export const ProductFormatDeclarationSchema = z.object({}).passthrough().merge(z format_shape: z.string().optional(), v1_format_ref: z.array(FormatReferenceStructuredObjectSchema).optional(), format_schema: PlatformExtensionReferenceSchema.optional() -}).passthrough()).and(z.union([ImageFormatDeclarationSchema, HTML5FormatDeclarationSchema, DisplayTagFormatDeclarationSchema, ImageCarouselFormatDeclarationSchema, HostedVideoFormatDeclarationSchema, VASTVideoFormatDeclarationSchema, HostedAudioFormatDeclarationSchema, DAASTAudioFormatDeclarationSchema, SponsoredPlacementFormatDeclarationSchema, NativeInFeedFormatDeclarationSchema, ResponsiveCreativeFormatDeclarationSchema, AgentPlacementFormatDeclarationSchema, CustomFormatDeclarationSchema])).and(z.object({ +}).passthrough()).and(z.union([ImageFormatDeclarationSchema, HTML5FormatDeclarationSchema, DisplayTagFormatDeclarationSchema, ImageCarouselFormatDeclarationSchema, HostedVideoFormatDeclarationSchema, VASTVideoFormatDeclarationSchema, HostedAudioFormatDeclarationSchema, DAASTAudioFormatDeclarationSchema, SponsoredPlacementFormatDeclarationSchema, NativeInFeedFormatDeclarationSchema, ResponsiveCreativeFormatDeclarationSchema, AgentPlacementFormatDeclarationSchema, SellerRenderedStatefulDisplayFormatDeclarationSchema, CoordinatedPlacementsFormatDeclarationSchema, CustomFormatDeclarationSchema])).and(z.object({ format_option_id: z.string().optional(), publisher_domain: z.string().optional(), display_name: z.string().optional(), @@ -6479,7 +6673,7 @@ export const ProductFormatDeclarationSchema = z.object({}).passthrough().merge(z format_shape: z.string().optional(), v1_format_ref: z.array(FormatReferenceStructuredObjectSchema).optional(), format_schema: PlatformExtensionReferenceSchema.optional() -}).passthrough()).and(z.union([ImageFormatDeclarationSchema, HTML5FormatDeclarationSchema, DisplayTagFormatDeclarationSchema, ImageCarouselFormatDeclarationSchema, HostedVideoFormatDeclarationSchema, VASTVideoFormatDeclarationSchema, HostedAudioFormatDeclarationSchema, DAASTAudioFormatDeclarationSchema, SponsoredPlacementFormatDeclarationSchema, NativeInFeedFormatDeclarationSchema, ResponsiveCreativeFormatDeclarationSchema, AgentPlacementFormatDeclarationSchema, CustomFormatDeclarationSchema])).and(z.object({ +}).passthrough()).and(z.union([ImageFormatDeclarationSchema, HTML5FormatDeclarationSchema, DisplayTagFormatDeclarationSchema, ImageCarouselFormatDeclarationSchema, HostedVideoFormatDeclarationSchema, VASTVideoFormatDeclarationSchema, HostedAudioFormatDeclarationSchema, DAASTAudioFormatDeclarationSchema, SponsoredPlacementFormatDeclarationSchema, NativeInFeedFormatDeclarationSchema, ResponsiveCreativeFormatDeclarationSchema, AgentPlacementFormatDeclarationSchema, SellerRenderedStatefulDisplayFormatDeclarationSchema, CoordinatedPlacementsFormatDeclarationSchema, CustomFormatDeclarationSchema])).and(z.object({ format_option_id: z.string().optional(), publisher_domain: z.string().optional(), display_name: z.string().optional(), @@ -6492,7 +6686,7 @@ export const ProductFormatDeclarationSchema = z.object({}).passthrough().merge(z format_shape: z.string().optional(), v1_format_ref: z.array(FormatReferenceStructuredObjectSchema).optional(), format_schema: PlatformExtensionReferenceSchema.optional() -}).passthrough()).and(z.union([ImageFormatDeclarationSchema, HTML5FormatDeclarationSchema, DisplayTagFormatDeclarationSchema, ImageCarouselFormatDeclarationSchema, HostedVideoFormatDeclarationSchema, VASTVideoFormatDeclarationSchema, HostedAudioFormatDeclarationSchema, DAASTAudioFormatDeclarationSchema, SponsoredPlacementFormatDeclarationSchema, NativeInFeedFormatDeclarationSchema, ResponsiveCreativeFormatDeclarationSchema, AgentPlacementFormatDeclarationSchema, CustomFormatDeclarationSchema])).and(z.object({ +}).passthrough()).and(z.union([ImageFormatDeclarationSchema, HTML5FormatDeclarationSchema, DisplayTagFormatDeclarationSchema, ImageCarouselFormatDeclarationSchema, HostedVideoFormatDeclarationSchema, VASTVideoFormatDeclarationSchema, HostedAudioFormatDeclarationSchema, DAASTAudioFormatDeclarationSchema, SponsoredPlacementFormatDeclarationSchema, NativeInFeedFormatDeclarationSchema, ResponsiveCreativeFormatDeclarationSchema, AgentPlacementFormatDeclarationSchema, SellerRenderedStatefulDisplayFormatDeclarationSchema, CoordinatedPlacementsFormatDeclarationSchema, CustomFormatDeclarationSchema])).and(z.object({ format_option_id: z.string().optional(), publisher_domain: z.string().optional(), display_name: z.string().optional(), @@ -6505,7 +6699,7 @@ export const ProductFormatDeclarationSchema = z.object({}).passthrough().merge(z format_shape: z.string().optional(), v1_format_ref: z.array(FormatReferenceStructuredObjectSchema).optional(), format_schema: PlatformExtensionReferenceSchema.optional() -}).passthrough()).and(z.union([ImageFormatDeclarationSchema, HTML5FormatDeclarationSchema, DisplayTagFormatDeclarationSchema, ImageCarouselFormatDeclarationSchema, HostedVideoFormatDeclarationSchema, VASTVideoFormatDeclarationSchema, HostedAudioFormatDeclarationSchema, DAASTAudioFormatDeclarationSchema, SponsoredPlacementFormatDeclarationSchema, NativeInFeedFormatDeclarationSchema, ResponsiveCreativeFormatDeclarationSchema, AgentPlacementFormatDeclarationSchema, CustomFormatDeclarationSchema])); +}).passthrough()).and(z.union([ImageFormatDeclarationSchema, HTML5FormatDeclarationSchema, DisplayTagFormatDeclarationSchema, ImageCarouselFormatDeclarationSchema, HostedVideoFormatDeclarationSchema, VASTVideoFormatDeclarationSchema, HostedAudioFormatDeclarationSchema, DAASTAudioFormatDeclarationSchema, SponsoredPlacementFormatDeclarationSchema, NativeInFeedFormatDeclarationSchema, ResponsiveCreativeFormatDeclarationSchema, AgentPlacementFormatDeclarationSchema, SellerRenderedStatefulDisplayFormatDeclarationSchema, CoordinatedPlacementsFormatDeclarationSchema, CustomFormatDeclarationSchema])); export const VendorPricingOptionSchema = z.object({ pricing_option_id: z.string(), @@ -6672,6 +6866,11 @@ export const ValidateInputResultSchema = z.object({}).passthrough().merge(z.obje predicted: z.object({}).passthrough().optional(), field: z.string(), retry_with: z.object({}).passthrough().optional() + }).passthrough()).optional(), + warnings: z.array(z.object({ + rule: z.string(), + expected: z.unknown().optional(), + predicted: z.unknown().optional() }).passthrough()).optional() }).passthrough()); @@ -7191,7 +7390,8 @@ export const SIUIElementSchema = z.object({}).passthrough().merge(z.object({ data: z.object({}).passthrough().optional() }).passthrough()); -export const AvailablePackageSchema = z.object({ +// @ts-ignore -- preserve the public schema type across lossy TS-to-Zod projection details. +export const AvailablePackageSchema: z.ZodObject<{ [K in keyof AvailablePackage]-?: z.ZodType }, any> & z.ZodType, AvailablePackage & Record> = z.object({ package_id: z.string(), media_buy_id: z.string(), seller_agent: SellerAgentReferenceSchema, @@ -7529,7 +7729,8 @@ export const GeographicPlaceRequirementSchema = z.object({ systems: z.record(z.string(), CatalogRequirementSchema) }).passthrough(); -export const PlacementSchema = z.object({}).passthrough().merge(z.object({}).passthrough()).merge(z.object({}).passthrough()).and(z.union([z.object({}).passthrough(), z.object({}).passthrough()])).and(z.object({ +// @ts-ignore -- preserve the public schema type across lossy TS-to-Zod projection details. +export const PlacementSchema: z.ZodType, Placement & Record> = z.object({}).passthrough().merge(z.object({}).passthrough()).merge(z.object({}).passthrough()).and(z.union([z.object({}).passthrough(), z.object({}).passthrough()])).and(z.object({ kind: z.union([z.literal("publisher_ref"), z.literal("seller_inline")]), placement_id: z.string(), publisher_domain: z.string().optional(), @@ -7578,6 +7779,7 @@ export const ReportingCapabilitiesSchema = z.object({ metric_id: VendorMetricIDSchema }).passthrough()).optional(), supports_creative_breakdown: z.boolean().optional(), + supports_format_breakdown: z.boolean().optional(), supports_keyword_breakdown: z.boolean().optional(), supports_geo_breakdown: GeographicBreakdownSupportSchema.optional(), supports_device_type_breakdown: z.boolean().optional(), @@ -8057,6 +8259,10 @@ export const GetMediaBuyDeliveryRequestSchema = z.object({ start_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(), end_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(), include_package_daily_breakdown: z.boolean().optional(), + requested_metrics: z.array(AvailableMetricSchema).min(1).refine( + values => new Set(values).size === values.length, + { message: "requested_metrics must contain unique metrics" } + ).optional(), time_granularity: ReportingFrequencySchema.optional(), include_window_breakdown: z.boolean().optional(), attribution_window: z.object({ @@ -8065,36 +8271,62 @@ export const GetMediaBuyDeliveryRequestSchema = z.object({ model: AttributionModelSchema.optional() }).passthrough().optional(), reporting_dimensions: z.object({ + catalog_item: z.object({ + limit: z.number().min(1).optional(), + sort_by: SortMetricSchema.optional(), + sort_direction: SortDirectionSchema.optional() + }).passthrough().optional(), + creative: z.object({ + limit: z.number().min(1).optional(), + sort_by: SortMetricSchema.optional(), + sort_direction: SortDirectionSchema.optional() + }).passthrough().optional(), + keyword: z.object({ + limit: z.number().min(1).optional(), + sort_by: SortMetricSchema.optional(), + sort_direction: SortDirectionSchema.optional() + }).passthrough().optional(), geo: z.object({ geo_level: GeographicTargetingLevelSchema, system: z.union([MetroAreaSystemSchema, PostalCodeSystemSchema, CountryFusedPostalCodeSystemSchema]).optional(), country: z.string().regex(/^[A-Z]{2}$/).optional(), limit: z.number().min(1).optional(), - sort_by: SortMetricSchema.optional() + sort_by: SortMetricSchema.optional(), + sort_direction: SortDirectionSchema.optional() }).passthrough().optional(), device_type: z.object({ limit: z.number().min(1).optional(), - sort_by: SortMetricSchema.optional() + sort_by: SortMetricSchema.optional(), + sort_direction: SortDirectionSchema.optional() }).passthrough().optional(), device_platform: z.object({ limit: z.number().min(1).optional(), - sort_by: SortMetricSchema.optional() + sort_by: SortMetricSchema.optional(), + sort_direction: SortDirectionSchema.optional() + }).passthrough().optional(), + format: z.object({ + limit: z.number().min(1).optional(), + sort_by: SortMetricSchema.optional(), + sort_direction: SortDirectionSchema.optional() }).passthrough().optional(), audience: z.object({ limit: z.number().min(1).optional(), - sort_by: SortMetricSchema.optional() + sort_by: SortMetricSchema.optional(), + sort_direction: SortDirectionSchema.optional() }).passthrough().optional(), demographic: z.object({ age_ranges: z.array(DemographicAgeRangeSchema).optional(), limit: z.number().min(1).optional(), - sort_by: SortMetricSchema.optional() + sort_by: SortMetricSchema.optional(), + sort_direction: SortDirectionSchema.optional() }).passthrough().optional(), spot: z.object({ limit: z.number().min(1).optional() }).passthrough().optional(), placement: z.object({ limit: z.number().min(1).optional(), - sort_by: SortMetricSchema.optional() + sort_by: SortMetricSchema.optional(), + sort_direction: SortDirectionSchema.optional() }).passthrough().optional() }).passthrough().optional(), context: ContextObjectSchema.optional(), @@ -8104,13 +8336,7 @@ export const GetMediaBuyDeliveryRequestSchema = z.object({ export const DeliveryMetricAggregateSchema = z.union([z.object({ scope: z.literal("standard"), metric_id: AvailableMetricSchema, - qualifier: z.object({ - viewability_standard: ViewabilityStandardSchema.optional(), - completion_source: CompletionSourceSchema.optional(), - attribution_methodology: AttributionMethodologySchema.optional(), - attribution_window: DurationSchema.optional(), - lift_dimension: LiftDimensionSchema.optional() - }).passthrough().optional(), + qualifier: CanonicalMetricQualifierSchema.optional(), value: z.number(), measurable_impressions: z.number().min(0).optional(), viewable_impressions: z.number().min(0).optional(), @@ -8124,10 +8350,23 @@ export const DeliveryMetricAggregateSchema = z.union([z.object({ scope: z.literal("vendor"), vendor: BrandReferenceSchema, metric_id: VendorMetricIDSchema, - qualifier: z.object({}).passthrough().optional(), + qualifier: CanonicalMetricQualifierSchema.optional(), value: z.number(), measurable_impressions: z.number().min(0).optional() - }).passthrough()]); + }).passthrough()]).superRefine((row, ctx) => { + if (row.scope !== "standard") return; + const requiredComponents: Record = { + viewable_rate: ["measurable_impressions", "viewable_impressions"], + completion_rate: ["impressions", "completed_views"], + cost_per_acquisition: ["spend", "conversions"], + roas: ["spend", "conversion_value"] + }; + for (const field of requiredComponents[row.metric_id] ?? []) { + if ((row as unknown as Record)[field] === undefined) { + ctx.addIssue({ code: "custom", path: [field], message: `${field} is required for ${row.metric_id}` }); + } + } +}); export const VendorMetricValueSchema = z.object({ vendor: BrandReferenceSchema, @@ -8135,6 +8374,7 @@ export const VendorMetricValueSchema = z.object({ value: z.number(), unit: z.string().optional(), measurable_impressions: z.number().optional(), + qualifier: CanonicalMetricQualifierSchema.optional(), breakdown: z.object({}).passthrough().optional() }).passthrough(); @@ -8680,7 +8920,7 @@ const CreativeAssetValueSchema: z.ZodType = z.unknown().superRefine((value, ctx) } }); -const CreativeAssetsSchema: z.ZodType> = z.record(z.string(), z.unknown()).superRefine((assets, ctx) => { +const CreativeAssetsRuntimeSchema: z.ZodType> = z.record(z.string(), z.unknown()).superRefine((assets, ctx) => { for (const [slotKey, assetValue] of Object.entries(assets)) { if (/^[a-z0-9_]+$/.test(slotKey) && !CreativeAssetValueSchema.safeParse(assetValue).success) { ctx.addIssue({ @@ -8692,17 +8932,18 @@ const CreativeAssetsSchema: z.ZodType> = z.record(z.stri } }); -export const CreativeManifestSchema = z.object({ +export const CreativeManifestSchema = z.object({}).passthrough().merge(z.object({ format_id: FormatReferenceStructuredObjectSchema.optional(), format_kind: CanonicalFormatKindSchema.optional(), format_option_ref: FormatOptionReferenceSchema.optional(), - assets: CreativeAssetsSchema, + assets: CreativeAssetsRuntimeSchema, + component_assets: z.record(z.string(), CreativeAssetsSchema).optional(), brand: BrandReferenceSchema.optional(), rights: z.array(RightsConstraintSchema).optional(), industry_identifiers: z.array(IndustryIdentifierSchema).optional(), provenance: ProvenanceSchema.optional(), ext: ExtensionObjectSchema.optional() -}).passthrough().superRefine((value, ctx) => { +}).passthrough()).superRefine((value, ctx) => { const hasFormatId = value.format_id !== undefined; const hasFormatKind = value.format_kind !== undefined; if (hasFormatId === hasFormatKind) { @@ -8918,7 +9159,8 @@ export const ListTransformersRequestCreativeAgentSchema = z.object({ ext: ExtensionObjectSchema.optional() }).passthrough(); -export const TransformerSchema = z.object({ +// @ts-ignore -- preserve the public schema type across lossy TS-to-Zod projection details. +export const TransformerSchema: z.ZodObject<{ [K in keyof Transformer]-?: z.ZodType }, any> & z.ZodType, Transformer & Record> = z.object({ transformer_id: z.string(), name: z.string(), description: z.string().optional(), @@ -8953,7 +9195,8 @@ export const TransformerSchema = z.object({ }).passthrough().optional() }).passthrough(); -export const ListTransformersResponseCreativeAgentSchema = z.object({ +// @ts-ignore -- preserve the public schema type across lossy TS-to-Zod projection details. +export const ListTransformersResponseCreativeAgentSchema: z.ZodObject<{ [K in keyof ListTransformersResponseCreativeAgent]-?: z.ZodType }, any> & z.ZodType, ListTransformersResponseCreativeAgent & Record> = z.object({ context_id: z.string().optional(), context: ContextObjectSchema.optional(), task_id: z.string().optional(), @@ -9028,6 +9271,12 @@ export const DeliveryMetricsSchema = z.object({ q3_views: z.number().min(0).optional(), q4_views: z.number().min(0).optional() }).passthrough().optional().nullable(), + time_based_views: z.array(z.object({ + threshold_seconds: z.number().gt(0), + basis: ViewThresholdBasisSchema, + views: z.number().min(0), + standard: ViewabilityStandardSchema.optional() + }).passthrough()).optional(), dooh_metrics: z.object({ loop_plays: z.number().min(0).optional(), screens_used: z.number().min(0).optional(), @@ -9143,13 +9392,14 @@ export const TargetLocalizationReadbackSchema = z.object({ assets: ResolvedAssetsSchema }).passthrough(); -export const CreativeAssetSchema = z.object({ +export const CreativeAssetSchema = z.object({}).passthrough().merge(z.object({ creative_id: z.string(), name: z.string(), format_id: FormatReferenceStructuredObjectSchema.optional(), format_kind: CanonicalFormatKindSchema.optional(), format_option_ref: FormatOptionReferenceSchema.optional(), - assets: CreativeAssetsSchema, + assets: CreativeAssetsRuntimeSchema, + component_assets: z.record(z.string(), CreativeAssetsSchema).optional(), inputs: z.array(z.object({ name: z.string(), macros: z.record(z.string(), z.string()).optional(), @@ -9163,7 +9413,7 @@ export const CreativeAssetSchema = z.object({ industry_identifiers: z.array(IndustryIdentifierSchema).optional(), provenance: ProvenanceSchema.optional(), rights: z.array(RightsConstraintSchema).optional() -}).passthrough().superRefine((value, ctx) => { +}).passthrough()).superRefine((value, ctx) => { const hasFormatId = value.format_id !== undefined; const hasFormatKind = value.format_kind !== undefined; if (hasFormatId === hasFormatKind) { @@ -11467,7 +11717,8 @@ export const RepeatableGroupAssetSchema = z.object({ export const ListTransformersRequestSchema = ListTransformersRequestCreativeAgentSchema; -export const ListTransformersResponseSchema = ListTransformersResponseCreativeAgentSchema; +// @ts-ignore -- preserve the public schema type across lossy TS-to-Zod projection details. +export const ListTransformersResponseSchema: z.ZodObject<{ [K in keyof ListTransformersResponse]-?: z.ZodType }, any> & z.ZodType, ListTransformersResponse & Record> = ListTransformersResponseCreativeAgentSchema; export const TargetingOverlaySupportSchema = z.object({ geo_countries: CountrySupportSchema.optional(), @@ -12858,22 +13109,6 @@ export const AudienceEvidenceSelectionSchema: z.ZodType = z.object({ ext: ExtensionObjectSchema.optional() }).passthrough(); -export const CanonicalFormatBaseSchema = z.object({ - experimental: z.boolean().optional(), - deprecated: z.boolean().optional(), - v1_translatable: z.boolean().optional(), - since_version: z.string().optional(), - migration_target_version: z.string().optional(), - composition_model: z.union([z.literal("deterministic"), z.literal("algorithmic")]).optional(), - provenance_required: z.boolean().optional(), - platform_extensions: z.array(PlatformExtensionReference1Schema).optional(), - synthesis_nondeterministic: z.boolean().optional(), - slots: z.array(z.object({}).passthrough()).optional(), - required_connections: z.array(DownstreamConnectionRequirementSchema).optional(), - reference_mutability: z.union([z.literal("immutable_snapshot"), z.literal("mutable_requires_reapproval"), z.literal("mutable_auto_recheck")]).optional(), - production_window_business_days: z.number().optional() -}).passthrough(); - export const PricingOptionSchema = z.union([CPMPricingOptionSchema, VCPMPricingOptionSchema, CPCPricingOptionSchema, CPCVPricingOptionSchema, CPVPricingOptionSchema, CPPPricingOptionSchema, CPAPricingOptionSchema, RevenueSharePricingOptionSchema, FlatRatePricingOptionSchema, TimeBasedPricingOptionSchema]); export const DeliveryForecastSchema = z.object({ @@ -13627,7 +13862,8 @@ export const SourceLocalizationReadbackSchema = z.object({ assets: ResolvedAssetsSchema }).passthrough(); -export const FormatSchema = z.object({ +// @ts-ignore -- preserve the public schema type across lossy TS-to-Zod projection details. +export const FormatSchema: z.ZodObject<{ [K in keyof Format]-?: z.ZodType }, any> & z.ZodType, Format & Record> = z.object({ format_id: FormatReferenceStructuredObjectSchema, name: z.string(), description: z.string().optional(), @@ -13738,7 +13974,7 @@ export const InlineDeclarationSchema = z.object({}).passthrough().merge(z.object format_shape: z.string().optional(), v1_format_ref: z.array(FormatReferenceStructuredObjectSchema).optional(), format_schema: PlatformExtensionReferenceSchema.optional() -}).passthrough()).and(z.union([ImageFormatDeclarationSchema, HTML5FormatDeclarationSchema, DisplayTagFormatDeclarationSchema, ImageCarouselFormatDeclarationSchema, HostedVideoFormatDeclarationSchema, VASTVideoFormatDeclarationSchema, HostedAudioFormatDeclarationSchema, DAASTAudioFormatDeclarationSchema, SponsoredPlacementFormatDeclarationSchema, NativeInFeedFormatDeclarationSchema, ResponsiveCreativeFormatDeclarationSchema, AgentPlacementFormatDeclarationSchema, CustomFormatDeclarationSchema])); +}).passthrough()).and(z.union([ImageFormatDeclarationSchema, HTML5FormatDeclarationSchema, DisplayTagFormatDeclarationSchema, ImageCarouselFormatDeclarationSchema, HostedVideoFormatDeclarationSchema, VASTVideoFormatDeclarationSchema, HostedAudioFormatDeclarationSchema, DAASTAudioFormatDeclarationSchema, SponsoredPlacementFormatDeclarationSchema, NativeInFeedFormatDeclarationSchema, ResponsiveCreativeFormatDeclarationSchema, AgentPlacementFormatDeclarationSchema, SellerRenderedStatefulDisplayFormatDeclarationSchema, CoordinatedPlacementsFormatDeclarationSchema, CustomFormatDeclarationSchema])); export const BoxDecorationSchema = z.object({ kind: z.literal("box"), @@ -15230,7 +15466,7 @@ export const CanonicalProposalSchema: z.ZodObject<{ [K in keyof CanonicalProposa }); } }).describe("Canonical fixed or seller-optimized allocation using only canonical optimization goals.").optional(), "pacing": z.enum(["even","asap","front_loaded"]).describe("Budget pacing strategy").optional(), "bidding": z.object({ "automatic": z.literal(true).describe("Explicitly use seller/provider automatic bidding at this authored scope. At package scope this is a complete override of a media-buy policy, not inheritance. It MUST be the only field in the block and MUST be preserved on readback.").optional(), "bid_amount": z.number().gt(0).describe("Manual auction bid denominated in the media-buy currency and expressed per the selected pricing option's auction unit. For example, a CPM option interprets the amount per thousand impressions. This is the amount submitted to the auction, not a promise that the clearing price equals it. Requires an auction-priced pricing option whose currency equals the media-buy currency.").optional(), "max_bid": z.number().gt(0).describe("Hard per-auction ceiling denominated in the media-buy currency and expressed per the selected pricing option's auction unit. This is the only canonical hard auction ceiling and MUST NOT be translated into an average outcome-cost control. Requires an auction-priced pricing option whose currency equals the media-buy currency. May stand alone or supplement cost_per/roas only when the relevant scope capability advertises that combination.").optional(), "cost_per": z.object({ "amount": z.number().gt(0).describe("Average cost amount per scope-bound primary-goal result, denominated in the media-buy currency."), "strength": z.enum(["cap","target"]).describe("`cap` optimizes for an average at or below the amount and accepts underdelivery when necessary; `target` optimizes around the amount while balancing volume and spend. Neither is a per-result or per-auction guarantee.") }).passthrough().describe("Average cost control per result of the scope-bound primary optimization goal. At seller-optimized media-buy scope it binds to budget_allocation.optimization_goals; at package scope it binds to that package's optimization_goals; at fixed media-buy scope it binds independently to each inheriting package and is valid only when their primary-goal result units are compatible. Metric goals are compatible only when metric and every result-defining qualifier match; vendor_metric goals only when vendor and metric_id match; event goals only when the event_type/custom_event_name set and resolved attribution_window match. Primary is the earliest array entry among goals tied for the lowest explicit numeric priority; unprioritized goals follow explicitly prioritized goals; when all priorities are absent, the first entry is primary.").optional(), "roas": z.object({ "value": z.number().gt(0).describe("Return per unit of ad spend; 4 means 4 units of value per 1 unit spent."), "strength": z.enum(["floor","target"]).describe("`floor` prefers underdelivery to knowingly optimizing below the requested return; `target` optimizes around the requested return. Neither guarantees realized return.") }).passthrough().describe("Dimensionless return-on-ad-spend control bound to the same scope-specific primary goal rules as cost_per. The bound goal must be value-bearing; a fixed media-buy default requires a value-bearing primary goal on every inheriting package. Every referenced value-bearing event source MUST declare value_currencies containing the media-buy currency. The seller validates this at buy creation; each buy consumes only exact-currency records, while other declared currencies remain available to other buys. Sellers MUST NOT perform currency conversion.").optional() }).passthrough().and(z.any().refine((value) => !z.union([z.object({ "automatic": z.any().nonoptional(), "bid_amount": z.any().nonoptional() }).passthrough(), z.object({ "automatic": z.any().nonoptional(), "max_bid": z.any().nonoptional() }).passthrough(), z.object({ "automatic": z.any().nonoptional(), "cost_per": z.any().nonoptional() }).passthrough(), z.object({ "automatic": z.any().nonoptional(), "roas": z.any().nonoptional() }).passthrough(), z.object({ "bid_amount": z.any().nonoptional(), "max_bid": z.any().nonoptional() }).passthrough(), z.object({ "bid_amount": z.any().nonoptional(), "cost_per": z.any().nonoptional() }).passthrough(), z.object({ "bid_amount": z.any().nonoptional(), "roas": z.any().nonoptional() }).passthrough(), z.object({ "cost_per": z.any().nonoptional(), "roas": z.any().nonoptional() }).passthrough()]).safeParse(value).success, "Invalid input: Should NOT be valid against schema")).describe("Buyer-authored execution policy for automatic delivery, auction bidding, average outcome cost, or return on ad spend. The containing object determines authored scope: media-buy `bidding` is a complete inherited default and package `bidding` is a complete package override. Sellers MUST preserve authored scope on readback and MUST NOT copy an inherited media-buy policy into package `bidding`. Every monetary field in this block is denominated in the media-buy currency; the selected pricing option supplies the auction unit, never another denomination. Auction-unit identity is the pricing_model plus every canonical billing-event qualifier after defaults are applied: for example CPV view threshold, CPP demographic system/demographic, CPA event tuple, time time_unit, and flat-rate/DOOH parameters. An extension qualifier participates only when its registered extension specification explicitly defines how it contributes to auction-unit identity. A media-buy bid_amount or max_bid is valid only when every inheriting package resolves the same auction-unit identity. Every affected pricing option MUST use the media-buy currency; split currency-mismatched packages into separate buys. Seller-optimized media-buy cost_per/roas bind to the primary budget_allocation.optimization_goals goal. Package-authored cost_per/roas bind to the package primary optimization goal. The primary goal is the earliest array entry among goals with the lowest explicit numeric priority; unprioritized goals follow explicitly prioritized goals; when all priorities are absent, the first entry is primary. In fixed allocation, an inherited media-buy cost_per is valid only when all inheriting packages have compatible primary-goal result units; inherited roas requires value-bearing primary goals on every inheriting package. Canonical ROAS requires each value-bearing event source to declare the media-buy currency in value_currencies; each buy consumes only exact-currency records and sellers MUST NOT convert them. Absence invokes inheritance or provider automatic delivery; `{automatic:true}` is an explicit authored policy that overrides inheritance. Sellers MUST reject unsupported modes, combinations, units, currency, goal bindings, or native placements before any provider mutation and MUST NOT silently translate semantics.").refine((value) => Object.keys(value).length >= 1, "Object must contain at least 1 property").optional(), "invoice_recipient": z.object({ "legal_name": z.string().max(200).describe("Registered legal name of the business entity"), "vat_id": z.string().regex(new RegExp("^[A-Z]{2}[A-Z0-9]{2,13}$")).describe("VAT identification number (e.g., DE123456789 for Germany, FR12345678901 for France). Required for B2B invoicing in the EU. Must be normalized: no spaces, dots, or dashes.").optional(), "tax_id": z.string().max(30).describe("Tax identification number for jurisdictions that do not use VAT (e.g., US EIN)").optional(), "registration_number": z.string().max(50).describe("Company registration number (e.g., HRB 12345 for German Handelsregister)").optional(), "address": z.object({ "street": z.string().max(200).describe("Street address including building number"), "city": z.string().max(100), "postal_code": z.string().max(20), "region": z.string().max(100).describe("State, province, or region").optional(), "country": z.string().regex(new RegExp("^[A-Z]{2}$")).describe("ISO 3166-1 alpha-2 country code") }).passthrough().describe("Postal address for invoicing and legal correspondence").optional(), "contacts": z.array(z.object({ "role": z.enum(["billing","legal","creative","general"]).describe("Contact's functional role in the business relationship"), "name": z.string().max(200).describe("Full name of the contact").optional(), "email": z.string().email().max(254).optional(), "phone": z.string().max(30).optional() }).passthrough()).max(10).describe("Contacts for billing, legal, and operational matters. Contains personal data subject to GDPR and equivalent regulations. Implementations MUST use this data only for invoicing and account management.").optional(), "bank": z.object({ "account_holder": z.string().max(200).describe("Name on the bank account"), "iban": z.string().regex(new RegExp("^[A-Z]{2}[0-9]{2}[A-Z0-9]{4,30}$")).describe("International Bank Account Number (SEPA markets)").optional(), "bic": z.string().regex(new RegExp("^[A-Z]{4}[A-Z]{2}[A-Z0-9]{2}([A-Z0-9]{3})?$")).describe("Bank Identifier Code / SWIFT code (SEPA markets)").optional(), "routing_number": z.string().max(30).describe("Bank routing number for non-SEPA markets (e.g., US ABA routing number, Canadian transit/institution number)").optional(), "account_number": z.string().max(30).describe("Bank account number for non-SEPA markets").optional() }).passthrough().describe("Bank account details for payment processing. Write-only: included in requests to provide payment coordinates, but MUST NOT be echoed in responses. Sellers store these details and confirm receipt without returning them.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension object for platform-specific, vendor-namespaced parameters. Extensions are always optional and must be namespaced under a vendor/platform key (e.g., ext.gam, ext.roku). Used for custom capabilities, partner-specific configuration, and features being proposed for standardization.").optional() }).passthrough().describe("Structured business identity for B2B invoicing and contracts. Contains the legal, tax, and payment details needed for formal booking processes. Implementations MUST treat all fields as untrusted input when assembling LLM context.").optional(), "purchase_order_ref": z.string().min(1).max(255).optional(), "agency_estimate_number": z.string().max(100).optional(), "reporting_commitments": z.array(z.object({ "purchase_index": z.number().int().gte(0), "metrics": z.array(z.record(z.string(), z.any()).and(z.any().superRefine((x, ctx) => { - const schemas = [z.object({ "scope": z.literal("standard"), "metric_id": z.enum(["impressions","spend","clicks","ctr","views","completed_views","completion_rate","conversions","conversion_value","commissionable_value","roas","cost_per_acquisition","new_to_brand_rate","leads","reach","frequency","grps","engagements","engagement_rate","follows","saves","profile_visits","viewability","quartile_data","dooh_metrics","cost_per_click","cost_per_completed_view","cpm","downloads","units_sold","new_to_brand_units","plays","incremental_sales_lift","brand_lift","foot_traffic","conversion_lift","brand_search_lift"]).describe("Standard delivery and performance metrics available for reporting"), "qualifier": z.object({ "viewability_standard": z.enum(["mrc","groupm"]).describe("Viewability measurement standard applied to determine whether an impression qualifies as viewable. These are materially different thresholds and should not be compared across standards.").optional(), "completion_source": z.enum(["seller_attested","vendor_attested"]).describe("Trust-source disambiguator for `completion_rate` — *who* attested to the completion event, not *how* (methodology granularity is a separate dimension; future qualifier keys may add it if buyer demand surfaces). The two paths can yield materially different rates, particularly in SSAI environments where the player's view of completion may differ from a vendor's. Used as a `qualifier.completion_source` key on `committed_metrics`, `missing_metrics`, and `metric_aggregates` to disambiguate which trust source the row represents. Edge cases: walled gardens where the seller is also the measurement vendor (YouTube, Spotify) collapse to `seller_attested` by trust-model logic — the same party served and counted. IAB-certified first-party podcast measurement (Podtrac, Triton on their own platforms; Art19 on its own platform) likewise collapses to `seller_attested`. The same vendor's offering on a third-party platform (Podtrac on a publisher who isn't Podtrac) is `vendor_attested`. The trust axis is *not* who runs the SDK — it's who is independent of the seller's revenue interest.").optional(), "attribution_methodology": z.enum(["deterministic_purchase","probabilistic","panel_based","modeled"]).describe("How attribution between ad exposure and outcome events was computed. Used as a `qualifier.attribution_methodology` key on `committed_metrics`, `missing_metrics`, `metric_aggregates`, and `performance-feedback.metric` to disambiguate the same outcome metric reported under different methodologies — `conversion_value` measured deterministically (matched purchase IDs) is not the same number as `conversion_value` measured probabilistically (modeled match) and should never be summed across methodologies. The retail-media closed-loop pattern typically reports under `deterministic_purchase`; MMM and clean-room outputs typically report under `modeled` or `probabilistic`; panel-based measurement (Nielsen, comScore, Edison) reports under `panel_based`.").optional(), "attribution_window": z.object({ "interval": z.number().int().gte(1).describe("Number of time units. Must be 1 when unit is 'campaign'."), "unit": z.enum(["seconds","minutes","hours","days","campaign"]).describe("Time unit. 'seconds' for sub-minute precision. 'campaign' spans the full campaign flight.") }).passthrough().describe("A time duration expressed as an interval and unit. Used for frequency cap windows, attribution windows, reach optimization windows, time budgets, and other time-based settings. When unit is 'campaign', interval must be 1 — the window spans the full campaign flight.").optional(), "lift_dimension": z.enum(["awareness","consideration","favorability","purchase_intent","ad_recall"]).describe("Brand-lift dimension disambiguator. Brand lift is multidimensional in production — Kantar, Upwave, Cint, DoubleVerify, and similar vendors report awareness, consideration, favorability, purchase intent, and ad recall as separate measurements with their own sample sizes and confidence intervals. Used as a `qualifier.lift_dimension` key on `committed_metrics`, `missing_metrics`, `metric_aggregates`, and `performance-feedback.metric` to disambiguate which dimension of `brand_lift` a row represents. Two `brand_lift` rows under different lift dimensions represent different surveyed outcomes and must not be combined into a single number.").optional() }).passthrough().describe("Compact qualifier set that distinguishes incompatible reporting definitions for the same metric ID.").optional(), "effective_at": z.string().refine(adcpJsonSchemaDateTime, "Invalid date-time").optional() }).passthrough(), z.object({ "scope": z.literal("vendor"), "vendor": z.object({ "domain": z.string().regex(new RegExp("^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$")).describe("Domain that hosts /.well-known/brand.json or is registered for the brand."), "brand_id": z.string().regex(new RegExp("^[a-z0-9_]+$")).describe("Brand within a house-of-brands manifest. Omit for a single-brand domain.").optional(), "countries": z.array(z.string().regex(new RegExp("^[A-Z]{2}$"))).min(1).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Canonical set of ISO 3166-1 alpha-2 countries for this advertiser identity. Omit when the identity is global or the house does not split the brand geographically. Array order is not meaningful; producers MUST sort codes lexicographically before computing keys or signatures. This qualifies account and proposal identity and is not delivery targeting.").optional() }).passthrough().describe("Stable brand identity used to resolve the canonical brand manifest, optionally qualified by the countries of a commercial advertiser entity. The manifest URL is derived from domain; callers do not send brand assets or per-call brand overrides. Countries do not target delivery."), "metric_id": z.string().regex(new RegExp("^[a-z][a-z0-9_]*$")).min(1).max(64).describe("Identifier for a vendor-defined metric within the vendor's vocabulary. Stable lookup key; the vendor publishes the canonical list (with category, methodology, and standard alignment) in `brand.json` `agents[type='measurement']`. Lowercase with underscores so a future enum promotion into `available-metric.json` is a literal string lift. Identifier is namespaced by the vendor — the same `metric_id` may mean different things in different vendors' vocabularies."), "qualifier": z.object({ "viewability_standard": z.enum(["mrc","groupm"]).describe("Viewability measurement standard applied to determine whether an impression qualifies as viewable. These are materially different thresholds and should not be compared across standards.").optional(), "completion_source": z.enum(["seller_attested","vendor_attested"]).describe("Trust-source disambiguator for `completion_rate` — *who* attested to the completion event, not *how* (methodology granularity is a separate dimension; future qualifier keys may add it if buyer demand surfaces). The two paths can yield materially different rates, particularly in SSAI environments where the player's view of completion may differ from a vendor's. Used as a `qualifier.completion_source` key on `committed_metrics`, `missing_metrics`, and `metric_aggregates` to disambiguate which trust source the row represents. Edge cases: walled gardens where the seller is also the measurement vendor (YouTube, Spotify) collapse to `seller_attested` by trust-model logic — the same party served and counted. IAB-certified first-party podcast measurement (Podtrac, Triton on their own platforms; Art19 on its own platform) likewise collapses to `seller_attested`. The same vendor's offering on a third-party platform (Podtrac on a publisher who isn't Podtrac) is `vendor_attested`. The trust axis is *not* who runs the SDK — it's who is independent of the seller's revenue interest.").optional(), "attribution_methodology": z.enum(["deterministic_purchase","probabilistic","panel_based","modeled"]).describe("How attribution between ad exposure and outcome events was computed. Used as a `qualifier.attribution_methodology` key on `committed_metrics`, `missing_metrics`, `metric_aggregates`, and `performance-feedback.metric` to disambiguate the same outcome metric reported under different methodologies — `conversion_value` measured deterministically (matched purchase IDs) is not the same number as `conversion_value` measured probabilistically (modeled match) and should never be summed across methodologies. The retail-media closed-loop pattern typically reports under `deterministic_purchase`; MMM and clean-room outputs typically report under `modeled` or `probabilistic`; panel-based measurement (Nielsen, comScore, Edison) reports under `panel_based`.").optional(), "attribution_window": z.object({ "interval": z.number().int().gte(1).describe("Number of time units. Must be 1 when unit is 'campaign'."), "unit": z.enum(["seconds","minutes","hours","days","campaign"]).describe("Time unit. 'seconds' for sub-minute precision. 'campaign' spans the full campaign flight.") }).passthrough().describe("A time duration expressed as an interval and unit. Used for frequency cap windows, attribution windows, reach optimization windows, time budgets, and other time-based settings. When unit is 'campaign', interval must be 1 — the window spans the full campaign flight.").optional(), "lift_dimension": z.enum(["awareness","consideration","favorability","purchase_intent","ad_recall"]).describe("Brand-lift dimension disambiguator. Brand lift is multidimensional in production — Kantar, Upwave, Cint, DoubleVerify, and similar vendors report awareness, consideration, favorability, purchase intent, and ad recall as separate measurements with their own sample sizes and confidence intervals. Used as a `qualifier.lift_dimension` key on `committed_metrics`, `missing_metrics`, `metric_aggregates`, and `performance-feedback.metric` to disambiguate which dimension of `brand_lift` a row represents. Two `brand_lift` rows under different lift dimensions represent different surveyed outcomes and must not be combined into a single number.").optional() }).passthrough().describe("Compact qualifier set that distinguishes incompatible reporting definitions for the same metric ID.").optional(), "effective_at": z.string().refine(adcpJsonSchemaDateTime, "Invalid date-time").optional() }).passthrough()]; + const schemas = [z.object({ "scope": z.literal("standard"), "metric_id": z.enum(["impressions","spend","clicks","ctr","views","completed_views","completion_rate","conversions","conversion_value","commissionable_value","roas","cost_per_acquisition","new_to_brand_rate","leads","reach","frequency","grps","engagements","engagement_rate","follows","saves","profile_visits","viewability","viewable_rate","viewable_impressions","measurable_impressions","viewed_seconds","quartile_data","quartile_25","quartile_50","quartile_75","quartile_100","time_based_views","dooh_metrics","cost_per_click","cost_per_completed_view","cpm","downloads","units_sold","new_to_brand_units","plays","incremental_sales_lift","brand_lift","foot_traffic","conversion_lift","brand_search_lift"]).describe("Standard delivery and performance metrics available for reporting. Most values name flat numeric fields of `core/delivery-metrics.json`. Leaf identities address a single numeric value nested inside an object-shaped metric so it can be declared, committed, aggregated, and sorted individually: `quartile_25`/`quartile_50`/`quartile_75`/`quartile_100` resolve to `quartile_data.q1_views`–`q4_views`, and `viewable_rate`/`viewable_impressions`/`measurable_impressions`/`viewed_seconds` resolve to the same-named fields of `viewability`. The nested object remains the canonical carrier of the reported value — leaf identities never introduce duplicate flat response fields. The container tokens (`viewability`, `quartile_data`, `dooh_metrics`, and the `time_based_views` array) declare the whole nested surface at once. **Container subsumption**: in every set operation over this enum — capability declaration, `required_metrics` filtering, format `reported_metrics` intersection, and `requested_metrics` selection — a container token subsumes its leaf identities: declaring `viewability` satisfies a requirement or request for `viewable_rate`, and requesting a leaf identity selects its canonical carrier object in the response (never a flat duplicate). A leaf declaration does not imply sibling leaves or the carrier's non-numeric fields."), "qualifier": z.object({ "viewability_standard": z.enum(["mrc","groupm"]).describe("Viewability measurement standard applied to determine whether an impression qualifies as viewable. These are materially different thresholds and should not be compared across standards.").optional(), "completion_source": z.enum(["seller_attested","vendor_attested"]).describe("Trust-source disambiguator for `completion_rate` — *who* attested to the completion event, not *how* (methodology granularity is a separate dimension; future qualifier keys may add it if buyer demand surfaces). The two paths can yield materially different rates, particularly in SSAI environments where the player's view of completion may differ from a vendor's. Used as a `qualifier.completion_source` key on `committed_metrics`, `missing_metrics`, and `metric_aggregates` to disambiguate which trust source the row represents. Edge cases: walled gardens where the seller is also the measurement vendor (YouTube, Spotify) collapse to `seller_attested` by trust-model logic — the same party served and counted. IAB-certified first-party podcast measurement (Podtrac, Triton on their own platforms; Art19 on its own platform) likewise collapses to `seller_attested`. The same vendor's offering on a third-party platform (Podtrac on a publisher who isn't Podtrac) is `vendor_attested`. The trust axis is *not* who runs the SDK — it's who is independent of the seller's revenue interest.").optional(), "attribution_methodology": z.enum(["deterministic_purchase","probabilistic","panel_based","modeled"]).describe("How attribution between ad exposure and outcome events was computed. Used as a `qualifier.attribution_methodology` key on `committed_metrics`, `missing_metrics`, `metric_aggregates`, and `performance-feedback.metric` to disambiguate the same outcome metric reported under different methodologies — `conversion_value` measured deterministically (matched purchase IDs) is not the same number as `conversion_value` measured probabilistically (modeled match) and should never be summed across methodologies. The retail-media closed-loop pattern typically reports under `deterministic_purchase`; MMM and clean-room outputs typically report under `modeled` or `probabilistic`; panel-based measurement (Nielsen, comScore, Edison) reports under `panel_based`.").optional(), "attribution_window": z.object({ "interval": z.number().int().gte(1).describe("Number of time units. Must be 1 when unit is 'campaign'."), "unit": z.enum(["seconds","minutes","hours","days","campaign"]).describe("Time unit. 'seconds' for sub-minute precision. 'campaign' spans the full campaign flight.") }).passthrough().describe("A time duration expressed as an interval and unit. Used for frequency cap windows, attribution windows, reach optimization windows, time budgets, and other time-based settings. When unit is 'campaign', interval must be 1 — the window spans the full campaign flight.").optional(), "lift_dimension": z.enum(["awareness","consideration","favorability","purchase_intent","ad_recall"]).describe("Brand-lift dimension disambiguator. Brand lift is multidimensional in production — Kantar, Upwave, Cint, DoubleVerify, and similar vendors report awareness, consideration, favorability, purchase intent, and ad recall as separate measurements with their own sample sizes and confidence intervals. Used as a `qualifier.lift_dimension` key on `committed_metrics`, `missing_metrics`, `metric_aggregates`, and `performance-feedback.metric` to disambiguate which dimension of `brand_lift` a row represents. Two `brand_lift` rows under different lift dimensions represent different surveyed outcomes and must not be combined into a single number.").optional() }).passthrough().describe("Compact qualifier set that distinguishes incompatible reporting definitions for the same metric ID.").optional(), "effective_at": z.string().refine(adcpJsonSchemaDateTime, "Invalid date-time").optional() }).passthrough(), z.object({ "scope": z.literal("vendor"), "vendor": z.object({ "domain": z.string().regex(new RegExp("^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$")).describe("Domain that hosts /.well-known/brand.json or is registered for the brand."), "brand_id": z.string().regex(new RegExp("^[a-z0-9_]+$")).describe("Brand within a house-of-brands manifest. Omit for a single-brand domain.").optional(), "countries": z.array(z.string().regex(new RegExp("^[A-Z]{2}$"))).min(1).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Canonical set of ISO 3166-1 alpha-2 countries for this advertiser identity. Omit when the identity is global or the house does not split the brand geographically. Array order is not meaningful; producers MUST sort codes lexicographically before computing keys or signatures. This qualifies account and proposal identity and is not delivery targeting.").optional() }).passthrough().describe("Stable brand identity used to resolve the canonical brand manifest, optionally qualified by the countries of a commercial advertiser entity. The manifest URL is derived from domain; callers do not send brand assets or per-call brand overrides. Countries do not target delivery."), "metric_id": z.string().regex(new RegExp("^[a-z][a-z0-9_]*$")).min(1).max(64).describe("Identifier for a vendor-defined metric within the vendor's vocabulary. Stable lookup key; the vendor publishes the canonical list (with category, methodology, and standard alignment) in `brand.json` `agents[type='measurement']`. Lowercase with underscores so a future enum promotion into `available-metric.json` is a literal string lift. Identifier is namespaced by the vendor — the same `metric_id` may mean different things in different vendors' vocabularies."), "qualifier": z.object({ "viewability_standard": z.enum(["mrc","groupm"]).describe("Viewability measurement standard applied to determine whether an impression qualifies as viewable. These are materially different thresholds and should not be compared across standards.").optional(), "completion_source": z.enum(["seller_attested","vendor_attested"]).describe("Trust-source disambiguator for `completion_rate` — *who* attested to the completion event, not *how* (methodology granularity is a separate dimension; future qualifier keys may add it if buyer demand surfaces). The two paths can yield materially different rates, particularly in SSAI environments where the player's view of completion may differ from a vendor's. Used as a `qualifier.completion_source` key on `committed_metrics`, `missing_metrics`, and `metric_aggregates` to disambiguate which trust source the row represents. Edge cases: walled gardens where the seller is also the measurement vendor (YouTube, Spotify) collapse to `seller_attested` by trust-model logic — the same party served and counted. IAB-certified first-party podcast measurement (Podtrac, Triton on their own platforms; Art19 on its own platform) likewise collapses to `seller_attested`. The same vendor's offering on a third-party platform (Podtrac on a publisher who isn't Podtrac) is `vendor_attested`. The trust axis is *not* who runs the SDK — it's who is independent of the seller's revenue interest.").optional(), "attribution_methodology": z.enum(["deterministic_purchase","probabilistic","panel_based","modeled"]).describe("How attribution between ad exposure and outcome events was computed. Used as a `qualifier.attribution_methodology` key on `committed_metrics`, `missing_metrics`, `metric_aggregates`, and `performance-feedback.metric` to disambiguate the same outcome metric reported under different methodologies — `conversion_value` measured deterministically (matched purchase IDs) is not the same number as `conversion_value` measured probabilistically (modeled match) and should never be summed across methodologies. The retail-media closed-loop pattern typically reports under `deterministic_purchase`; MMM and clean-room outputs typically report under `modeled` or `probabilistic`; panel-based measurement (Nielsen, comScore, Edison) reports under `panel_based`.").optional(), "attribution_window": z.object({ "interval": z.number().int().gte(1).describe("Number of time units. Must be 1 when unit is 'campaign'."), "unit": z.enum(["seconds","minutes","hours","days","campaign"]).describe("Time unit. 'seconds' for sub-minute precision. 'campaign' spans the full campaign flight.") }).passthrough().describe("A time duration expressed as an interval and unit. Used for frequency cap windows, attribution windows, reach optimization windows, time budgets, and other time-based settings. When unit is 'campaign', interval must be 1 — the window spans the full campaign flight.").optional(), "lift_dimension": z.enum(["awareness","consideration","favorability","purchase_intent","ad_recall"]).describe("Brand-lift dimension disambiguator. Brand lift is multidimensional in production — Kantar, Upwave, Cint, DoubleVerify, and similar vendors report awareness, consideration, favorability, purchase intent, and ad recall as separate measurements with their own sample sizes and confidence intervals. Used as a `qualifier.lift_dimension` key on `committed_metrics`, `missing_metrics`, `metric_aggregates`, and `performance-feedback.metric` to disambiguate which dimension of `brand_lift` a row represents. Two `brand_lift` rows under different lift dimensions represent different surveyed outcomes and must not be combined into a single number.").optional() }).passthrough().describe("Compact qualifier set that distinguishes incompatible reporting definitions for the same metric ID.").optional(), "effective_at": z.string().refine(adcpJsonSchemaDateTime, "Invalid date-time").optional() }).passthrough()]; const { errors, failed } = schemas.reduce<{ errors: z.core.$ZodIssue[]; failed: number; @@ -15436,7 +15672,8 @@ export const ControlMediaBuyRequestSchema = z.object({ ext: ExtensionObjectSchema.optional() }).passthrough(); -export const ListCreativeFormatsResponseSchema = z.object({ +// @ts-ignore -- preserve the public schema type across lossy TS-to-Zod projection details. +export const ListCreativeFormatsResponseSchema: z.ZodObject<{ [K in keyof ListCreativeFormatsResponse]-?: z.ZodType }, any> & z.ZodType, ListCreativeFormatsResponse & Record> = z.object({ context_id: z.string().optional(), context: ContextObjectSchema.optional(), task_id: z.string().optional(), @@ -15500,7 +15737,14 @@ export const PackageRequestSchema: z.ZodType = AdCPVersionEnvelopeSchema.merge(z }).passthrough(), z.object({ scope: z.literal("vendor"), vendor: BrandReferenceSchema, - metric_id: VendorMetricIDSchema + metric_id: VendorMetricIDSchema, + qualifier: z.object({ + viewability_standard: ViewabilityStandardSchema.optional(), + completion_source: CompletionSourceSchema.optional(), + attribution_methodology: AttributionMethodologySchema.optional(), + attribution_window: DurationSchema.optional(), + lift_dimension: LiftDimensionSchema.optional() + }).passthrough().optional() }).passthrough()])]).rest(z.union([z.object({ scope: z.literal("standard"), metric_id: AvailableMetricSchema, @@ -15514,7 +15758,14 @@ export const PackageRequestSchema: z.ZodType = AdCPVersionEnvelopeSchema.merge(z }).passthrough(), z.object({ scope: z.literal("vendor"), vendor: BrandReferenceSchema, - metric_id: VendorMetricIDSchema + metric_id: VendorMetricIDSchema, + qualifier: z.object({ + viewability_standard: ViewabilityStandardSchema.optional(), + completion_source: CompletionSourceSchema.optional(), + attribution_methodology: AttributionMethodologySchema.optional(), + attribution_window: DurationSchema.optional(), + lift_dimension: LiftDimensionSchema.optional() + }).passthrough().optional() }).passthrough()])).optional(), creative_assignments: z.array(CreativeAssignmentSchema).optional(), creatives: z.array(CreativeAssetSchema.and(z.object({}).passthrough())).optional(), @@ -15620,7 +15871,8 @@ export const UpdateMediaBuySuccessSchema: z.ZodType = z.object({ ext: ExtensionObjectSchema.optional() }).passthrough(); -export const PackageStatusSchema = z.object({ +// @ts-ignore -- preserve the public schema type across lossy TS-to-Zod projection details. +export const PackageStatusSchema: z.ZodObject<{ [K in keyof PackageStatus]-?: z.ZodType }, any> & z.ZodType, PackageStatus & Record> = z.object({ package_id: z.string(), product_id: z.string().optional(), budget: z.number().min(0).optional(), @@ -15792,25 +16044,52 @@ export const GetMediaBuyDeliveryResponseSchema = z.object({ supersedes_window: z.string().max(50).optional(), missing_metrics: z.array(MissingMetricSchema).optional(), by_catalog_item: z.array(GetMediaBuyDeliveryCatalogItemMetricsSchema).optional(), + by_catalog_item_truncated: z.boolean().optional(), + by_catalog_item_sorted_by: SortMetricSchema.optional(), + by_catalog_item_sort_direction: SortDirectionSchema.optional(), by_creative: z.array(CreativeDeliveryMetricsSchema).optional(), + by_format: z.array(DeliveryMetricsSchema.merge(z.object({ + format_kind: CanonicalFormatKindSchema + }).passthrough())).optional(), + by_format_truncated: z.boolean().optional(), + by_format_sorted_by: SortMetricSchema.optional(), + by_format_sort_direction: SortDirectionSchema.optional(), + by_creative_truncated: z.boolean().optional(), + by_creative_sorted_by: SortMetricSchema.optional(), + by_creative_sort_direction: SortDirectionSchema.optional(), by_keyword: z.array(GetMediaBuyDeliveryKeywordMetricsSchema).optional(), + by_keyword_truncated: z.boolean().optional(), + by_keyword_sorted_by: SortMetricSchema.optional(), + by_keyword_sort_direction: SortDirectionSchema.optional(), by_geo: z.array(GetMediaBuyDeliveryGeoMetricsSchema).optional(), by_geo_truncated: z.boolean().optional(), + by_geo_sorted_by: SortMetricSchema.optional(), + by_geo_sort_direction: SortDirectionSchema.optional(), by_device_type: z.array(GetMediaBuyDeliveryDeviceTypeMetricsSchema).optional(), by_device_type_truncated: z.boolean().optional(), + by_device_type_sorted_by: SortMetricSchema.optional(), + by_device_type_sort_direction: SortDirectionSchema.optional(), by_device_platform: z.array(GetMediaBuyDeliveryDevicePlatformMetricsSchema).optional(), by_device_platform_truncated: z.boolean().optional(), + by_device_platform_sorted_by: SortMetricSchema.optional(), + by_device_platform_sort_direction: SortDirectionSchema.optional(), by_audience: z.array(GetMediaBuyDeliveryAudienceMetricsSchema).optional(), by_audience_truncated: z.boolean().optional(), + by_audience_sorted_by: SortMetricSchema.optional(), + by_audience_sort_direction: SortDirectionSchema.optional(), by_demographic: z.array(DeliveryMetricsSchema.merge(z.object({ demographic: z.string().min(1).regex(/\S/), demographic_system: DemographicSystemSchema, age: DemographicAgeRangeSchema.optional() }).passthrough())).optional(), by_demographic_truncated: z.boolean().optional(), + by_demographic_sorted_by: SortMetricSchema.optional(), + by_demographic_sort_direction: SortDirectionSchema.optional(), by_demographic_suppressed: z.boolean().optional(), by_placement: z.array(GetMediaBuyDeliveryPlacementMetricsSchema).optional(), by_placement_truncated: z.boolean().optional(), + by_placement_sorted_by: SortMetricSchema.optional(), + by_placement_sort_direction: SortDirectionSchema.optional(), by_spot: z.array(DeliveryMetricsSchema.merge(z.object({ spot_id: z.string().min(1), aired_at: z.iso.datetime(), @@ -16060,6 +16339,7 @@ export const ListedCreativeCanonicalFormatKindSchema: z.ZodType = z.object({ created_date: z.iso.datetime(), updated_date: z.iso.datetime(), assets: z.record(z.string(), z.union([AssetVariantSchema, z.array(AssetVariantSchema)])).optional(), + component_assets: z.record(z.string(), CreativeAssetsSchema).optional(), localization: CreativeLocalizationReadbackSchema.optional(), localization_unavailable: z.object({ errors: z.array(ErrorSchema), @@ -16334,7 +16614,8 @@ export const CheckGovernanceRequestSchema = z.object({}).passthrough().merge(z.o ext: ExtensionObjectSchema.optional() }).passthrough()); -export const GetAdCPCapabilitiesResponseSchema = z.object({ +// @ts-ignore -- preserve the public schema type across lossy TS-to-Zod projection details. +export const GetAdCPCapabilitiesResponseSchema: z.ZodObject<{ [K in keyof GetAdCPCapabilitiesResponse]-?: z.ZodType }, any> & z.ZodType, GetAdCPCapabilitiesResponse & Record> = z.object({ context_id: z.string().optional(), context: ContextObjectSchema.optional(), task_id: z.string().optional(), @@ -17769,6 +18050,7 @@ export const ListedCreativeNamedFormatReferenceSchema: z.ZodType = z.object({ created_date: z.iso.datetime(), updated_date: z.iso.datetime(), assets: z.record(z.string(), z.union([AssetVariantSchema, z.array(AssetVariantSchema)])).optional(), + component_assets: z.record(z.string(), CreativeAssetsSchema).optional(), localization: CreativeLocalizationReadbackSchema.optional(), localization_unavailable: z.object({ errors: z.array(ErrorSchema), diff --git a/src/lib/types/tools.generated.ts b/src/lib/types/tools.generated.ts index b4178e884..235bf91ec 100644 --- a/src/lib/types/tools.generated.ts +++ b/src/lib/types/tools.generated.ts @@ -37,10 +37,12 @@ import type { BusinessEntity, C2PAWatermarkAction, CSSAssetRequirements, + CTVAdExperience, CanceledBy, CancellationPolicy, CanonicalFormatAgentPlacementAISurfaceSponsoredPlacement, CanonicalFormatBase, + CanonicalFormatCoordinatedPlacements, CanonicalFormatDAASTAudio, CanonicalFormatDisplayTag, CanonicalFormatHTML5Banner, @@ -49,7 +51,9 @@ import type { CanonicalFormatImage, CanonicalFormatImageCarousel, CanonicalFormatNativeInFeed, + CanonicalFormatOption, CanonicalFormatResponsiveCreative, + CanonicalFormatSellerRenderedStatefulDisplay, CanonicalFormatSponsoredPlacementRetailMediaCatalogDriven, CanonicalFormatVASTVideo, CanonicalMediaBuyActionMode, @@ -71,12 +75,14 @@ import type { ContentRatingSystem, CountryFusedPostalCodeSystem, CreativeAction, + CreativeActivationMethod, CreativeAgentCapability, CreativeApprovalStatus, CreativeAsset, CreativeBrief, CreativeEventReasonCode, CreativeIdentifierType, + CreativeMotionLevel, CreativeQuality, CreativeSelectionStrategy, CreativeSortField, @@ -233,6 +239,7 @@ import type { ValidationMode, VideoAssetRequirements, VideoPlacementType, + ViewThresholdBasis, ViewabilityStandard, WCAGLevel, WarningAffectedResource, @@ -243,7 +250,7 @@ import type { WebhookSecurityMethod, } from './core.generated'; -export type { AccountCurrencyMode, AccountReference, AccountScope, AccountStatus, ActionNotAllowedReason, ActionSource, AdCPProtocol, AdCPSpecialism, AdCPVersionEnvelope, AdvertiserIndustry, AgeDeterminationBasis, AgeVerificationMethod, AssessmentStatus, AssetContentType, AssetVariant, AttestationClaim, AttributionMethodology, AttributionModel, AudienceConstraints, AudienceEvidenceMethodology, AudienceResolutionMethod, AudienceSource, AudienceStatus, AudienceSubjectType, AudioChannelLayout, AudioDistributionType, AuthenticationScheme, AvailabilityStatus, AvailableMetric, BillingParty, BinaryVerdict, BrandAgentType, BrandReference, BrowserFamily, BusinessEntity, C2PAWatermarkAction, CanceledBy, CancellationPolicy, CanonicalFormatAgentPlacementAISurfaceSponsoredPlacement, CanonicalFormatBase, CanonicalFormatDAASTAudio, CanonicalFormatDisplayTag, CanonicalFormatHTML5Banner, CanonicalFormatHostedAudio, CanonicalFormatHostedVideo, CanonicalFormatImage, CanonicalFormatImageCarousel, CanonicalFormatNativeInFeed, CanonicalFormatResponsiveCreative, CanonicalFormatSponsoredPlacementRetailMediaCatalogDriven, CanonicalFormatVASTVideo, CanonicalMediaBuyActionMode, CanonicalMediaBuyActionName, CanonicalProposal, CatalogAction, CatalogItemDeliveryMetrics, CatalogItemStatus, CatalogType, CloudStorageProtocol, CoBrandingRequirement, CollectionCadence, CollectionKind, CollectionRelationship, CollectionStatus, CompletionSource, ConsentBasis, ContentIDType, ContentRatingSystem, CountryFusedPostalCodeSystem, CreativeAction, CreativeAgentCapability, CreativeApprovalStatus, CreativeAsset, CreativeBrief, CreativeEventReasonCode, CreativeIdentifierType, CreativeQuality, CreativeSelectionStrategy, CreativeSortField, CreativeStatus, DAASTTrackingEvent, DAASTVersion, DayOfWeek, DelegationAuthority, DeliveryMetricAggregate, DeliveryMetrics, DeliveryStatus, DeliveryType, DemographicSystem, DerivativeType, DevicePlatform, DeviceType, DigitalSourceType, DimensionUnit, DisclosurePersistence, DisclosurePosition, DistanceUnit, DistributionIdentifierType, EmbeddedProvenanceMethod, ErrorCode, ErrorScope, EscalationSeverity, EventType, Exclusivity, ExtensionObject, FeatureCheckStatus, FeedFormat, FeedbackSource, Fixed, ForecastMethod, ForecastPoint, ForecastRangeUnit, ForecastableMetric, Format, FormatIDParameter, FormatReferenceStructuredObject, FrameRateType, FrequencyCapScope, GOPType, GenreTaxonomy, GeoDeliveryMetrics, GeographicTargetingLevel, GetProductsAsyncSubmitted, GovernanceDecision, GovernanceDomain, GovernanceMode, GovernancePhase, HTTPMethod, HistoryEntryType, ImageAsset, ImpairmentOfflineState, ImpairmentReasonCode, IndicatorType, InstallmentStatus, JavaScriptModuleType, KeywordDeliveryMetrics, LandingPageRequirement, LiftDimension, LogoSlot, MakegoodRemedy, MarkdownFlavor, MatchIDType, MatchType, MeasurementTerms, MediaBuyActionMode, MediaBuyHealth, MediaBuyStatus, MediaBuyValidAction, MediaChannel, MetricScope, MetricType, MetroAreaSystem, MoovAtomPosition, MultiSize, None, NotificationType, OfferingAvailabilityStatus, OutcomeType, Pacing, PackageUpdate, PaymentTerms, PerformanceBaseline, PerformanceStandardMetric, PlatformExtensionReference, PolicyCategory, PolicyEnforcementLevel, PostalCodeSystem, PostalCountrySystem, PreviewOutputFormat, PriceAdjustmentKind, PricingModel, PricingStructure, ProductionQuality, PropertyIdentifierTypes, PropertyType, ProposalDeclineReason, ProposalRefinementReason, ProposalStatus, ProtocolEnvelope, Provenance, PublisherIdentifierTypes, PublisherPropertySelector, PurchaseType, ReachUnit, ReportingFrequency, RequestProposalsResponse, Responsive, RestrictedAttribute, RightType, RightUse, RightsBillingPeriod, RightsConstraint, SISessionStatus, ScanType, ScopedCreativeApproval, SignalAvailabilityType, SignalDefinitionEnrichment, SignalSource, SignalTargetingExpression, SignalValueType, SizeModeMutex, SnapshotUnavailableReason, SocialPlacementSurface, SortDirection, SortMetric, SpecialCategory, SponsoredPlacementType, TMPResponseType, TalentRole, TargetingOverlayRequirements, TargetingOverlaySupport, TaskStatus, TaskType, TransportMode, TravelTimeUnit, UIDType, URLAssetType, UniversalMacro, UpdateFrequency, VASTTrackingEvent, VASTVersion, ValidationMode, VideoPlacementType, ViewabilityStandard, WCAGLevel, WarningAffectedResource, WarningCode, WatermarkMediaType, WebhookResponseType, WebhookSecurityMethod } from './core.generated'; +export type { AccountCurrencyMode, AccountReference, AccountScope, AccountStatus, ActionNotAllowedReason, ActionSource, AdCPProtocol, AdCPSpecialism, AdCPVersionEnvelope, AdvertiserIndustry, AgeDeterminationBasis, AgeVerificationMethod, AssessmentStatus, AssetContentType, AssetVariant, AttestationClaim, AttributionMethodology, AttributionModel, AudienceConstraints, AudienceEvidenceMethodology, AudienceResolutionMethod, AudienceSource, AudienceStatus, AudienceSubjectType, AudioChannelLayout, AudioDistributionType, AuthenticationScheme, AvailabilityStatus, AvailableMetric, BillingParty, BinaryVerdict, BrandAgentType, BrandReference, BrowserFamily, BusinessEntity, C2PAWatermarkAction, CTVAdExperience, CanceledBy, CancellationPolicy, CanonicalFormatAgentPlacementAISurfaceSponsoredPlacement, CanonicalFormatBase, CanonicalFormatCoordinatedPlacements, CanonicalFormatDAASTAudio, CanonicalFormatDisplayTag, CanonicalFormatHTML5Banner, CanonicalFormatHostedAudio, CanonicalFormatHostedVideo, CanonicalFormatImage, CanonicalFormatImageCarousel, CanonicalFormatNativeInFeed, CanonicalFormatOption, CanonicalFormatResponsiveCreative, CanonicalFormatSellerRenderedStatefulDisplay, CanonicalFormatSponsoredPlacementRetailMediaCatalogDriven, CanonicalFormatVASTVideo, CanonicalMediaBuyActionMode, CanonicalMediaBuyActionName, CanonicalProposal, CatalogAction, CatalogItemDeliveryMetrics, CatalogItemStatus, CatalogType, CloudStorageProtocol, CoBrandingRequirement, CollectionCadence, CollectionKind, CollectionRelationship, CollectionStatus, CompletionSource, ConsentBasis, ContentIDType, ContentRatingSystem, CountryFusedPostalCodeSystem, CreativeAction, CreativeActivationMethod, CreativeAgentCapability, CreativeApprovalStatus, CreativeAsset, CreativeBrief, CreativeEventReasonCode, CreativeIdentifierType, CreativeMotionLevel, CreativeQuality, CreativeSelectionStrategy, CreativeSortField, CreativeStatus, DAASTTrackingEvent, DAASTVersion, DayOfWeek, DelegationAuthority, DeliveryMetricAggregate, DeliveryMetrics, DeliveryStatus, DeliveryType, DemographicSystem, DerivativeType, DevicePlatform, DeviceType, DigitalSourceType, DimensionUnit, DisclosurePersistence, DisclosurePosition, DistanceUnit, DistributionIdentifierType, EmbeddedProvenanceMethod, ErrorCode, ErrorScope, EscalationSeverity, EventType, Exclusivity, ExtensionObject, FeatureCheckStatus, FeedFormat, FeedbackSource, Fixed, ForecastMethod, ForecastPoint, ForecastRangeUnit, ForecastableMetric, Format, FormatIDParameter, FormatReferenceStructuredObject, FrameRateType, FrequencyCapScope, GOPType, GenreTaxonomy, GeoDeliveryMetrics, GeographicTargetingLevel, GetProductsAsyncSubmitted, GovernanceDecision, GovernanceDomain, GovernanceMode, GovernancePhase, HTTPMethod, HistoryEntryType, ImageAsset, ImpairmentOfflineState, ImpairmentReasonCode, IndicatorType, InstallmentStatus, JavaScriptModuleType, KeywordDeliveryMetrics, LandingPageRequirement, LiftDimension, LogoSlot, MakegoodRemedy, MarkdownFlavor, MatchIDType, MatchType, MeasurementTerms, MediaBuyActionMode, MediaBuyHealth, MediaBuyStatus, MediaBuyValidAction, MediaChannel, MetricScope, MetricType, MetroAreaSystem, MoovAtomPosition, MultiSize, None, NotificationType, OfferingAvailabilityStatus, OutcomeType, Pacing, PackageUpdate, PaymentTerms, PerformanceBaseline, PerformanceStandardMetric, PlatformExtensionReference, PolicyCategory, PolicyEnforcementLevel, PostalCodeSystem, PostalCountrySystem, PreviewOutputFormat, PriceAdjustmentKind, PricingModel, PricingStructure, ProductionQuality, PropertyIdentifierTypes, PropertyType, ProposalDeclineReason, ProposalRefinementReason, ProposalStatus, ProtocolEnvelope, Provenance, PublisherIdentifierTypes, PublisherPropertySelector, PurchaseType, ReachUnit, ReportingFrequency, RequestProposalsResponse, Responsive, RestrictedAttribute, RightType, RightUse, RightsBillingPeriod, RightsConstraint, SISessionStatus, ScanType, ScopedCreativeApproval, SignalAvailabilityType, SignalDefinitionEnrichment, SignalSource, SignalTargetingExpression, SignalValueType, SizeModeMutex, SnapshotUnavailableReason, SocialPlacementSurface, SortDirection, SortMetric, SpecialCategory, SponsoredPlacementType, TMPResponseType, TalentRole, TargetingOverlayRequirements, TargetingOverlaySupport, TaskStatus, TaskType, TransportMode, TravelTimeUnit, UIDType, URLAssetType, UniversalMacro, UpdateFrequency, VASTTrackingEvent, VASTVersion, ValidationMode, VideoPlacementType, ViewThresholdBasis, ViewabilityStandard, WCAGLevel, WarningAffectedResource, WarningCode, WatermarkMediaType, WebhookResponseType, WebhookSecurityMethod } from './core.generated'; // Tool Parameter and Response Types // Generated from official AdCP schemas @@ -348,13 +355,7 @@ export type CatalogFieldMapping = { ext?: ExtensionObject; }; /** - * Discriminator value naming one of the 12 canonical creative formats — plus `custom` for adopter-defined shapes that don't fit the canonicals (multi-placement takeover, roadblock, branded content, cross-screen sponsorship, AR lens, etc.). Used by `product-format-declaration.json` (the product's inline format declaration), `creative-manifest.json` (the buyer's v2 manifest path), and any other surface that needs to identify which canonical a payload targets. - * - * When `format_kind: "custom"`, the declaration MUST also carry `format_shape` (referencing the [format-shape vocabulary registry](/schemas/core/format-shape-vocabulary.json) — recognized global pattern this custom shape is an instance of) and `format_schema` (URI+digest reference to a fetchable schema describing the shape's actual `params` and `slots`). Buyer agents fetch the schema, validate manifests structurally, and reason about manifests without per-seller integration code — same mechanic as `platform_extensions`. See [adcp#3666](https://github.com/adcontextprotocol/adcp/issues/3666) for the canonical promotion queue. - * - * The canonical enum mirrors the `oneOf` branches in `product-format-declaration.json`; keep them in sync. - * - * **Open-enum semantics (normative).** The enum list above is the AdCP 3.1 baseline. New canonical formats promoted from `format_shape` entries (see [#3666](https://github.com/adcontextprotocol/adcp/issues/3666)) extend this enum in subsequent minor releases — adding a value is non-breaking by design. Consumer SDKs MUST treat this enum as **open** at parse time: an unknown `format_kind` value MUST be retained as-is on the in-memory object (not silently dropped or rewritten to `"custom"`) and MUST NOT cause the surrounding payload to fail validation. Buyers reading a declaration with an unknown `format_kind` SHOULD treat the declaration as a local SDK-only declared/unsupported route for routing purposes (filter out of default product views; opt-in to surface) so adopters on older SDK versions don't crash on newer canonical values, and adopters on newer SDK versions get a graceful unknown-value handling story. The producer-side enum stays closed (sellers MUST NOT mint ad-hoc `format_kind` values — use `format_kind: "custom"` with `format_shape` + `format_schema` for non-registered shapes); the consumer-side enum stays open for forward compatibility. + * Discriminator naming one of the 14 AdCP 3.2 canonical creative formats, plus `custom` for adopter-defined shapes. AdCP 3.2 promotes the legacy `multi_state_display` and `multi_placement_takeover` custom shapes to `seller_rendered_stateful_display` and `coordinated_placements`. When `format_kind` is `custom`, the declaration also carries `format_shape` and `format_schema`. The enum mirrors the branches in `product-format-declaration.json` and is closed for producer validation: sellers do not mint ad-hoc values. SDKs may retain unknown future values in tolerant in-memory parsers, but that does not change versioned JSON Schema validation. A seller responding to a 3.1-or-earlier request MUST project away 3.2-only kinds and emit the compatible legacy custom declaration; a strict older schema correctly rejects an unprojected newer kind. See adcp#3666 and the promotion migration contract in canonical-formats.mdx. */ export type CanonicalFormatKind = | 'image' @@ -369,6 +370,8 @@ export type CanonicalFormatKind = | 'native_in_feed' | 'responsive_creative' | 'agent_placement' + | 'seller_rendered_stateful_display' + | 'coordinated_placements' | 'custom'; /** * Discriminated reference to a product format option. The global canonical shape is still named by `format_kind`; this reference selects one concrete product `format_options[]` entry. `scope: "publisher"` identifies a publisher-declared catalog option by `{ publisher_domain, format_option_id }`. `scope: "product"` identifies a product-local option by `format_option_id`; the enclosing package/product context supplies the namespace. @@ -1587,7 +1590,7 @@ export interface ProductFilters { */ required_performance_standards?: [PerformanceStandard, ...PerformanceStandard[]]; /** - * Filter to products whose `reporting_capabilities.available_metrics` is a superset of these metrics — i.e., products that commit to reporting all listed metrics in delivery responses. Use this for capability-level discovery (e.g., 'I need products that report `completed_views` for a CTV CPCV buy'); guarantee-level requirements with thresholds belong in `required_performance_standards` and `measurement_terms`. Sellers MUST silently exclude products that cannot meet this list (filter-not-fail; do not return an error). The product's declared `available_metrics` becomes the binding reporting contract carried into the resulting media buy — the same metric vocabulary is used to compute `missing_metrics` on `get_media_buy_delivery`. + * Filter to products whose `reporting_capabilities.available_metrics` is a superset of these metrics — i.e., products that commit to reporting all listed metrics in delivery responses. Use this for capability-level discovery (e.g., 'I need products that report `completed_views` for a CTV CPCV buy'); guarantee-level requirements with thresholds belong in `required_performance_standards` and `measurement_terms`. Sellers MUST silently exclude products that cannot meet this list (filter-not-fail; do not return an error). Superset evaluation follows the container-subsumption rule in `enums/available-metric.json` — a product declaring the `viewability` container satisfies a requirement for the `viewable_rate` leaf. The product's declared `available_metrics` becomes the binding reporting contract carried into the resulting media buy — the same metric vocabulary is used to compute `missing_metrics` on `get_media_buy_delivery`. * * @minItems 1 */ @@ -2609,7 +2612,7 @@ export type PropertyID = string; * * **Format matching vs satisfaction (normative).** Legacy named formats MUST be normalized to canonical declarations before comparison; do not exact-match raw `(agent_url, id)` pairs once a `format_id` has been projected through `canonical`, `v1_format_ref`, or the canonical mapping registry. Equivalence matching can treat a legacy fixed-size display ID and `format_kind: "image"` with matching `width`/`height` as the same underlying shape. Product satisfaction is stricter and directional: when this declaration specifies fixed constraints such as `width`, `height`, `duration_ms_exact`, or `duration_ms_range`, a buyer request or creative manifest MUST declare and satisfy those constraints. A broad request with no dimensions or duration does not satisfy a fixed-size or fixed-duration product; a broad product MAY accept a more specific creative unless another product constraint excludes it. Duration precedence is `duration_ms_exact` > `duration_ms_range`. Range constraints use containment: a range-based request satisfies this declaration only when every value it permits falls within this declaration's accepted range; overlap alone is insufficient. An exact value satisfies a range when the exact value falls inside the accepted interval. For hosted audio/video, a null range endpoint is unbounded: [null, 60000] means up to 60s, and [15000, null] means at least 15s; [null, null] is invalid because at least one endpoint must be bounded. * - * **Custom format_kind** (`format_kind: "custom"`): for adopter-defined shapes that don't fit the 12 canonicals (multi-placement takeover, roadblock, branded content, cross-screen sponsorship, sponsorship lockup, newsletter sponsorship, AR lens, playable, live event sponsorship). When `format_kind` is `custom`, the declaration MUST carry `format_shape` (recognized global pattern from the [format-shape vocabulary registry](/schemas/core/format-shape-vocabulary.json)) AND `format_schema` (URI+digest reference to a fetchable schema describing the actual `params` and `slots`). Buyer agents fetch the schema, validate manifests structurally, and reason about manifests without per-seller integration code. See [adcp#3666](https://github.com/adcontextprotocol/adcp/issues/3666) for the canonical promotion queue. + * **Custom format_kind** (`format_kind: "custom"`): for adopter-defined shapes that don't fit the 14 canonicals (branded content, cross-screen sponsorship, sponsorship lockup, newsletter sponsorship, AR lens, playable, live event sponsorship). When `format_kind` is `custom`, the declaration MUST carry `format_shape` (recognized global pattern from the [format-shape vocabulary registry](/schemas/core/format-shape-vocabulary.json)) AND `format_schema` (URI+digest reference to a fetchable schema describing the actual `params` and `slots`). Buyer agents fetch the schema, validate manifests structurally, and reason about manifests without per-seller integration code. See [adcp#3666](https://github.com/adcontextprotocol/adcp/issues/3666) for the canonical promotion queue. */ export type ProductFormatDeclaration = { } & { @@ -2653,7 +2656,7 @@ export type ProductFormatDeclaration = { */ experimental?: boolean; /** - * REQUIRED when `format_kind: "custom"`; otherwise MUST be absent. Recognized global pattern this custom shape is an instance of, drawn from the [format-shape vocabulary registry](/schemas/core/format-shape-vocabulary.json) (`multi_placement_takeover`, `roadblock`, `branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values valid (validators MAY soft-warn) — adopters CAN ship a shape that isn't yet in the registry. Adding entries is a vocabulary PR. Once a `format_shape` entry sees 2+ adopters with substantively similar `format_schema` content for 90+ days, the working group promotes it to a first-class canonical. + * REQUIRED when `format_kind: "custom"`; otherwise MUST be absent. Recognized global pattern this custom shape is an instance of, drawn from the [format-shape vocabulary registry](/schemas/core/format-shape-vocabulary.json) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are valid (validators MAY soft-warn) — adopters CAN ship a shape that isn't yet in the registry. Adding entries is a vocabulary PR. Once a `format_shape` entry sees 2+ adopters with substantively similar `format_schema` content for 90+ days, the working group may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format. */ format_shape?: string; /** @@ -2694,6 +2697,8 @@ export type ProductFormatDeclaration = { | NativeInFeedFormatDeclaration | ResponsiveCreativeFormatDeclaration | AgentPlacementFormatDeclaration + | SellerRenderedStatefulDisplayFormatDeclaration + | CoordinatedPlacementsFormatDeclaration | CustomFormatDeclaration ) & { /** @@ -2732,7 +2737,7 @@ export type ProductFormatDeclaration = { */ experimental?: boolean; /** - * REQUIRED when `format_kind: "custom"`; otherwise MUST be absent. Recognized global pattern this custom shape is an instance of, drawn from the [format-shape vocabulary registry](/schemas/core/format-shape-vocabulary.json) (`multi_placement_takeover`, `roadblock`, `branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values valid (validators MAY soft-warn) — adopters CAN ship a shape that isn't yet in the registry. Adding entries is a vocabulary PR. Once a `format_shape` entry sees 2+ adopters with substantively similar `format_schema` content for 90+ days, the working group promotes it to a first-class canonical. + * REQUIRED when `format_kind: "custom"`; otherwise MUST be absent. Recognized global pattern this custom shape is an instance of, drawn from the [format-shape vocabulary registry](/schemas/core/format-shape-vocabulary.json) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are valid (validators MAY soft-warn) — adopters CAN ship a shape that isn't yet in the registry. Adding entries is a vocabulary PR. Once a `format_shape` entry sees 2+ adopters with substantively similar `format_schema` content for 90+ days, the working group may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format. */ format_shape?: string; /** @@ -2773,6 +2778,8 @@ export type ProductFormatDeclaration = { | NativeInFeedFormatDeclaration | ResponsiveCreativeFormatDeclaration | AgentPlacementFormatDeclaration + | SellerRenderedStatefulDisplayFormatDeclaration + | CoordinatedPlacementsFormatDeclaration | CustomFormatDeclaration ) & { /** @@ -2811,7 +2818,7 @@ export type ProductFormatDeclaration = { */ experimental?: boolean; /** - * REQUIRED when `format_kind: "custom"`; otherwise MUST be absent. Recognized global pattern this custom shape is an instance of, drawn from the [format-shape vocabulary registry](/schemas/core/format-shape-vocabulary.json) (`multi_placement_takeover`, `roadblock`, `branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values valid (validators MAY soft-warn) — adopters CAN ship a shape that isn't yet in the registry. Adding entries is a vocabulary PR. Once a `format_shape` entry sees 2+ adopters with substantively similar `format_schema` content for 90+ days, the working group promotes it to a first-class canonical. + * REQUIRED when `format_kind: "custom"`; otherwise MUST be absent. Recognized global pattern this custom shape is an instance of, drawn from the [format-shape vocabulary registry](/schemas/core/format-shape-vocabulary.json) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are valid (validators MAY soft-warn) — adopters CAN ship a shape that isn't yet in the registry. Adding entries is a vocabulary PR. Once a `format_shape` entry sees 2+ adopters with substantively similar `format_schema` content for 90+ days, the working group may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format. */ format_shape?: string; /** @@ -2852,6 +2859,8 @@ export type ProductFormatDeclaration = { | NativeInFeedFormatDeclaration | ResponsiveCreativeFormatDeclaration | AgentPlacementFormatDeclaration + | SellerRenderedStatefulDisplayFormatDeclaration + | CoordinatedPlacementsFormatDeclaration | CustomFormatDeclaration ) & { /** @@ -2890,7 +2899,7 @@ export type ProductFormatDeclaration = { */ experimental?: boolean; /** - * REQUIRED when `format_kind: "custom"`; otherwise MUST be absent. Recognized global pattern this custom shape is an instance of, drawn from the [format-shape vocabulary registry](/schemas/core/format-shape-vocabulary.json) (`multi_placement_takeover`, `roadblock`, `branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values valid (validators MAY soft-warn) — adopters CAN ship a shape that isn't yet in the registry. Adding entries is a vocabulary PR. Once a `format_shape` entry sees 2+ adopters with substantively similar `format_schema` content for 90+ days, the working group promotes it to a first-class canonical. + * REQUIRED when `format_kind: "custom"`; otherwise MUST be absent. Recognized global pattern this custom shape is an instance of, drawn from the [format-shape vocabulary registry](/schemas/core/format-shape-vocabulary.json) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are valid (validators MAY soft-warn) — adopters CAN ship a shape that isn't yet in the registry. Adding entries is a vocabulary PR. Once a `format_shape` entry sees 2+ adopters with substantively similar `format_schema` content for 90+ days, the working group may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format. */ format_shape?: string; /** @@ -2931,6 +2940,8 @@ export type ProductFormatDeclaration = { | NativeInFeedFormatDeclaration | ResponsiveCreativeFormatDeclaration | AgentPlacementFormatDeclaration + | SellerRenderedStatefulDisplayFormatDeclaration + | CoordinatedPlacementsFormatDeclaration | CustomFormatDeclaration ); /** @@ -5573,8 +5584,26 @@ export interface AgentPlacementFormatDeclaration { format_kind: 'agent_placement'; params: CanonicalFormatAgentPlacementAISurfaceSponsoredPlacement; } +export interface SellerRenderedStatefulDisplayFormatDeclaration { + format_kind: 'seller_rendered_stateful_display'; + params: CanonicalFormatSellerRenderedStatefulDisplay; +} /** - * Adopter-defined shape that doesn't fit the 12 canonicals. Requires `format_shape` (vocabulary-registered global pattern) and `format_schema` (URI+digest reference to a fetchable schema describing the actual params/slots). `params` shape is governed by the fetched schema rather than baked into AdCP — kept as `type: object` here with `additionalProperties: true` because the canonical schema validates dynamically post-fetch. + * Rectangular rule applied to buyer artwork. State and breakpoint selectors are optional so the same shape can constrain a coordinated-placement component or a specific stateful-display canvas. + */ +export interface CanvasConstraint { + constraint: 'safe_area' | 'reserved_region' | 'decoration_only_edge' | 'no_text_or_logos'; + state_id?: string; + breakpoint_id?: string; + region: { + }; +} +export interface CoordinatedPlacementsFormatDeclaration { + format_kind: 'coordinated_placements'; + params: CanonicalFormatCoordinatedPlacements; +} +/** + * Adopter-defined shape that doesn't fit the 14 canonicals. Requires `format_shape` (vocabulary-registered global pattern) and `format_schema` (URI+digest reference to a fetchable schema describing the actual params/slots). `params` shape is governed by the fetched schema rather than baked into AdCP — kept as `type: object` here with `additionalProperties: true` because the canonical schema validates dynamically post-fetch. */ export interface CustomFormatDeclaration { format_kind: 'custom'; @@ -6264,6 +6293,10 @@ export interface ReportingCapabilities { * Whether this product supports creative-level metric breakdowns in delivery reporting (by_creative within by_package) */ supports_creative_breakdown?: boolean; + /** + * Whether this product supports canonical creative-format breakdowns in GET delivery reporting (by_format within by_package, keyed by format_kind). This is independent from supports_creative_breakdown because a seller may expose aggregate format-grain reporting without exposing individual creative performance. + */ + supports_format_breakdown?: boolean; /** * Whether this product supports keyword-level metric breakdowns in delivery reporting (by_keyword within by_package) */ @@ -7583,6 +7616,10 @@ export interface URLAsset { * Description of what this URL points to */ description?: string; + /** + * Binding used only when this URL populates a `seller_rendered_stateful_display` `state_click_urls` slot. It MUST match one declared `states[].state_id` (semantic validators resolve it); at most one entry per state. Omit for ordinary URL slots. + */ + state_id?: string; provenance?: Provenance; } /** @@ -8472,135 +8509,6 @@ export type ListProductsResponse = { outcome: 'unchanged'; } ); -/** - * Compact canonical creative-format declaration. Legacy named-format links are intentionally absent; params are validated against the canonical schema selected by format_kind without inlining every format union into product discovery. - */ -export type CanonicalFormatOption = { -} & { - format_option_id?: string; - publisher_domain?: string; - display_name?: string; - sample_render_url?: string; - /** - * @minItems 1 - */ - applies_to_channels?: [MediaChannel, ...MediaChannel[]]; - seller_preference?: 'preferred' | 'accepted' | 'discouraged'; - locale_policy?: CreativeLocalePolicy; - canonical_formats_only?: boolean; - experimental?: boolean; - format_kind: - | 'image' - | 'html5' - | 'display_tag' - | 'image_carousel' - | 'video_hosted' - | 'video_vast' - | 'audio_hosted' - | 'audio_daast' - | 'sponsored_placement' - | 'native_in_feed' - | 'responsive_creative' - | 'agent_placement' - | 'custom'; - params: { - }; - format_shape?: string; - format_schema?: PlatformExtensionReference; -} & { - format_option_id?: string; - publisher_domain?: string; - display_name?: string; - sample_render_url?: string; - /** - * @minItems 1 - */ - applies_to_channels?: [MediaChannel, ...MediaChannel[]]; - seller_preference?: 'preferred' | 'accepted' | 'discouraged'; - locale_policy?: CreativeLocalePolicy; - canonical_formats_only?: boolean; - experimental?: boolean; - format_kind: - | 'image' - | 'html5' - | 'display_tag' - | 'image_carousel' - | 'video_hosted' - | 'video_vast' - | 'audio_hosted' - | 'audio_daast' - | 'sponsored_placement' - | 'native_in_feed' - | 'responsive_creative' - | 'agent_placement' - | 'custom'; - params: { - }; - format_shape?: string; - format_schema?: PlatformExtensionReference; -} & { - format_option_id?: string; - publisher_domain?: string; - display_name?: string; - sample_render_url?: string; - /** - * @minItems 1 - */ - applies_to_channels?: [MediaChannel, ...MediaChannel[]]; - seller_preference?: 'preferred' | 'accepted' | 'discouraged'; - locale_policy?: CreativeLocalePolicy; - canonical_formats_only?: boolean; - experimental?: boolean; - format_kind: - | 'image' - | 'html5' - | 'display_tag' - | 'image_carousel' - | 'video_hosted' - | 'video_vast' - | 'audio_hosted' - | 'audio_daast' - | 'sponsored_placement' - | 'native_in_feed' - | 'responsive_creative' - | 'agent_placement' - | 'custom'; - params: { - }; - format_shape?: string; - format_schema?: PlatformExtensionReference; -} & { - format_option_id?: string; - publisher_domain?: string; - display_name?: string; - sample_render_url?: string; - /** - * @minItems 1 - */ - applies_to_channels?: [MediaChannel, ...MediaChannel[]]; - seller_preference?: 'preferred' | 'accepted' | 'discouraged'; - locale_policy?: CreativeLocalePolicy; - canonical_formats_only?: boolean; - experimental?: boolean; - format_kind: - | 'image' - | 'html5' - | 'display_tag' - | 'image_carousel' - | 'video_hosted' - | 'video_vast' - | 'audio_hosted' - | 'audio_daast' - | 'sponsored_placement' - | 'native_in_feed' - | 'responsive_creative' - | 'agent_placement' - | 'custom'; - params: { - }; - format_shape?: string; - format_schema?: PlatformExtensionReference; -}; /** * Compact product placement with canonical format narrowing only. */ @@ -8935,6 +8843,7 @@ export interface CanonicalReportingCapabilities { metric_id: VendorMetricID; }[]; supports_creative_breakdown?: boolean; + supports_format_breakdown?: boolean; supports_keyword_breakdown?: boolean; supports_geo_breakdown?: GeographicBreakdownSupport; supports_device_type_breakdown?: boolean; @@ -10036,7 +9945,7 @@ export interface ReportingWebhook { */ reporting_frequency: 'hourly' | 'daily' | 'monthly'; /** - * Optional list of metrics to include in webhook notifications. If omitted, all available metrics are included. Must be subset of product's available_metrics. + * Optional list of metrics to include in webhook notifications. If omitted, all available metrics are included; an empty array has the same meaning as omission (it does not narrow to impressions and spend only). impressions and spend are always included regardless of this list. Must be subset of product's available_metrics. Subset evaluation and leaf resolution follow the container-subsumption rule in `enums/available-metric.json`: requesting a leaf identity selects its canonical carrier object in the payload. Same narrowing semantics as get_media_buy_delivery's requested_metrics (which additionally requires at least one entry when present). */ requested_metrics?: AvailableMetric[]; } @@ -11359,6 +11268,16 @@ export type PackageRequest = AdCPVersionEnvelope & { scope: 'vendor'; vendor: BrandReference; metric_id: VendorMetricID; + /** + * Optional disambiguator for vendor metrics committed under more than one methodology or window — same closed key set as standard-scope entries. + */ + qualifier?: { + viewability_standard?: ViewabilityStandard; + completion_source?: CompletionSource; + attribution_methodology?: AttributionMethodology; + attribution_window?: Duration; + lift_dimension?: LiftDimension; + }; } ), ...( @@ -11386,6 +11305,16 @@ export type PackageRequest = AdCPVersionEnvelope & { scope: 'vendor'; vendor: BrandReference; metric_id: VendorMetricID; + /** + * Optional disambiguator for vendor metrics committed under more than one methodology or window — same closed key set as standard-scope entries. + */ + qualifier?: { + viewability_standard?: ViewabilityStandard; + completion_source?: CompletionSource; + attribution_methodology?: AttributionMethodology; + attribution_window?: Duration; + lift_dimension?: LiftDimension; + }; } )[] ]; @@ -12144,6 +12073,14 @@ export interface CardAsset { platform_extensions?: PlatformExtensionReference[]; provenance?: Provenance; } +/** + * Map of canonical asset-group or legacy asset identifiers to supplied creative assets. Values are either a single discriminated asset or a non-empty repeatable asset array. + * + * This interface was referenced by `undefined`'s JSON-Schema definition + * via the `patternProperty` "^[a-z][a-z0-9_]*$". + */ +export interface CreativeAssets { +} /** * An industry-standard or market-specific identifier for an advertising creative (e.g., Ad-ID, ISCI, Clearcast clock number, IDcrea). These identifiers are managed by external registries or clearance bodies and used across the supply chain to track and reference specific creative assets. Add a PR to extend creative-identifier-type when another shared identifier scheme needs first-class support. */ @@ -13628,6 +13565,16 @@ export type CommittedMetric = scope: 'vendor'; vendor: BrandReference; metric_id: VendorMetricID; + /** + * Optional qualifier disambiguating commitments to the same vendor metric measured under different methodologies or windows. Same closed key set as standard-scope entries; new keys ship explicitly. + */ + qualifier?: { + viewability_standard?: ViewabilityStandard; + completion_source?: CompletionSource; + attribution_methodology?: AttributionMethodology; + attribution_window?: Duration; + lift_dimension?: LiftDimension; + }; /** * ISO 8601 timestamp when this vendor metric became part of the contract. */ @@ -15209,6 +15156,10 @@ export interface GetMediaBuyDeliveryRequest { * When true, include daily_breakdown arrays within each package in by_package. Useful for per-package pacing analysis and line-item monitoring. Omit or set false to reduce response size — package daily data can be large for multi-package buys over long flights. */ include_package_daily_breakdown?: boolean; + /** + * Optional list of metrics to include in the response. When omitted, all available metrics are included (unchanged behavior). Applies to every metrics-bearing object in the response: totals, by_package, daily and window slices, and breakdown rows. impressions and spend are always included regardless of this list. Requesting a leaf metric identity returns its canonical nested carrier — e.g. requesting viewable_rate returns the viewability object, requesting quartile_75 returns quartile_data — never a flat duplicate. Metrics requested but not available for this buy are omitted from the response without error; contract accountability is unchanged — missing_metrics still reconciles against committed_metrics, but sellers MUST NOT list a metric in missing_metrics when its absence is solely due to this narrowing. Must be a subset of the product's reporting_capabilities.available_metrics; values outside the declared set are ignored. Subset evaluation follows the container-subsumption rule in enums/available-metric.json. Sort is evaluated before narrowing: excluding a metric from this list never triggers the sort_by fallback, and breakdown rows may be ordered by a metric absent from the narrowed payload — the applied-sort echo still names it. Same narrowing semantics as reporting_webhook.requested_metrics, with one shape difference: this field requires at least one entry when present (omit it entirely for full payloads), while the webhook field permits an empty array with the same meaning as omission. + */ + requested_metrics?: AvailableMetric[]; time_granularity?: ReportingFrequency; /** * When true, the response includes media_buy_deliveries[].windows[] — an array of per-window delivery slices over the date range at the requested time_granularity. Ignored when time_granularity is omitted. Each window's payload mirrors what reporting_webhook would have delivered for the same window, enabling lossless GET-path recovery for buyers who missed webhook fires. Omit or set false to reduce response size when only cumulative aggregates are needed. @@ -15229,9 +15180,45 @@ export interface GetMediaBuyDeliveryRequest { model?: AttributionModel; }; /** - * Request dimensional breakdowns in delivery reporting. Each key enables a specific breakdown dimension within by_package — include as an empty object (e.g., "device_type": {}) to activate with defaults. Omit entirely for no breakdowns (backward compatible). Unsupported dimensions are silently omitted from the response. Note: keyword, catalog_item, and creative breakdowns are returned automatically when the seller supports them and are not controlled by this object. + * Request dimensional breakdowns in delivery reporting. Each key enables a specific breakdown dimension within by_package — include as an empty object (e.g., "device_type": {}) to activate with defaults. Omit entirely for no breakdowns (backward compatible). Unsupported dimensions are silently omitted from the response. Note: keyword, catalog_item, and creative breakdowns are returned automatically when the seller supports them; including their keys here is optional and upgrades them to a negotiated contract (limit, sort, and truncation disclosure) without changing the automatic default. */ reporting_dimensions?: { + /** + * Request a negotiated catalog_item breakdown. Omitting this key preserves the automatic behavior — sellers return catalog_item rows at their discretion with no truncation contract. Including it (even as {}) makes the truncation disclosure and applied-sort echo binding. + */ + catalog_item?: { + /** + * Maximum number of catalog_item entries to return. When omitted, the seller returns its automatic default set. + * @minimum 1 + */ + limit?: number; + sort_by?: SortMetric; + sort_direction?: SortDirection; + }; + /** + * Request a negotiated creative breakdown. Omitting this key preserves the automatic behavior — sellers return creative rows at their discretion with no truncation contract. Including it (even as {}) makes the truncation disclosure and applied-sort echo binding. + */ + creative?: { + /** + * Maximum number of creative entries to return. When omitted, the seller returns its automatic default set. + * @minimum 1 + */ + limit?: number; + sort_by?: SortMetric; + sort_direction?: SortDirection; + }; + /** + * Request a negotiated keyword breakdown. Omitting this key preserves the automatic behavior — sellers return keyword rows at their discretion with no truncation contract. Including it (even as {}) makes the truncation disclosure and applied-sort echo binding. + */ + keyword?: { + /** + * Maximum number of keyword entries to return. When omitted, the seller returns its automatic default set. + * @minimum 1 + */ + limit?: number; + sort_by?: SortMetric; + sort_direction?: SortDirection; + }; /** * Request geographic breakdown. Check reporting_capabilities.supports_geo_breakdown for available levels and systems. */ @@ -15252,6 +15239,7 @@ export interface GetMediaBuyDeliveryRequest { */ limit?: number; sort_by?: SortMetric; + sort_direction?: SortDirection; }; /** * Request device type breakdown. @@ -15263,6 +15251,7 @@ export interface GetMediaBuyDeliveryRequest { */ limit?: number; sort_by?: SortMetric; + sort_direction?: SortDirection; }; /** * Request device platform breakdown. @@ -15274,6 +15263,19 @@ export interface GetMediaBuyDeliveryRequest { */ limit?: number; sort_by?: SortMetric; + sort_direction?: SortDirection; + }; + /** + * Request delivery broken down by canonical creative format kind. This dimension is negotiated on the GET path. Reporting webhook configuration does not negotiate or guarantee dimensional breakdowns, although a webhook payload may carry the same fields as an extension. + */ + format?: { + /** + * Maximum number of format rows to return. When omitted, all rows are returned because the canonical format-kind vocabulary is small and bounded. + * @minimum 1 + */ + limit?: number; + sort_by?: SortMetric; + sort_direction?: SortDirection; }; /** * Request audience segment breakdown. @@ -15285,6 +15287,7 @@ export interface GetMediaBuyDeliveryRequest { */ limit?: number; sort_by?: SortMetric; + sort_direction?: SortDirection; }; /** * Request delivery broken down by demographic. Check the product's reporting_capabilities.supports_demographic_breakdown independently from demographic_targeting. When age_ranges is present, every requested range MUST be exactly supported by exact_predicates or equal one of the declared enumerated_intervals; sellers MUST reject unsupported ranges with UNSUPPORTED_FEATURE rather than silently widen or narrow them. @@ -15300,6 +15303,7 @@ export interface GetMediaBuyDeliveryRequest { */ limit?: number; sort_by?: SortMetric; + sort_direction?: SortDirection; }; /** * Request a spot-level as-run airing log for broadcast TV, radio, or other scheduled inventory. Rows are ordered by aired_at ascending. When limit is omitted, sellers SHOULD return the complete log for the requested reporting period. @@ -15321,6 +15325,7 @@ export interface GetMediaBuyDeliveryRequest { */ limit?: number; sort_by?: SortMetric; + sort_direction?: SortDirection; }; }; context?: ContextObject; @@ -15350,6 +15355,16 @@ export type MissingMetric = scope: 'vendor'; vendor: BrandReference; metric_id: VendorMetricID; + /** + * Mirrors the qualifier on the corresponding vendor-scope `committed_metrics` entry. MUST match that entry so reconciliation joins on (vendor, metric_id, qualifier). + */ + qualifier?: { + viewability_standard?: ViewabilityStandard; + completion_source?: CompletionSource; + attribution_methodology?: AttributionMethodology; + attribution_window?: Duration; + lift_dimension?: LiftDimension; + }; }; /** * Delivery metrics row for one catalog item within a package. @@ -15439,7 +15454,6 @@ export type GetMediaBuyDeliveryPlacementMetrics = DeliveryMetrics & { */ publisher_domain?: string; }; - /** * Response payload for get_media_buy_delivery task */ @@ -15725,21 +15739,51 @@ export interface GetMediaBuyDeliveryResponse { */ supersedes_window?: string; /** - * Metrics that the binding reporting contract declared but that are NOT populated in this report. Reconciliation source: when `package.committed_metrics` is present, `missing_metrics` is computed against entries where `committed_at < reporting_period.end` — independent of subsequent product mutations and respecting the commitment timestamp on each entry (a metric committed mid-flight is only flagged missing in reports for periods after its commitment). When `package.committed_metrics` is absent, fall back to the product's current `reporting_capabilities.available_metrics` (no timestamp filter). Empty array (or absent) indicates clean delivery against the contract. Non-empty signals an accountability breach — the seller committed to the metric but did not produce the value here. Sellers MUST exclude metrics that are not yet measurable for the current `measurement_window` (e.g., post-IVT counts during the live window) — those will appear (or not) when a wider window supersedes this report via `supersedes_window`. Each entry uses an explicit `scope` discriminator: `standard` for entries from the closed `available-metric.json` enum, `vendor` for vendor-defined metrics anchored on a BrandRef. Symmetric with `committed_metrics`. + * Metrics that the binding reporting contract declared but that are NOT populated in this report. Reconciliation source: when `package.committed_metrics` is present, `missing_metrics` is computed against entries where `committed_at < reporting_period.end` — independent of subsequent product mutations and respecting the commitment timestamp on each entry (a metric committed mid-flight is only flagged missing in reports for periods after its commitment). When `package.committed_metrics` is absent, fall back to the product's current `reporting_capabilities.available_metrics` (no timestamp filter). Empty array (or absent) indicates clean delivery against the contract. Non-empty signals an accountability breach — the seller committed to the metric but did not produce the value here. Sellers MUST exclude metrics that are not yet measurable for the current `measurement_window` (e.g., post-IVT counts during the live window) — those will appear (or not) when a wider window supersedes this report via `supersedes_window`. Each entry uses an explicit `scope` discriminator: `standard` for entries from the closed `available-metric.json` enum, `vendor` for vendor-defined metrics anchored on a BrandRef. Symmetric with `committed_metrics`. When the request narrowed the payload via requested_metrics, sellers MUST NOT list a committed metric here solely because the buyer excluded it — missing_metrics reports delivery gaps, not request narrowing. */ missing_metrics?: MissingMetric[]; /** * Delivery by catalog item within this package. Available for catalog-driven packages when the seller supports item-level reporting. */ by_catalog_item?: GetMediaBuyDeliveryCatalogItemMetrics[]; + /** + * Whether by_catalog_item was truncated due to the requested limit or a seller-imposed maximum. Sellers MUST return this flag whenever by_catalog_item is present and the request included reporting_dimensions.catalog_item (false means the list is complete). When the breakdown was returned automatically without a request key, the flag is RECOMMENDED but not required — automatic rows carry no completeness contract. + */ + by_catalog_item_truncated?: boolean; + by_catalog_item_sorted_by?: SortMetric; + by_catalog_item_sort_direction?: SortDirection; /** * Metrics broken down by creative within this package. Available when the seller supports creative-level reporting. */ by_creative?: CreativeDeliveryMetrics[]; + /** + * Delivery by canonical creative format kind within this package. Negotiated on the GET path when the buyer requests reporting_dimensions.format and the product declares supports_format_breakdown; reporting webhook configuration does not negotiate or guarantee this breakdown. Each row aggregates every served creative of that format kind. Sellers MUST aggregate all delivery using adopter-defined shapes into one format_kind 'custom' row. When by_format_truncated is false, additive metrics such as impressions and spend across the rows SHOULD reconcile to the corresponding package totals, subject to the measurement and attribution semantics of each metric. Buyers MUST NOT expect row-level correspondence between by_format and by_creative because the two breakdowns are independently produced at different grains. + */ + by_format?: (DeliveryMetrics & { + format_kind: CanonicalFormatKind; + })[]; + /** + * Whether by_format was truncated due to the requested limit or a seller-imposed maximum. Sellers MUST return this flag whenever by_format is present (false means the list is complete). + */ + by_format_truncated?: boolean; + by_format_sorted_by?: SortMetric; + by_format_sort_direction?: SortDirection; + /** + * Whether by_creative was truncated due to the requested limit or a seller-imposed maximum. Sellers MUST return this flag whenever by_creative is present and the request included reporting_dimensions.creative (false means the list is complete). When the breakdown was returned automatically without a request key, the flag is RECOMMENDED but not required — automatic rows carry no completeness contract. + */ + by_creative_truncated?: boolean; + by_creative_sorted_by?: SortMetric; + by_creative_sort_direction?: SortDirection; /** * Metrics broken down by keyword within this package. One row per (keyword, match_type) pair — the same keyword with different match types appears as separate rows. Keyword-grain only: rows reflect aggregate performance of each targeted keyword, not individual search queries. Rows may not sum to package totals when a single impression is attributed to the triggering keyword only. Available for search and retail media packages when the seller supports keyword-level reporting. */ by_keyword?: GetMediaBuyDeliveryKeywordMetrics[]; + /** + * Whether by_keyword was truncated due to the requested limit or a seller-imposed maximum. Sellers MUST return this flag whenever by_keyword is present and the request included reporting_dimensions.keyword (false means the list is complete). When the breakdown was returned automatically without a request key, the flag is RECOMMENDED but not required — automatic rows carry no completeness contract. + */ + by_keyword_truncated?: boolean; + by_keyword_sorted_by?: SortMetric; + by_keyword_sort_direction?: SortDirection; /** * Delivery by geographic area within this package. Available when the buyer requests geo breakdown via reporting_dimensions and the seller supports it. Each dimension's rows are independent slices that should sum to the package total. */ @@ -15748,6 +15792,8 @@ export interface GetMediaBuyDeliveryResponse { * Whether by_geo was truncated due to the requested limit or a seller-imposed maximum. Sellers MUST return this flag whenever by_geo is present (false means the list is complete). */ by_geo_truncated?: boolean; + by_geo_sorted_by?: SortMetric; + by_geo_sort_direction?: SortDirection; /** * Delivery by device form factor within this package. Available when the buyer requests device_type breakdown via reporting_dimensions and the seller supports it. */ @@ -15756,6 +15802,8 @@ export interface GetMediaBuyDeliveryResponse { * Whether by_device_type was truncated. Sellers MUST return this flag whenever by_device_type is present (false means the list is complete). */ by_device_type_truncated?: boolean; + by_device_type_sorted_by?: SortMetric; + by_device_type_sort_direction?: SortDirection; /** * Delivery by operating system within this package. Available when the buyer requests device_platform breakdown via reporting_dimensions and the seller supports it. Useful for CTV campaigns where tvOS vs Roku OS vs Fire OS matters. */ @@ -15764,6 +15812,8 @@ export interface GetMediaBuyDeliveryResponse { * Whether by_device_platform was truncated. Sellers MUST return this flag whenever by_device_platform is present (false means the list is complete). */ by_device_platform_truncated?: boolean; + by_device_platform_sorted_by?: SortMetric; + by_device_platform_sort_direction?: SortDirection; /** * Delivery by audience segment within this package. Available when the buyer requests audience breakdown via reporting_dimensions and the seller supports it. Only 'synced' audiences are directly targetable via the targeting overlay; other sources are informational. */ @@ -15772,6 +15822,8 @@ export interface GetMediaBuyDeliveryResponse { * Whether by_audience was truncated. Sellers MUST return this flag whenever by_audience is present (false means the list is complete). */ by_audience_truncated?: boolean; + by_audience_sorted_by?: SortMetric; + by_audience_sort_direction?: SortDirection; /** * Delivery by demographic within this package. Available when the buyer requests demographic breakdown and the product declares supports_demographic_breakdown. A free-form measurement code does not prove alignment with buyer targeting. When age is present it is the authoritative machine-comparable interval; for requested age_ranges, sellers MUST echo the exact requested interval and MUST NOT substitute a wider or narrower native bucket. */ @@ -15789,6 +15841,8 @@ export interface GetMediaBuyDeliveryResponse { * Whether non-suppressed by_demographic rows were truncated due to the requested limit or a seller-imposed maximum. Sellers MUST return this flag whenever by_demographic is present. False means every non-suppressed row is present; inspect by_demographic_suppressed separately before reconciling rows to package totals. */ by_demographic_truncated?: boolean; + by_demographic_sorted_by?: SortMetric; + by_demographic_sort_direction?: SortDirection; /** * Whether one or more otherwise reportable demographic rows were omitted due to privacy, policy, or measurement thresholds. Sellers MUST return this flag whenever by_demographic is present. False means no rows were threshold-suppressed. */ @@ -15801,6 +15855,8 @@ export interface GetMediaBuyDeliveryResponse { * Whether by_placement was truncated. Sellers MUST return this flag whenever by_placement is present (false means the list is complete). */ by_placement_truncated?: boolean; + by_placement_sorted_by?: SortMetric; + by_placement_sort_direction?: SortDirection; /** * Spot-level as-run airing records for broadcast TV, radio, or other scheduled inventory. Available when the buyer requests spot breakdown and the product declares supports_spot_breakdown. Sellers MUST order rows by aired_at ascending. The same spot_id is reused when a later package measurement_window adds or revises metrics. Network and station are optional so station-direct radio and network-level TV records use the same channel-neutral shape. */ @@ -15901,7 +15957,7 @@ export interface GetMediaBuyDeliveryResponse { */ totals: DeliveryMetrics; /** - * Per-package metrics for this window slice. Same shape as the parent media_buy_deliveries[].by_package row but scoped to the window. Sellers MAY omit when per-package window-level data is unavailable; when present, package_id values MUST match the parent by_package entries. + * Per-package metrics for this window slice, using the same metric envelope and package identity as the parent media_buy_deliveries[].by_package row but scoped to the window. Requested reporting_dimensions do not apply to these webhook-aligned recovery rows; sellers may include dimensional fields only as webhook payload extensions, not as a guaranteed result of the GET request. Sellers MAY omit by_package when per-package window-level data is unavailable; when present, package_id values MUST match the parent by_package entries. */ by_package?: (DeliveryMetrics & { /** @@ -15994,6 +16050,16 @@ export interface VendorMetricValue { * Number of impressions in this reporting period that the vendor was able to measure. Coverage denominator — buyers compute coverage rate as `measurable_impressions / impressions`. When absent, coverage is unspecified — buyers MUST NOT compute a coverage rate or assume full coverage. When the vendor measured zero impressions but is integrated, set to 0 explicitly. When the entry is omitted from `vendor_metric_values` entirely, the buyer infers no measurement happened (no integration). This pattern parallels `viewability.measurable_impressions` (`delivery-metrics.json#/properties/viewability`), which has handled vendor coverage in the IAS/DV/MRC ecosystem for over a decade — same convention: absence is unknown, not full. */ measurable_impressions?: number; + /** + * Optional qualifier disambiguating this row from sibling rows for the same (vendor, metric_id) — e.g., the same vendor outcome metric reported under 7-day and 30-day attribution windows. Same closed key set as `committed-metric`. When the matching `committed_metrics` entry carries a qualifier, this row MUST carry the identical qualifier so reconciliation joins on `(vendor, metric_id, qualifier)`. + */ + qualifier?: { + viewability_standard?: ViewabilityStandard; + completion_source?: CompletionSource; + attribution_methodology?: AttributionMethodology; + attribution_window?: Duration; + lift_dimension?: LiftDimension; + }; /** * Optional structured payload for vendor metrics that don't fit a single scalar — panel demographic breakouts, co-view audience composition, incremental reach + frequency + lift decompositions. Free-form; the keys and value semantics are defined by the vendor (see the vendor's `brand.json` measurement-agent docs). Buyers MUST treat this object as opaque without consulting the vendor's documentation. Vendors place any fields beyond the standard envelope (e.g., confidence intervals, panel sizes) inside this object rather than at the top level. */ @@ -16024,6 +16090,16 @@ export type PerformanceFeedbackMetric = scope: 'vendor'; vendor: BrandReference; metric_id: VendorMetricID; + /** + * Optional disambiguator mirroring the vendor-scope qualifier on `committed_metrics` — same closed key set as standard-scope entries. + */ + qualifier?: { + viewability_standard?: ViewabilityStandard; + completion_source?: CompletionSource; + attribution_methodology?: AttributionMethodology; + attribution_window?: Duration; + lift_dimension?: LiftDimension; + }; }; /** * Submits one compact optimizer-ready assertion. Measurement agents submit to a buyer-controlled orchestrator gateway through this task; the orchestrator validates, normalizes, and forwards feedback to sellers under the buyer's identity. @@ -17681,6 +17757,7 @@ export type BuildCreativeRequest = { * Creative manifest to transform or generate from. On the canonical 3.2 path it carries `format_kind`, optional `format_option_ref`, and the required input assets. For transformation (for example resizing or reformatting), this is the complete creative to adapt. When creative_id is provided, the agent resolves the creative from its library and this field is ignored. */ export type CreativeManifest = { +} & { format_id?: FormatReferenceStructuredObject; format_kind?: CanonicalFormatKind; format_option_ref?: FormatOptionReference; @@ -17692,6 +17769,12 @@ export type CreativeManifest = { assets: { [k: string]: AssetVariant | AssetVariant[]; }; + /** + * Component-addressed asset maps for `coordinated_placements`. Each key MUST match one `params.components[].component_id`; its value supplies that component's canonical slots. Shared assets remain in top-level `assets` and are injected only into components named by `shared_slots[].consumed_by`. This namespace allows two components to use the same canonical slot name, such as `image_main`, without collision. It MUST be absent for non-`coordinated_placements` manifests. + */ + component_assets?: { + [k: string]: CreativeAssets | undefined; + }; brand?: BrandReference; /** * Rights constraints attached to this creative. Buyer-carried fields are informational until a serving party evaluates an issuer-bound attestation reference under its own policy. Only a verified, unexpired, unrevoked, digest-matched evaluation can support serving authorization; verification_url is never authority. @@ -20151,6 +20234,12 @@ export interface ListedCreativeNamedFormatReference { */ [k: string]: AssetVariant | AssetVariant[]; }; + /** + * Preserved component-addressed asset maps for `coordinated_placements`, keyed by coordinated component ID. + */ + component_assets?: { + [k: string]: CreativeAssets | undefined; + }; localization?: CreativeLocalizationReadback; /** * Per-creative fail-closed state returned instead of localization when the seller knows the creative is localized but cannot construct complete exact readback. The creative remains in this page and counts toward query_summary.returned and pagination; buyers may continue using the base creative fields but MUST NOT infer locale eligibility. @@ -20434,6 +20523,12 @@ export interface ListedCreativeCanonicalFormatKind { */ [k: string]: AssetVariant | AssetVariant[]; }; + /** + * Preserved component-addressed asset maps for `coordinated_placements`, keyed by coordinated component ID. + */ + component_assets?: { + [k: string]: CreativeAssets | undefined; + }; localization?: CreativeLocalizationReadback; /** * Per-creative fail-closed state returned instead of localization when the seller knows the creative is localized but cannot construct complete exact readback. The creative remains in this page and counts toward query_summary.returned and pagination; buyers may continue using the base creative fields but MUST NOT infer locale eligibility. @@ -21005,6 +21100,14 @@ export type ValidateInputResult = { retry_with?: { }; }[]; + /** + * Non-blocking observations (e.g. LEAN policy advisories such as hover-triggered expansion or non-user-initiated entry into overlay anchoring) that do not affect `result_kind`. MAY be present alongside `validated_pass`, `validated_fail`, or `unvalidatable_nondeterministic`. Same item shape as `violations`. + */ + warnings?: { + rule: string; + expected?: unknown; + predicted?: unknown; + }[]; }; /** @@ -28977,15 +29080,15 @@ export interface GetAdCPCapabilitiesResponse { */ supported_for?: string[]; /** - * JSON-RPC protocol method names (e.g., 'tasks/cancel', 'tasks/get') for which this agent verifies signatures when present. Under the 3.2 profile, a presented signature on a body-bearing request without content-digest coverage rejects. Disjoint from supported_for, which carries AdCP tool names only. Items MUST be wire-format JSON-RPC method strings containing `/`; plain AdCP tool names belong in supported_for. + * JSON-RPC protocol method names for which this agent verifies signatures when present. Values MUST use exact, case-sensitive equality after JSON decoding: slash-path names such as 'tasks/cancel' and 'tasks/pushNotificationConfig/set' for A2A 0.3, or PascalCase names such as 'CancelTask' and 'CreateTaskPushNotificationConfig' for A2A 1.0. A dual-stack agent lists each supported wire name independently; implementations MUST NOT translate or normalize between protocol versions. The reserved MCP envelope method 'tools/call' is forbidden because its AdCP operation identity is params.name and belongs in supported_for. Under the 3.2 profile, a presented signature on a body-bearing request without content-digest coverage rejects. Disjoint from supported_for, which carries lower_snake_case AdCP operation names only. */ protocol_methods_supported_for?: string[]; /** - * Protocol method names for shadow-mode verification, mirroring warn_for in the AdCP-tool namespace. Missing signatures and well-formed signatures that fail verification or body binding are recorded but MUST NOT establish verified-signer identity; processing continues only when an independent authenticator succeeds. A partial or malformed Signature / Signature-Input pair always hard-rejects. An item MUST NOT appear in both protocol_methods_warn_for and protocol_methods_required_for; see x-adcp-validation. + * Exact JSON-RPC protocol method names for shadow-mode verification, mirroring warn_for in the AdCP-operation namespace. Wire-name grammar and exact, case-sensitive matching semantics are identical to protocol_methods_supported_for. Missing signatures and well-formed signatures that fail verification or body binding are recorded but MUST NOT establish verified-signer identity; processing continues only when an independent authenticator succeeds. A partial or malformed Signature / Signature-Input pair always hard-rejects. An item MUST NOT appear in both protocol_methods_warn_for and protocol_methods_required_for; see x-adcp-validation. */ protocol_methods_warn_for?: string[]; /** - * JSON-RPC protocol method names for which this agent rejects an unsigned request with request_signature_required unless an independently valid configured fallback authenticator succeeds. Separate namespace from required_for: this bucket binds against the JSON-RPC method field, not tools/call params.name. Every listed method MUST also appear in protocol_methods_supported_for; see x-adcp-validation. + * Exact JSON-RPC protocol method names for which this agent rejects an unsigned request with request_signature_required unless an independently valid configured fallback authenticator succeeds. Wire-name grammar and exact, case-sensitive matching semantics are identical to protocol_methods_supported_for. Separate namespace from required_for: this bucket binds against the JSON-RPC method field, not tools/call params.name. Every listed method MUST also appear in protocol_methods_supported_for; see x-adcp-validation. */ protocol_methods_required_for?: string[]; }; diff --git a/src/lib/types/wellknown-schemas.generated.ts b/src/lib/types/wellknown-schemas.generated.ts index 2c0e73ea6..fb3806c83 100644 --- a/src/lib/types/wellknown-schemas.generated.ts +++ b/src/lib/types/wellknown-schemas.generated.ts @@ -1,5 +1,5 @@ // Generated Zod schemas for AdCP well-known files (brand.json, adagents.json) -// Generated at: 2026-08-18T22:43:22.395Z +// Generated at: 2026-08-24T06:46:40.269Z // Source: schemas/cache/latest/*.json → json-schema-to-zod // // DO NOT EDIT — regenerate with: npm run generate-wellknown-schemas @@ -7,26 +7,26 @@ import { z } from 'zod'; // ---- brand.json ---- -export const BrandJsonSchema = z.union([z.object({ "$schema": z.string().optional(), "authoritative_location": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL of the authoritative brand.json file"), "redirect_reason": z.enum(["acquisition","divestiture","rebrand","regional","legacy","consolidation","other"]).describe("Optional structured signal indicating why this redirect was put in place. Consumers SHOULD use this to inform cache TTL decisions: 'acquisition' / 'divestiture' / 'rebrand' / 'consolidation' suggest the resolved target is in transition and consumers SHOULD shorten cache TTL until stable. 'regional' / 'legacy' suggest a stable redirect with no special cache handling needed. Free-text rationale belongs in 'note'.").optional(), "redirect_effective_at": z.string().datetime().describe("Optional timestamp when this redirect became effective. Caches MUST treat any entry cached before this timestamp as stale and re-fetch through the redirect.").optional(), "note": z.string().describe("Optional human-readable rationale for the redirect.").optional(), "last_updated": z.string().datetime().optional() }).strict().describe("Redirects to a hosted brand.json file at another URL"), z.object({ "$schema": z.string().optional(), "house": z.string().regex(new RegExp("^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$")).describe("House domain to fetch brand portfolio from"), "region": z.string().regex(new RegExp("^[A-Z]{2}$")).describe("ISO 3166-1 alpha-2 country code if this is a regional domain").optional(), "redirect_reason": z.enum(["acquisition","divestiture","rebrand","regional","legacy","consolidation","other"]).describe("Optional structured signal indicating why this redirect was put in place. Consumers SHOULD use this to inform cache TTL decisions: 'acquisition' / 'divestiture' / 'rebrand' / 'consolidation' suggest the resolved target is in transition and consumers SHOULD shorten cache TTL until stable. 'regional' / 'legacy' suggest a stable redirect with no special cache handling needed. Free-text rationale belongs in 'note'.").optional(), "redirect_effective_at": z.string().datetime().describe("Optional timestamp when this redirect became effective. Caches MUST treat any entry cached before this timestamp as stale and re-fetch through the redirect.").optional(), "note": z.string().describe("Optional human-readable rationale for the redirect.").optional(), "last_updated": z.string().datetime().optional() }).strict().describe("Redirects to the house domain that contains the full brand portfolio"), z.object({ "$schema": z.string().optional(), "version": z.string().optional(), "agents": z.array(z.object({ "type": z.enum(["brand","rights","measurement","governance","creative","sales","buying","signals"]).describe("Functional role of this agent"), "url": z.string().url().regex(new RegExp("^https://")).describe("Agent endpoint URL (MCP or A2A). Callers comparing a brand's declared agent URL against another value (e.g., resolving 'is this the agent that signed this artifact?' or matching against a discovery cache) MUST canonicalize both sides per the AdCP URL canonicalization rules, not byte-equality. See docs/reference/url-canonicalization."), "id": z.string().regex(new RegExp("^[a-z0-9_]+$")).max(100).describe("Agent identifier (useful for logging, multi-tenant platforms)"), "description": z.string().max(500).describe("Human-readable description of this agent's capabilities or scope").optional(), "jwks_uri": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL of the agent's JWKS (RFC 7517) containing public keys used to verify artifacts this agent signs or requests it sends. Verified artifacts include signed governance_context tokens (for governance agents) and RFC 9421 HTTP Signatures on outgoing requests (for any agent). When absent, verifiers MUST default to /.well-known/jwks.json on the origin of `url`. Keys are identified by `kid` in the JWS header or RFC 9421 `keyid` parameter; JWKS MAY contain multiple keys to support rotation and per-purpose separation via `key_ops` and `use`.").optional(), "available_uses": z.array(z.enum(["likeness","voice","name","endorsement","motion_capture","signature","catchphrase","sync","background_music","editorial","commercial","ai_generated_image"]).describe("Types of rights usage that can be licensed through the brand protocol. Aligned with DDEX UseType direction for interoperability with music and media rights systems.")).describe("For rights agents: rights uses available for licensing").optional(), "right_types": z.array(z.enum(["talent","character","brand_ip","music","stock_media"]).describe("Categories of intellectual property rights that can be licensed through the brand protocol.")).describe("For rights agents: types of rights available").optional(), "countries": z.array(z.string().regex(new RegExp("^[A-Z]{2}$"))).describe("ISO 3166-1 alpha-2 country codes where this agent operates. Omit for global scope.").optional() }).catchall(z.any()).describe("An agent declared by a brand or house. Each entry identifies one agent endpoint and its functional role in the advertising ecosystem.")).describe("Agents declared by this brand or house. Multiple entries with the same type are permitted when they have distinct url values, such as one endpoint URL per tenant or property scope. Agent url values MUST be unique within this array; duplicate urls are invalid because signature verifiers resolve a signing key by matching one agent url to one agents[] entry and reject ambiguous matches.").optional(), "brand_agent": z.object({ "url": z.string().url().regex(new RegExp("^https://")).describe("Brand agent MCP endpoint URL. Callers comparing this URL against another value (e.g., resolving 'is this the brand's declared agent?' against a discovery cache) MUST canonicalize both sides per the AdCP URL canonicalization rules, not byte-equality. See docs/reference/url-canonicalization."), "id": z.string().regex(new RegExp("^[a-z0-9_]+$")).describe("Agent identifier (useful for logging, multi-tenant DAMs)") }).catchall(z.any()).describe("Reference to a brand agent that provides brand data via MCP").optional(), "contact": z.object({ "name": z.string().min(1).max(255), "email": z.string().email().max(255).optional(), "domain": z.string().regex(new RegExp("^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$")).describe("A valid domain name").optional() }).catchall(z.any()).describe("Contact information").optional(), "data_subject_contestation": z.object({ "url": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL to a human-accessible contestation form or information page.").optional(), "email": z.string().email().describe("Email address for contestation requests. Deployer MUST monitor and respond within the timelines set by applicable law (e.g., 1 month under GDPR Art. 12(3)).").optional(), "languages": z.array(z.string()).describe("BCP 47 language tags the contestation channel supports (e.g., ['en', 'de', 'fr']). At minimum SHOULD include a language spoken in every jurisdiction where the brand runs regulated-vertical campaigns.").optional() }).strict().and(z.union([z.any(), z.any()])).describe("Contact point where a data subject can request human intervention, express their view, or contest an automated decision — satisfying GDPR Article 22(3) and EU AI Act Article 26(11) transparency obligations. This is a contact reference (URL, email, or both), not a machine-callable API. AdCP surfaces the pointer; the deployer runs the contestation workflow.").optional(), "last_updated": z.string().datetime().optional() }).strict().and(z.union([z.any(), z.any()])).describe("Brand represented by agents that provide brand info via MCP"), z.object({ "$schema": z.string().optional(), "version": z.string().optional(), "default_language": z.string().regex(new RegExp("^(?:[a-z]{2,8}(?:-[A-Z][a-z]{3})?(?:-(?:[A-Z]{2}|[0-9]{3}))?(?:-(?:[a-z0-9]{5,8}|[0-9][a-z0-9]{3}))*(?:-[0-9a-wy-z](?:-[a-z0-9]{2,8})+)*(?:-x(?:-[a-z0-9]{1,8})+)?|x(?:-[a-z0-9]{1,8})+)$")).min(2).max(63).describe("A well-formed BCP 47 language tag used by AdCP only as language identity. Script and region may refine that identity; other valid BCP 47 subtags remain part of tag matching but do not make this a general locale-settings object. It does not determine currency, time zone, number/date formatting, market, or legal jurisdiction. The AdCP canonical wire profile requires lower-case language and variants, title-case script, and upper-case region (for example `en-US`, `zh-Hant-TW`, or `x-private`). RFC 5646 comparisons are case-insensitive and its case regularization is optional; AdCP intentionally requires this stricter single wire spelling and receivers MUST reject differently cased tags rather than silently normalizing them. The schema pattern enforces the AdCP casing profile and extension structure for commonly used tags; conforming receivers additionally validate the complete RFC 5646 grammar and registry rules. Every new AdCP field carrying BCP 47 language identity or a concrete language range MUST reference this schema instead of declaring independent string constraints.").describe("BCP 47 language tag for legacy plain-string and plain-array content in this document. Readers assume en when omitted.").default("en"), "house": z.object({ "domain": z.string().regex(new RegExp("^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$")).describe("The house's domain where brand.json is hosted"), "name": z.string().min(1).describe("Primary display name of the house"), "names": z.array(z.record(z.string(), z.string().min(1)).describe("A localized name using the legacy underscore-separated language/region key convention (for example 'en_US', 'fr_CA', or 'zh_CN') and a name value. Bare language codes such as 'en' are also accepted for backwards compatibility. These keys are not AdCP locale-tag values: a bridge to /schemas/core/locale-tag.json replaces '_' with '-', applies the AdCP canonical wire profile, and validates the result rather than passing the key through unchanged.")).describe("Localized house names including legal name, stock symbol, etc.").optional(), "architecture": z.enum(["branded_house","house_of_brands","hybrid"]).describe("Brand architecture model: branded_house (Google), house_of_brands (P&G), hybrid (Nike)").optional(), "agents": z.array(z.object({ "type": z.enum(["brand","rights","measurement","governance","creative","sales","buying","signals"]).describe("Functional role of this agent"), "url": z.string().url().regex(new RegExp("^https://")).describe("Agent endpoint URL (MCP or A2A). Callers comparing a brand's declared agent URL against another value (e.g., resolving 'is this the agent that signed this artifact?' or matching against a discovery cache) MUST canonicalize both sides per the AdCP URL canonicalization rules, not byte-equality. See docs/reference/url-canonicalization."), "id": z.string().regex(new RegExp("^[a-z0-9_]+$")).max(100).describe("Agent identifier (useful for logging, multi-tenant platforms)"), "description": z.string().max(500).describe("Human-readable description of this agent's capabilities or scope").optional(), "jwks_uri": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL of the agent's JWKS (RFC 7517) containing public keys used to verify artifacts this agent signs or requests it sends. Verified artifacts include signed governance_context tokens (for governance agents) and RFC 9421 HTTP Signatures on outgoing requests (for any agent). When absent, verifiers MUST default to /.well-known/jwks.json on the origin of `url`. Keys are identified by `kid` in the JWS header or RFC 9421 `keyid` parameter; JWKS MAY contain multiple keys to support rotation and per-purpose separation via `key_ops` and `use`.").optional(), "available_uses": z.array(z.enum(["likeness","voice","name","endorsement","motion_capture","signature","catchphrase","sync","background_music","editorial","commercial","ai_generated_image"]).describe("Types of rights usage that can be licensed through the brand protocol. Aligned with DDEX UseType direction for interoperability with music and media rights systems.")).describe("For rights agents: rights uses available for licensing").optional(), "right_types": z.array(z.enum(["talent","character","brand_ip","music","stock_media"]).describe("Categories of intellectual property rights that can be licensed through the brand protocol.")).describe("For rights agents: types of rights available").optional(), "countries": z.array(z.string().regex(new RegExp("^[A-Z]{2}$"))).describe("ISO 3166-1 alpha-2 country codes where this agent operates. Omit for global scope.").optional() }).catchall(z.any()).describe("An agent declared by a brand or house. Each entry identifies one agent endpoint and its functional role in the advertising ecosystem.")).describe("House-level agents that apply to all brands unless overridden at the brand level").optional(), "data_subject_contestation": z.object({ "url": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL to a human-accessible contestation form or information page.").optional(), "email": z.string().email().describe("Email address for contestation requests. Deployer MUST monitor and respond within the timelines set by applicable law (e.g., 1 month under GDPR Art. 12(3)).").optional(), "languages": z.array(z.string()).describe("BCP 47 language tags the contestation channel supports (e.g., ['en', 'de', 'fr']). At minimum SHOULD include a language spoken in every jurisdiction where the brand runs regulated-vertical campaigns.").optional() }).strict().and(z.union([z.any(), z.any()])).describe("House-level fallback contestation contact. Governance agents resolve in order: brand.data_subject_contestation → house.data_subject_contestation → missing (critical finding when human review required).").optional(), "identity_relying_parties": z.array(z.object({ "issuer": z.object({ "domain": z.string().regex(new RegExp("^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$")).describe("Domain where /.well-known/brand.json is hosted, or the brand's operating domain"), "brand_id": z.string().regex(new RegExp("^[a-z0-9_]+$")).describe("Brand identifier within the house portfolio. Optional for single-brand domains.").optional(), "countries": z.array(z.string().regex(new RegExp("^[A-Z]{2}$"))).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Canonical set of ISO 3166-1 alpha-2 countries for this advertiser identity. Omit for a global/default identity. Array order is not meaningful; producers MUST sort codes lexicographically before computing keys or signatures. This qualifies account identity and is not delivery targeting.").optional(), "industries": z.array(z.string()).describe("Inline override for the brand's industries. Useful when the caller cannot modify the brand's canonical brand.json but needs to declare industries for governance (e.g., Annex III vertical detection). brand.json remains the canonical source; when omitted here, governance agents SHOULD resolve from brand.json.").optional(), "data_subject_contestation": z.object({ "url": z.string().url().regex(new RegExp("^https://")).optional(), "email": z.string().email().optional(), "languages": z.array(z.string()).optional() }).strict().and(z.union([z.any(), z.any()])).describe("Inline override for the brand's contestation contact point. Useful when the operator does not control brand.json but needs to discharge Art 22(3) for this plan. brand.json is canonical; when omitted, governance agents resolve brand → house → missing.").optional(), "brand_kit_override": z.object({ "logo": z.object({ "asset_type": z.literal("image").describe("Discriminator identifying this as an image asset. See /schemas/creative/asset-types for the registry."), "url": z.string().url().describe("URL to the image asset"), "width": z.number().int().gte(1).describe("Width in pixels"), "height": z.number().int().gte(1).describe("Height in pixels"), "file_size_bytes": z.number().int().gte(1).describe("Image file size in bytes. Required by agents that advertise a max_file_size_kb constraint.").optional(), "pixel_ratio": z.number().gt(0).describe("Intrinsic pixels per logical render pixel (for example `2` for a 600×500 image intended to render at 300×250). Optional because a validator can infer the ratio when the target format declares logical dimensions. When supplied, it MUST agree with both `width / logical_width` and `height / logical_height`; it is never a substitute for the intrinsic `width` and `height` fields.").optional(), "format": z.string().describe("Image file format (jpg, png, gif, webp, etc.)").optional(), "alt_text": z.string().describe("Alternative text for accessibility").optional(), "provenance": z.object({ "digital_source_type": z.enum(["digital_capture","digital_creation","trained_algorithmic_media","composite_with_trained_algorithmic_media","algorithmic_media","composite_capture","composite_synthetic","human_edits","data_driven_media"]).describe("IPTC-aligned classification of AI involvement in producing this content").optional(), "synthetic_depiction": z.boolean().describe("Assessed declaration of whether the content synthetically depicts a real or fictional person performing or appearing in a way that was generated or materially manipulated rather than captured as depicted. `true` covers both a fully synthetic performer and material manipulation of a real performer; `false` is an assessed declaration that the content does not contain such a depiction. Absence means the content has not been assessed for synthetic depiction. This field does not claim consent, legality, or independent verification, and receivers MUST NOT derive it solely from `digital_source_type`.").optional(), "ai_tool": z.object({ "name": z.string().describe("Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')"), "version": z.string().describe("Version identifier for the AI tool or model (e.g., '25.1', '0125', '2.1'). For generative models, use the model version rather than the API version.").optional(), "provider": z.string().describe("Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')").optional() }).describe("AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.").optional(), "human_oversight": z.enum(["none","prompt_only","selected","edited","directed"]).describe("Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` — the protocol does not derive disclosure obligations from oversight level. Some regulations include carve-outs for human-edited or human-directed AI output, but those carve-outs have factual prerequisites the schema cannot evaluate. Asserting `edited` or `directed` does not by itself justify `disclosure.required: false`.").optional(), "declared_by": z.object({ "agent_url": z.string().url().describe("URL of the agent or service that declared this provenance").optional(), "role": z.enum(["creator","advertiser","agency","platform","tool"]).describe("Role of the declaring party in the supply chain") }).describe("Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.").optional(), "declared_at": z.string().datetime().describe("When this provenance claim was made (ISO 8601). Distinct from created_time, which records when the content itself was produced. A provenance claim may be attached well after content creation, for example when retroactively declaring AI involvement for regulatory compliance.").optional(), "created_time": z.string().datetime().describe("When this content was created or generated (ISO 8601)").optional(), "c2pa": z.object({ "manifest_url": z.string().url().describe("URL to the C2PA manifest store for this content") }).describe("C2PA sidecar manifest reference. Links to a detached cryptographic provenance manifest for this content. Note: file-level C2PA bindings break when ad servers transcode, resize, or re-encode assets. For pipelines with intermediaries, consider embedded_provenance as the primary provenance mechanism.").optional(), "embedded_provenance": z.array(z.object({ "method": z.enum(["manifest_wrapper","provenance_markers"]).describe("How provenance data is carried within the content"), "standard": z.string().describe("Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)").optional(), "provider": z.string().describe("Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context — not a wire identifier."), "verify_agent": z.object({ "agent_url": z.string().url().regex(new RegExp("^https://")).describe("URL of the governance agent the buyer represents was used to embed/verify this layer. MUST use the `https://` scheme and MUST appear in the seller's `creative_policy.accepted_verifiers[].agent_url` list (canonicalized per /docs/reference/url-canonicalization: lowercase scheme and host, strip default port, normalize path dot-segments). Sellers MUST NOT call this URL until the canonicalized match is confirmed."), "feature_id": z.string().describe("Optional `feature_id` the buyer represents the seller should request via `get_creative_features` (e.g., `encypher.markers_present_v2`). SHOULD match the `feature_id` declared on the matching `accepted_verifiers[]` entry, or be omitted to defer the selector to the seller. When the seller's entry pins a `feature_id`, that value wins; when neither side pins, the seller selects from the agent's `governance.creative_features` catalog.").optional() }).strict().describe("Buyer's representation that this embedding can be verified by a governance agent on the seller's `creative_policy.accepted_verifiers` list. The `agent_url` MUST match (canonicalized) one of the seller's published `accepted_verifiers[].agent_url` entries; sellers reject `sync_creatives` submissions whose `verify_agent.agent_url` is off-list with `PROVENANCE_VERIFIER_NOT_ACCEPTED`. This is buyer-supplied evidence, not buyer-driven routing — the seller is the verifier-of-record and the seller controls which agent it actually calls (the seller MAY use a different on-list agent if it determines this is more appropriate; the seller does not call buyer-asserted endpoints outside its allowlist). MAY be omitted for self-verifiable embeddings (e.g., a C2PA text manifest with a public key the seller already trusts).").optional(), "embedded_at": z.string().datetime().describe("When the provenance data was embedded (ISO 8601)").optional() })).describe("Provenance metadata embedded within the content stream. Each entry declares one embedding layer: structured provenance data carried inside the content itself, as distinct from sidecar references (c2pa.manifest_url). Embedded provenance survives operations that break sidecar and file-level bindings: ad-server transcoding, CMS ingestion, copy-paste, reformatting, and CDN re-encoding. For ad-tech pipelines where content passes through multiple intermediaries, embedded provenance is the reliable path for provenance that persists from declaration through delivery. This is a declaration by the embedding party. The receiving party (the seller) is the verifier-of-record: it confirms the claim by calling a governance agent it trusts (typically one published in `creative_policy.accepted_verifiers`).").optional(), "watermarks": z.array(z.object({ "media_type": z.enum(["audio","image","video","text"]).describe("Media category of the watermarked content"), "provider": z.string().describe("Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context — not a wire identifier."), "verify_agent": z.object({ "agent_url": z.string().url().regex(new RegExp("^https://")).describe("URL of the governance agent the buyer represents was used to apply/detect this watermark. MUST use the `https://` scheme and MUST appear in the seller's `creative_policy.accepted_verifiers[].agent_url` list (canonicalized per /docs/reference/url-canonicalization: lowercase scheme and host, strip default port, normalize path dot-segments). Sellers MUST NOT call this URL until the canonicalized match is confirmed."), "feature_id": z.string().describe("Optional `feature_id` the buyer represents the seller should request via `get_creative_features` (e.g., `imatag.watermark_detected`). SHOULD match the `feature_id` declared on the matching `accepted_verifiers[]` entry, or be omitted to defer the selector to the seller. When the seller's entry pins a `feature_id`, that value wins; when neither side pins, the seller selects from the agent's `governance.creative_features` catalog.").optional() }).strict().describe("Buyer's representation that this watermark can be detected by a governance agent on the seller's `creative_policy.accepted_verifiers` list. The `agent_url` MUST match (canonicalized) one of the seller's published `accepted_verifiers[].agent_url` entries; sellers reject `sync_creatives` submissions whose `verify_agent.agent_url` is off-list with `PROVENANCE_VERIFIER_NOT_ACCEPTED`. This is buyer-supplied evidence, not buyer-driven routing — the seller is the verifier-of-record and the seller controls which agent it actually calls (the seller MAY use a different on-list agent if it determines this is more appropriate; the seller does not call buyer-asserted endpoints outside its allowlist).").optional(), "c2pa_action": z.enum(["c2pa.watermarked.bound","c2pa.watermarked.unbound"]).describe("C2PA action classification for this watermark").optional(), "embedded_at": z.string().datetime().describe("When the watermark was applied (ISO 8601)").optional() })).describe("Content watermarks applied to this asset. Each entry declares one watermarking layer: a content modification that encodes an identifier or fingerprint within the asset. Watermarks differ from embedded provenance: a watermark encodes an identifier (who generated it, who owns it), while embedded provenance carries or references a structured provenance record (the full chain of custody). A single asset may carry both. Aligns with C2PA action taxonomy: c2pa.watermarked.bound (watermark linked to a C2PA manifest) and c2pa.watermarked.unbound (watermark independent of any manifest). This is a declaration by the watermarking party. The receiving party (the seller) is the verifier-of-record: it confirms the claim by calling a governance agent it trusts (typically one published in `creative_policy.accepted_verifiers`).").optional(), "disclosure": z.object({ "required": z.boolean().describe("The declaring party's claim that AI disclosure is required for this content under applicable regulations. This is a declared signal carried through the supply chain — useful as a routing and audit input — not a regulatory determination made by the protocol. Receiving parties remain responsible for their own jurisdictional analysis and should not treat `required: false` as compliance cover."), "jurisdictions": z.array(z.object({ "country": z.string().describe("ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')"), "region": z.string().describe("Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)").optional(), "regulation": z.string().describe("Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')"), "label_text": z.string().describe("Required disclosure label text for this jurisdiction, in the local language").optional(), "render_guidance": z.object({ "persistence": z.enum(["continuous","initial","flexible"]).describe("How long the disclosure must persist during content playback or display").optional(), "min_duration_ms": z.number().int().gte(1).describe("Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial — without it, the duration is at the publisher's discretion. At serve time the publisher reads this from provenance since the brief is not available.").optional(), "positions": z.array(z.enum(["prominent","footer","audio","subtitle","overlay","end_card","pre_roll","companion"]).describe("Where a required disclosure should appear within a creative. Used by creative briefs to specify disclosure placement and by formats to declare which positions they can render.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Preferred disclosure positions in priority order. The first position a format supports should be used.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension object for platform-specific, vendor-namespaced parameters. Extensions are always optional and must be namespaced under a vendor/platform key (e.g., ext.gam, ext.roku). Used for custom capabilities, partner-specific configuration, and features being proposed for standardization.").optional() }).describe("How the disclosure should be rendered for this jurisdiction. Expresses the declaring party's intent for persistence and position based on regulatory requirements. Publishers control actual rendering but governance agents can audit whether guidance was followed.").optional() })).describe("Jurisdictions where disclosure obligations apply").optional() }).describe("Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.").optional(), "verification": z.array(z.object({ "verified_by": z.string().describe("Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')"), "verified_time": z.string().datetime().describe("When the verification was performed (ISO 8601)").optional(), "result": z.enum(["authentic","ai_generated","ai_modified","inconclusive"]).describe("Verification outcome"), "confidence": z.number().gte(0).lte(1).describe("Confidence score of the verification result (0.0 to 1.0)").optional(), "details_url": z.string().url().describe("URL to the full verification report").optional() })).describe("Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim — verification results attached by the declaring party are supplementary. The enforcing party (e.g., seller/publisher) should run its own verification via get_creative_features or calibrate_content.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension object for platform-specific, vendor-namespaced parameters. Extensions are always optional and must be namespaced under a vendor/platform key (e.g., ext.gam, ext.roku). Used for custom capabilities, partner-specific configuration, and features being proposed for standardization.").optional() }).describe("Provenance metadata for this asset, overrides manifest-level provenance").optional() }).catchall(z.any()).describe("Override logo asset.").optional(), "colors": z.object({ "primary": z.string().regex(new RegExp("^#[0-9a-fA-F]{6}$")).optional(), "secondary": z.string().regex(new RegExp("^#[0-9a-fA-F]{6}$")).optional(), "accent": z.string().regex(new RegExp("^#[0-9a-fA-F]{6}$")).optional() }).catchall(z.any()).describe("Override brand colors (hex strings).").optional(), "voice": z.string().describe("Override brand-voice description for surface-composed text/audio output.").optional(), "tagline": z.string().describe("Override tagline.").optional() }).catchall(z.any()).describe("Inline override for brand-kit fields normally resolved from `/.well-known/brand.json` on `domain` (logo, colors, voice, tagline). Use when brand.json is missing, stale, or inappropriate for this specific call — e.g., a campaign-scoped tagline, a co-branded creative, a freshly-rebranded color palette the brand.json hasn't shipped yet. Same inline-override pattern as `industries` and `data_subject_contestation` above: brand.json is canonical, the override is per-call. Adopters needing to override fields outside this subset (`voice_attributes`, `prohibited_terms`, etc.) MUST publish a different brand.json and reference it via a different `domain` — the inline override is intentionally narrow to a small high-traffic subset.\n\n**Merge semantics (normative).** The merge is **field-level**, not whole-object replacement. Each field within `brand_kit_override` (`logo`, `colors`, `voice`, `tagline`) is evaluated independently — when a field is present on the override the override value applies; when a field is absent the brand.json value applies (or is absent if brand.json doesn't carry one either). For composite fields (`colors.primary`, `colors.secondary`, `colors.accent`), the merge is one level deeper: each color slot is evaluated independently — a producer can override `colors.primary` while still inheriting `colors.secondary` from brand.json. SDKs MUST NOT treat a present `brand_kit_override.colors` as wiping the brand.json `colors` block entirely; only the per-slot fields present in the override take precedence. Without this rule, a partial-override semantics would diverge across SDKs and produce inconsistent rendering for the same payload.").optional() }).strict().describe("Identity issuer / attestation authority, referenced as a vendor BrandRef (e.g. {\"domain\": \"world.org\"}) — the same vendor-reference shape AdCP uses for measurement and signals vendors. The issuer's canonical domain is the anchor; it need not host a brand.json, but if it does, that is where its verifier metadata (scheme versions, verify endpoint, JWKs) lives. Issuer-agnostic: World ID, ISO 18013-5 mDL, and W3C-VC issuers all reference by domain. The relying party is namespaced by the issuer — identity is the tuple (issuer.domain, issuer.brand_id, relying_party_id), mirroring (vendor.domain, vendor.brand_id, metric_id)."), "scheme": z.string().describe("Proof scheme and version, e.g. \"world_id_v4\".").optional(), "relying_party_id": z.string().describe("The relying-party id registered with the issuer (and, for on-chain issuers like World ID, with the issuer's registry). One entity may operate many relying parties (scope=entity vs scope=property); this is the attestation's audience / linkability boundary, not an entity identifier."), "scope": z.enum(["entity","property"]).describe("Whether this relying_party_id is shared across the entity's properties (entity → a within-entity unique-human graph) or scoped to a single property (property → per-property pseudonyms, unlinkable across the entity).").optional() }).strict().describe("A verified-identity relying party an entity or brand operates. Used for attestation provenance in TMP Identity Match (the buyer checks a forwarded attestation's relying_party_id against the owner's published list). Issuer-agnostic; World ID is the first issuer.")).describe("Verified-identity relying parties this house/entity operates, so a buyer can verify that a forwarded TMP identity attestation's relying_party_id genuinely belongs to this entity (provenance) rather than being replayed under another owner. For network-as-RP, the network publishes its own relying_party_id here. See specs/tmp-verified-identity-attestation.md.").optional() }).catchall(z.any()).describe("Corporate or organizational entity that owns brands"), "brands": z.array(z.object({ "id": z.string().regex(new RegExp("^[a-z0-9_]+$")).describe("Brand identifier within the house. House chooses this ID."), "url": z.string().url().describe("Primary brand URL for context and asset discovery").optional(), "identity_relying_parties": z.array(z.object({ "issuer": z.object({ "domain": z.string().regex(new RegExp("^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$")).describe("Domain where /.well-known/brand.json is hosted, or the brand's operating domain"), "brand_id": z.string().regex(new RegExp("^[a-z0-9_]+$")).describe("Brand identifier within the house portfolio. Optional for single-brand domains.").optional(), "countries": z.array(z.string().regex(new RegExp("^[A-Z]{2}$"))).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Canonical set of ISO 3166-1 alpha-2 countries for this advertiser identity. Omit for a global/default identity. Array order is not meaningful; producers MUST sort codes lexicographically before computing keys or signatures. This qualifies account identity and is not delivery targeting.").optional(), "industries": z.array(z.string()).describe("Inline override for the brand's industries. Useful when the caller cannot modify the brand's canonical brand.json but needs to declare industries for governance (e.g., Annex III vertical detection). brand.json remains the canonical source; when omitted here, governance agents SHOULD resolve from brand.json.").optional(), "data_subject_contestation": z.object({ "url": z.string().url().regex(new RegExp("^https://")).optional(), "email": z.string().email().optional(), "languages": z.array(z.string()).optional() }).strict().and(z.union([z.any(), z.any()])).describe("Inline override for the brand's contestation contact point. Useful when the operator does not control brand.json but needs to discharge Art 22(3) for this plan. brand.json is canonical; when omitted, governance agents resolve brand → house → missing.").optional(), "brand_kit_override": z.object({ "logo": z.object({ "asset_type": z.literal("image").describe("Discriminator identifying this as an image asset. See /schemas/creative/asset-types for the registry."), "url": z.string().url().describe("URL to the image asset"), "width": z.number().int().gte(1).describe("Width in pixels"), "height": z.number().int().gte(1).describe("Height in pixels"), "file_size_bytes": z.number().int().gte(1).describe("Image file size in bytes. Required by agents that advertise a max_file_size_kb constraint.").optional(), "pixel_ratio": z.number().gt(0).describe("Intrinsic pixels per logical render pixel (for example `2` for a 600×500 image intended to render at 300×250). Optional because a validator can infer the ratio when the target format declares logical dimensions. When supplied, it MUST agree with both `width / logical_width` and `height / logical_height`; it is never a substitute for the intrinsic `width` and `height` fields.").optional(), "format": z.string().describe("Image file format (jpg, png, gif, webp, etc.)").optional(), "alt_text": z.string().describe("Alternative text for accessibility").optional(), "provenance": z.object({ "digital_source_type": z.enum(["digital_capture","digital_creation","trained_algorithmic_media","composite_with_trained_algorithmic_media","algorithmic_media","composite_capture","composite_synthetic","human_edits","data_driven_media"]).describe("IPTC-aligned classification of AI involvement in producing this content").optional(), "synthetic_depiction": z.boolean().describe("Assessed declaration of whether the content synthetically depicts a real or fictional person performing or appearing in a way that was generated or materially manipulated rather than captured as depicted. `true` covers both a fully synthetic performer and material manipulation of a real performer; `false` is an assessed declaration that the content does not contain such a depiction. Absence means the content has not been assessed for synthetic depiction. This field does not claim consent, legality, or independent verification, and receivers MUST NOT derive it solely from `digital_source_type`.").optional(), "ai_tool": z.object({ "name": z.string().describe("Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')"), "version": z.string().describe("Version identifier for the AI tool or model (e.g., '25.1', '0125', '2.1'). For generative models, use the model version rather than the API version.").optional(), "provider": z.string().describe("Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')").optional() }).describe("AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.").optional(), "human_oversight": z.enum(["none","prompt_only","selected","edited","directed"]).describe("Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` — the protocol does not derive disclosure obligations from oversight level. Some regulations include carve-outs for human-edited or human-directed AI output, but those carve-outs have factual prerequisites the schema cannot evaluate. Asserting `edited` or `directed` does not by itself justify `disclosure.required: false`.").optional(), "declared_by": z.object({ "agent_url": z.string().url().describe("URL of the agent or service that declared this provenance").optional(), "role": z.enum(["creator","advertiser","agency","platform","tool"]).describe("Role of the declaring party in the supply chain") }).describe("Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.").optional(), "declared_at": z.string().datetime().describe("When this provenance claim was made (ISO 8601). Distinct from created_time, which records when the content itself was produced. A provenance claim may be attached well after content creation, for example when retroactively declaring AI involvement for regulatory compliance.").optional(), "created_time": z.string().datetime().describe("When this content was created or generated (ISO 8601)").optional(), "c2pa": z.object({ "manifest_url": z.string().url().describe("URL to the C2PA manifest store for this content") }).describe("C2PA sidecar manifest reference. Links to a detached cryptographic provenance manifest for this content. Note: file-level C2PA bindings break when ad servers transcode, resize, or re-encode assets. For pipelines with intermediaries, consider embedded_provenance as the primary provenance mechanism.").optional(), "embedded_provenance": z.array(z.object({ "method": z.enum(["manifest_wrapper","provenance_markers"]).describe("How provenance data is carried within the content"), "standard": z.string().describe("Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)").optional(), "provider": z.string().describe("Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context — not a wire identifier."), "verify_agent": z.object({ "agent_url": z.string().url().regex(new RegExp("^https://")).describe("URL of the governance agent the buyer represents was used to embed/verify this layer. MUST use the `https://` scheme and MUST appear in the seller's `creative_policy.accepted_verifiers[].agent_url` list (canonicalized per /docs/reference/url-canonicalization: lowercase scheme and host, strip default port, normalize path dot-segments). Sellers MUST NOT call this URL until the canonicalized match is confirmed."), "feature_id": z.string().describe("Optional `feature_id` the buyer represents the seller should request via `get_creative_features` (e.g., `encypher.markers_present_v2`). SHOULD match the `feature_id` declared on the matching `accepted_verifiers[]` entry, or be omitted to defer the selector to the seller. When the seller's entry pins a `feature_id`, that value wins; when neither side pins, the seller selects from the agent's `governance.creative_features` catalog.").optional() }).strict().describe("Buyer's representation that this embedding can be verified by a governance agent on the seller's `creative_policy.accepted_verifiers` list. The `agent_url` MUST match (canonicalized) one of the seller's published `accepted_verifiers[].agent_url` entries; sellers reject `sync_creatives` submissions whose `verify_agent.agent_url` is off-list with `PROVENANCE_VERIFIER_NOT_ACCEPTED`. This is buyer-supplied evidence, not buyer-driven routing — the seller is the verifier-of-record and the seller controls which agent it actually calls (the seller MAY use a different on-list agent if it determines this is more appropriate; the seller does not call buyer-asserted endpoints outside its allowlist). MAY be omitted for self-verifiable embeddings (e.g., a C2PA text manifest with a public key the seller already trusts).").optional(), "embedded_at": z.string().datetime().describe("When the provenance data was embedded (ISO 8601)").optional() })).describe("Provenance metadata embedded within the content stream. Each entry declares one embedding layer: structured provenance data carried inside the content itself, as distinct from sidecar references (c2pa.manifest_url). Embedded provenance survives operations that break sidecar and file-level bindings: ad-server transcoding, CMS ingestion, copy-paste, reformatting, and CDN re-encoding. For ad-tech pipelines where content passes through multiple intermediaries, embedded provenance is the reliable path for provenance that persists from declaration through delivery. This is a declaration by the embedding party. The receiving party (the seller) is the verifier-of-record: it confirms the claim by calling a governance agent it trusts (typically one published in `creative_policy.accepted_verifiers`).").optional(), "watermarks": z.array(z.object({ "media_type": z.enum(["audio","image","video","text"]).describe("Media category of the watermarked content"), "provider": z.string().describe("Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context — not a wire identifier."), "verify_agent": z.object({ "agent_url": z.string().url().regex(new RegExp("^https://")).describe("URL of the governance agent the buyer represents was used to apply/detect this watermark. MUST use the `https://` scheme and MUST appear in the seller's `creative_policy.accepted_verifiers[].agent_url` list (canonicalized per /docs/reference/url-canonicalization: lowercase scheme and host, strip default port, normalize path dot-segments). Sellers MUST NOT call this URL until the canonicalized match is confirmed."), "feature_id": z.string().describe("Optional `feature_id` the buyer represents the seller should request via `get_creative_features` (e.g., `imatag.watermark_detected`). SHOULD match the `feature_id` declared on the matching `accepted_verifiers[]` entry, or be omitted to defer the selector to the seller. When the seller's entry pins a `feature_id`, that value wins; when neither side pins, the seller selects from the agent's `governance.creative_features` catalog.").optional() }).strict().describe("Buyer's representation that this watermark can be detected by a governance agent on the seller's `creative_policy.accepted_verifiers` list. The `agent_url` MUST match (canonicalized) one of the seller's published `accepted_verifiers[].agent_url` entries; sellers reject `sync_creatives` submissions whose `verify_agent.agent_url` is off-list with `PROVENANCE_VERIFIER_NOT_ACCEPTED`. This is buyer-supplied evidence, not buyer-driven routing — the seller is the verifier-of-record and the seller controls which agent it actually calls (the seller MAY use a different on-list agent if it determines this is more appropriate; the seller does not call buyer-asserted endpoints outside its allowlist).").optional(), "c2pa_action": z.enum(["c2pa.watermarked.bound","c2pa.watermarked.unbound"]).describe("C2PA action classification for this watermark").optional(), "embedded_at": z.string().datetime().describe("When the watermark was applied (ISO 8601)").optional() })).describe("Content watermarks applied to this asset. Each entry declares one watermarking layer: a content modification that encodes an identifier or fingerprint within the asset. Watermarks differ from embedded provenance: a watermark encodes an identifier (who generated it, who owns it), while embedded provenance carries or references a structured provenance record (the full chain of custody). A single asset may carry both. Aligns with C2PA action taxonomy: c2pa.watermarked.bound (watermark linked to a C2PA manifest) and c2pa.watermarked.unbound (watermark independent of any manifest). This is a declaration by the watermarking party. The receiving party (the seller) is the verifier-of-record: it confirms the claim by calling a governance agent it trusts (typically one published in `creative_policy.accepted_verifiers`).").optional(), "disclosure": z.object({ "required": z.boolean().describe("The declaring party's claim that AI disclosure is required for this content under applicable regulations. This is a declared signal carried through the supply chain — useful as a routing and audit input — not a regulatory determination made by the protocol. Receiving parties remain responsible for their own jurisdictional analysis and should not treat `required: false` as compliance cover."), "jurisdictions": z.array(z.object({ "country": z.string().describe("ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')"), "region": z.string().describe("Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)").optional(), "regulation": z.string().describe("Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')"), "label_text": z.string().describe("Required disclosure label text for this jurisdiction, in the local language").optional(), "render_guidance": z.object({ "persistence": z.enum(["continuous","initial","flexible"]).describe("How long the disclosure must persist during content playback or display").optional(), "min_duration_ms": z.number().int().gte(1).describe("Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial — without it, the duration is at the publisher's discretion. At serve time the publisher reads this from provenance since the brief is not available.").optional(), "positions": z.array(z.enum(["prominent","footer","audio","subtitle","overlay","end_card","pre_roll","companion"]).describe("Where a required disclosure should appear within a creative. Used by creative briefs to specify disclosure placement and by formats to declare which positions they can render.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Preferred disclosure positions in priority order. The first position a format supports should be used.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension object for platform-specific, vendor-namespaced parameters. Extensions are always optional and must be namespaced under a vendor/platform key (e.g., ext.gam, ext.roku). Used for custom capabilities, partner-specific configuration, and features being proposed for standardization.").optional() }).describe("How the disclosure should be rendered for this jurisdiction. Expresses the declaring party's intent for persistence and position based on regulatory requirements. Publishers control actual rendering but governance agents can audit whether guidance was followed.").optional() })).describe("Jurisdictions where disclosure obligations apply").optional() }).describe("Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.").optional(), "verification": z.array(z.object({ "verified_by": z.string().describe("Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')"), "verified_time": z.string().datetime().describe("When the verification was performed (ISO 8601)").optional(), "result": z.enum(["authentic","ai_generated","ai_modified","inconclusive"]).describe("Verification outcome"), "confidence": z.number().gte(0).lte(1).describe("Confidence score of the verification result (0.0 to 1.0)").optional(), "details_url": z.string().url().describe("URL to the full verification report").optional() })).describe("Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim — verification results attached by the declaring party are supplementary. The enforcing party (e.g., seller/publisher) should run its own verification via get_creative_features or calibrate_content.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension object for platform-specific, vendor-namespaced parameters. Extensions are always optional and must be namespaced under a vendor/platform key (e.g., ext.gam, ext.roku). Used for custom capabilities, partner-specific configuration, and features being proposed for standardization.").optional() }).describe("Provenance metadata for this asset, overrides manifest-level provenance").optional() }).catchall(z.any()).describe("Override logo asset.").optional(), "colors": z.object({ "primary": z.string().regex(new RegExp("^#[0-9a-fA-F]{6}$")).optional(), "secondary": z.string().regex(new RegExp("^#[0-9a-fA-F]{6}$")).optional(), "accent": z.string().regex(new RegExp("^#[0-9a-fA-F]{6}$")).optional() }).catchall(z.any()).describe("Override brand colors (hex strings).").optional(), "voice": z.string().describe("Override brand-voice description for surface-composed text/audio output.").optional(), "tagline": z.string().describe("Override tagline.").optional() }).catchall(z.any()).describe("Inline override for brand-kit fields normally resolved from `/.well-known/brand.json` on `domain` (logo, colors, voice, tagline). Use when brand.json is missing, stale, or inappropriate for this specific call — e.g., a campaign-scoped tagline, a co-branded creative, a freshly-rebranded color palette the brand.json hasn't shipped yet. Same inline-override pattern as `industries` and `data_subject_contestation` above: brand.json is canonical, the override is per-call. Adopters needing to override fields outside this subset (`voice_attributes`, `prohibited_terms`, etc.) MUST publish a different brand.json and reference it via a different `domain` — the inline override is intentionally narrow to a small high-traffic subset.\n\n**Merge semantics (normative).** The merge is **field-level**, not whole-object replacement. Each field within `brand_kit_override` (`logo`, `colors`, `voice`, `tagline`) is evaluated independently — when a field is present on the override the override value applies; when a field is absent the brand.json value applies (or is absent if brand.json doesn't carry one either). For composite fields (`colors.primary`, `colors.secondary`, `colors.accent`), the merge is one level deeper: each color slot is evaluated independently — a producer can override `colors.primary` while still inheriting `colors.secondary` from brand.json. SDKs MUST NOT treat a present `brand_kit_override.colors` as wiping the brand.json `colors` block entirely; only the per-slot fields present in the override take precedence. Without this rule, a partial-override semantics would diverge across SDKs and produce inconsistent rendering for the same payload.").optional() }).strict().describe("Identity issuer / attestation authority, referenced as a vendor BrandRef (e.g. {\"domain\": \"world.org\"}) — the same vendor-reference shape AdCP uses for measurement and signals vendors. The issuer's canonical domain is the anchor; it need not host a brand.json, but if it does, that is where its verifier metadata (scheme versions, verify endpoint, JWKs) lives. Issuer-agnostic: World ID, ISO 18013-5 mDL, and W3C-VC issuers all reference by domain. The relying party is namespaced by the issuer — identity is the tuple (issuer.domain, issuer.brand_id, relying_party_id), mirroring (vendor.domain, vendor.brand_id, metric_id)."), "scheme": z.string().describe("Proof scheme and version, e.g. \"world_id_v4\".").optional(), "relying_party_id": z.string().describe("The relying-party id registered with the issuer (and, for on-chain issuers like World ID, with the issuer's registry). One entity may operate many relying parties (scope=entity vs scope=property); this is the attestation's audience / linkability boundary, not an entity identifier."), "scope": z.enum(["entity","property"]).describe("Whether this relying_party_id is shared across the entity's properties (entity → a within-entity unique-human graph) or scoped to a single property (property → per-property pseudonyms, unlinkable across the entity).").optional() }).strict().describe("A verified-identity relying party an entity or brand operates. Used for attestation provenance in TMP Identity Match (the buyer checks a forwarded attestation's relying_party_id against the owner's published list). Issuer-agnostic; World ID is the first issuer.")).describe("Verified-identity relying parties scoped to this brand/property, for attestation provenance in TMP Identity Match. Use when a brand or property runs its own relying_party_id (per-property pseudonyms); entity-wide relying parties live on the house object. See specs/tmp-verified-identity-attestation.md.").optional(), "names": z.array(z.record(z.string(), z.string().min(1)).describe("A localized name using the legacy underscore-separated language/region key convention (for example 'en_US', 'fr_CA', or 'zh_CN') and a name value. Bare language codes such as 'en' are also accepted for backwards compatibility. These keys are not AdCP locale-tag values: a bridge to /schemas/core/locale-tag.json replaces '_' with '-', applies the AdCP canonical wire profile, and validates the result rather than passing the key through unchanged.")).describe("Localized brand names. Multiple entries per language allowed for aliases."), "keller_type": z.enum(["master","sub_brand","endorsed","independent"]).describe("Brand architecture type from Keller's theory. master: primary brand of house. sub_brand: carries parent name (Nike SB). endorsed: independent identity backed by parent (Air Jordan 'by Nike'). independent: operates separately (Converse under Nike, Inc.)").optional(), "parent_brand": z.string().regex(new RegExp("^[a-z0-9_]+$")).describe("Parent brand ID for sub-brands and endorsed brands").optional(), "description": z.string().describe("Brand description").optional(), "industries": z.array(z.string()).describe("Brand industries (e.g., ['automotive'] or ['pharmaceutical', 'cpg'] for a consumer health company). Describes what the company does — not what regulatory regimes apply (use policy_categories for that).").optional(), "target_audience": z.string().describe("Primary target audience").optional(), "logos": z.array(z.object({ "id": z.string().regex(new RegExp("^[a-z0-9][a-z0-9_-]*$")).describe("Stable identifier for this logo entry. Recommended when logo usage rules or mark lockups need to bind to a specific logo asset.").optional(), "url": z.string().url().describe("URL to the logo asset"), "orientation": z.enum(["square","horizontal","vertical","stacked"]).describe("Logo aspect ratio orientation. square: ~1:1, horizontal: wide, vertical: tall, stacked: vertically arranged elements").optional(), "background": z.enum(["dark-bg","light-bg","transparent-bg"]).describe("Background compatibility. dark-bg: use on dark backgrounds, light-bg: use on light backgrounds, transparent-bg: has transparent background").optional(), "variant": z.enum(["primary","secondary","icon","wordmark","full-lockup"]).describe("Logo variant type. primary: main logo, secondary: alternative, icon: symbol only, wordmark: text only, full-lockup: complete logo").optional(), "tags": z.array(z.string()).describe("Additional semantic tags for custom categorization beyond the standard orientation, background, and variant fields").optional(), "slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Canonical renderer slots where this logo is appropriate. Consumers SHOULD prefer this over inferring from tags or usage prose when selecting a logo for a specific UI surface.").optional(), "usage": z.string().describe("Human-readable description of when to use this logo variant (e.g., 'Primary logo for use on light backgrounds')").optional(), "width": z.number().int().describe("Width in pixels").optional(), "height": z.number().int().describe("Height in pixels").optional() }).catchall(z.any()).describe("Brand logo asset with structured fields for orientation, background compatibility, and variant type")).describe("Brand logo assets").optional(), "colors": z.object({ "primary": z.union([z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")), z.array(z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")))]).optional(), "secondary": z.union([z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")), z.array(z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")))]).optional(), "accent": z.union([z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")), z.array(z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")))]).optional(), "background": z.union([z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")), z.array(z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")))]).optional(), "text": z.union([z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")), z.array(z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")))]).optional(), "heading": z.union([z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")), z.array(z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")))]).optional(), "body": z.union([z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")), z.array(z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")))]).optional(), "label": z.union([z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")), z.array(z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")))]).optional(), "border": z.union([z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")), z.array(z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")))]).optional(), "divider": z.union([z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")), z.array(z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")))]).optional(), "surface_1": z.union([z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")), z.array(z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")))]).optional(), "surface_2": z.union([z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")), z.array(z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")))]).optional() }).catchall(z.union([z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")), z.array(z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")))])).describe("Brand color palette. Each role accepts a single hex color or an array of hex colors for brands with multiple values per role. Beyond the core five roles, brands can provide additional color roles for finer granularity — heading, body, label, border, divider, surface_1, surface_2, etc.").optional(), "fonts": z.object({ "primary": z.union([z.string().describe("CSS font-family name (e.g., 'Montserrat', 'Arial, sans-serif')"), z.object({ "family": z.string().describe("CSS font-family name (e.g., 'Brand Sans')"), "files": z.array(z.object({ "url": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL to the font file (WOFF2, TTF, or OTF)"), "weight": z.number().int().gte(100).lte(900).describe("CSS numeric font-weight for static fonts (100-900)").optional(), "weight_range": z.array(z.number().int().gte(100).lte(900)).max(2).describe("Variable font weight axis range as [min, max] (e.g., [100, 900]). Use instead of weight for variable fonts.").optional(), "style": z.enum(["normal","italic","oblique"]).describe("CSS font-style").optional() }).catchall(z.any()).describe("A font file with weight, style, and variable font metadata")).max(36).describe("Font files for different weights and styles").optional(), "opentype_features": z.array(z.string().regex(new RegExp("^[a-z0-9]{4}$"))).max(20).describe("OpenType feature tags to enable (e.g., ['ss01', 'tnum', 'cv01']). These are four-character tags per the OpenType spec.").optional(), "fallbacks": z.array(z.string().max(100)).max(10).describe("Ordered fallback font-family names for when the primary font is unavailable or does not support the required script (e.g., ['Noto Sans Arabic', 'Noto Sans SC', 'sans-serif'])").optional() }).catchall(z.any()).describe("Structured font with family name, downloadable font files, and typographic metadata")]).describe("Primary font family").optional(), "secondary": z.union([z.string().describe("CSS font-family name (e.g., 'Montserrat', 'Arial, sans-serif')"), z.object({ "family": z.string().describe("CSS font-family name (e.g., 'Brand Sans')"), "files": z.array(z.object({ "url": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL to the font file (WOFF2, TTF, or OTF)"), "weight": z.number().int().gte(100).lte(900).describe("CSS numeric font-weight for static fonts (100-900)").optional(), "weight_range": z.array(z.number().int().gte(100).lte(900)).max(2).describe("Variable font weight axis range as [min, max] (e.g., [100, 900]). Use instead of weight for variable fonts.").optional(), "style": z.enum(["normal","italic","oblique"]).describe("CSS font-style").optional() }).catchall(z.any()).describe("A font file with weight, style, and variable font metadata")).max(36).describe("Font files for different weights and styles").optional(), "opentype_features": z.array(z.string().regex(new RegExp("^[a-z0-9]{4}$"))).max(20).describe("OpenType feature tags to enable (e.g., ['ss01', 'tnum', 'cv01']). These are four-character tags per the OpenType spec.").optional(), "fallbacks": z.array(z.string().max(100)).max(10).describe("Ordered fallback font-family names for when the primary font is unavailable or does not support the required script (e.g., ['Noto Sans Arabic', 'Noto Sans SC', 'sans-serif'])").optional() }).catchall(z.any()).describe("Structured font with family name, downloadable font files, and typographic metadata")]).describe("Secondary font family").optional() }).catchall(z.union([z.string().describe("CSS font-family name (e.g., 'Montserrat', 'Arial, sans-serif')"), z.object({ "family": z.string().describe("CSS font-family name (e.g., 'Brand Sans')"), "files": z.array(z.object({ "url": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL to the font file (WOFF2, TTF, or OTF)"), "weight": z.number().int().gte(100).lte(900).describe("CSS numeric font-weight for static fonts (100-900)").optional(), "weight_range": z.array(z.number().int().gte(100).lte(900)).max(2).describe("Variable font weight axis range as [min, max] (e.g., [100, 900]). Use instead of weight for variable fonts.").optional(), "style": z.enum(["normal","italic","oblique"]).describe("CSS font-style").optional() }).catchall(z.any()).describe("A font file with weight, style, and variable font metadata")).max(36).describe("Font files for different weights and styles").optional(), "opentype_features": z.array(z.string().regex(new RegExp("^[a-z0-9]{4}$"))).max(20).describe("OpenType feature tags to enable (e.g., ['ss01', 'tnum', 'cv01']). These are four-character tags per the OpenType spec.").optional(), "fallbacks": z.array(z.string().max(100)).max(10).describe("Ordered fallback font-family names for when the primary font is unavailable or does not support the required script (e.g., ['Noto Sans Arabic', 'Noto Sans SC', 'sans-serif'])").optional() }).catchall(z.any()).describe("Structured font with family name, downloadable font files, and typographic metadata")]).describe("A font role entry. Either a CSS font-family string (simple) or a structured object with family name and font files (rich).")).describe("Brand typography. Each key is a role name (e.g., 'primary', 'secondary') referenced by type_scale entries. Values are either a CSS font-family string or a structured object with font files for reliable resolution.").optional(), "tone": z.union([z.string().describe("Simple tone descriptors for backwards compatibility"), z.object({ "voice": z.union([z.string(), z.array(z.record(z.string(), z.string().min(1)).describe("A localized name using the legacy underscore-separated language/region key convention (for example 'en_US', 'fr_CA', or 'zh_CN') and a name value. Bare language codes such as 'en' are also accepted for backwards compatibility. These keys are not AdCP locale-tag values: a bridge to /schemas/core/locale-tag.json replaces '_' with '-', applies the AdCP canonical wire profile, and validates the result rather than passing the key through unchanged."))]).describe("High-level voice descriptor, either a legacy plain string or localized values.").optional(), "attributes": z.union([z.array(z.string()), z.array(z.record(z.string(), z.array(z.string())).describe("One locale mapped to a complete ordered string list. The list is selected as a unit so readers never assemble mixed-language output from independently localized elements."))]).describe("Personality traits that characterize the brand voice. Localized lists are selected as a whole.").optional(), "dos": z.union([z.array(z.string()), z.array(z.record(z.string(), z.array(z.string())).describe("One locale mapped to a complete ordered string list. The list is selected as a unit so readers never assemble mixed-language output from independently localized elements."))]).describe("Guidance for copy generation - what TO do. Localized lists are selected as a whole.").optional(), "donts": z.union([z.array(z.string()), z.array(z.record(z.string(), z.array(z.string())).describe("One locale mapped to a complete ordered string list. The list is selected as a unit so readers never assemble mixed-language output from independently localized elements."))]).describe("Guardrails to avoid brand violations - what NOT to do. Localized lists are selected as a whole.").optional() }).describe("Structured brand voice guidelines")]).describe("Brand voice and messaging tone guidelines").optional(), "tagline": z.union([z.string().describe("Plain tagline string for backwards compatibility"), z.array(z.record(z.string(), z.string().min(1)).describe("A localized name using the legacy underscore-separated language/region key convention (for example 'en_US', 'fr_CA', or 'zh_CN') and a name value. Bare language codes such as 'en' are also accepted for backwards compatibility. These keys are not AdCP locale-tag values: a bridge to /schemas/core/locale-tag.json replaces '_' with '-', applies the AdCP canonical wire profile, and validates the result rather than passing the key through unchanged.")).describe("Localized taglines with BCP 47 locale codes")]).describe("Brand tagline or slogan. Accepts a plain string or a localized array matching the names pattern.").optional(), "assets": z.array(z.object({ "asset_id": z.string().describe("Unique identifier"), "asset_type": z.enum(["image","video","audio","text","markdown","html","css","javascript","zip","vast","daast","url","webhook","brief","catalog","published_post"]).describe("Type of asset content"), "url": z.string().url().describe("URL to CDN-hosted asset file"), "tags": z.array(z.string()).describe("Tags for discovery (e.g., 'hero', 'lifestyle', 'product', 'holiday')").optional(), "name": z.union([z.string(), z.array(z.record(z.string(), z.string().min(1)).describe("A localized name using the legacy underscore-separated language/region key convention (for example 'en_US', 'fr_CA', or 'zh_CN') and a name value. Bare language codes such as 'en' are also accepted for backwards compatibility. These keys are not AdCP locale-tag values: a bridge to /schemas/core/locale-tag.json replaces '_' with '-', applies the AdCP canonical wire profile, and validates the result rather than passing the key through unchanged."))]).describe("Human-readable name, either a legacy plain string or localized values.").optional(), "description": z.union([z.string(), z.array(z.record(z.string(), z.string().min(1)).describe("A localized name using the legacy underscore-separated language/region key convention (for example 'en_US', 'fr_CA', or 'zh_CN') and a name value. Bare language codes such as 'en' are also accepted for backwards compatibility. These keys are not AdCP locale-tag values: a bridge to /schemas/core/locale-tag.json replaces '_' with '-', applies the AdCP canonical wire profile, and validates the result rather than passing the key through unchanged."))]).describe("Asset description or usage notes, either a legacy plain string or localized values.").optional(), "width": z.number().int().describe("Image/video width in pixels").optional(), "height": z.number().int().describe("Image/video height in pixels").optional(), "duration_seconds": z.number().describe("Video/audio duration in seconds").optional(), "file_size_bytes": z.number().int().describe("File size in bytes").optional(), "format": z.string().describe("File format (e.g., 'jpg', 'mp4', 'mp3')").optional(), "metadata": z.record(z.string(), z.any()).describe("Additional asset-specific metadata").optional() }).catchall(z.any()).describe("Brand asset (image, video, audio, text)")).describe("Brand asset library").optional(), "properties": z.array(z.object({ "type": z.enum(["website","mobile_app","ctv_app","desktop_app","dooh","podcast","radio","linear_tv","streaming_audio","ai_assistant"]).describe("Property type"), "identifier": z.string().min(1).describe("Property identifier - domain for websites, bundle ID for apps"), "store": z.enum(["apple","google","amazon","roku","samsung","lg","other"]).describe("App store for mobile/CTV apps").optional(), "region": z.string().regex(new RegExp("^([A-Z]{2}|global)$")).describe("ISO 3166-1 alpha-2 country code or 'global'").optional(), "primary": z.boolean().describe("Whether this is the primary property for the brand").default(false), "relationship": z.enum(["owned","direct","delegated","ad_network"]).describe("How this brand relates to the property. 'owned': the brand owns and operates this property (default) and has no adagents.json delegation_type counterpart. 'direct': the brand is the direct sales path for this property, even if a third party operates the software (e.g., a publisher's in-house ad team using a vendor's tech). 'delegated': the brand manages monetization for this property — they are in charge of ad sales (e.g., Mediavine managing a food blog). 'ad_network': the brand sells this property's inventory as part of a network or exchange — they are a path to the inventory, not the path (e.g., PubMatic as an SSP). For non-owned properties, the publisher confirms the relationship by setting the matching delegation_type on the agent's authorization in their adagents.json.").default("owned") }).catchall(z.any()).describe("A digital property associated with a brand. Defaults to owned; use 'relationship' to declare direct, delegated, or ad_network properties. For delegated and network paths, these values match the delegation_type field in adagents.json, creating a bilateral verification chain: the operator declares the relationship here, the publisher confirms by setting the same delegation_type on the agent's authorization in their adagents.json. 'owned' is an inline ownership declaration with no adagents.json counterpart.")).describe("Digital properties associated with this brand — owned, managed, or represented").optional(), "product_catalog": z.object({ "feed_url": z.string().url().describe("URL to product catalog feed"), "feed_format": z.enum(["google_merchant_center","facebook_catalog","shopify","linkedin_jobs","tiktok_shop","pinterest_catalog","openai_product_feed","custom"]).describe("Format of the product feed").optional(), "categories": z.array(z.string()).describe("Product categories available in the catalog").optional(), "last_updated": z.string().datetime().describe("When the product catalog was last updated").optional(), "update_frequency": z.enum(["realtime","hourly","daily","weekly"]).describe("How frequently the product catalog is updated").optional(), "agentic_checkout": z.object({ "endpoint": z.string().url().describe("Base URL for checkout session API"), "spec": z.string().describe("Checkout API specification identifier. Use a namespaced string to identify the checkout protocol (e.g., vendor-prefixed or custom). Vendor-specific values belong under ext.{vendor}."), "supported_payment_providers": z.array(z.string()).describe("Payment providers supported by this checkout endpoint").optional() }).describe("Agentic checkout endpoint configuration").optional() }).catchall(z.any()).describe("Product catalog for e-commerce brands").optional(), "privacy_policy_url": z.string().url().describe("URL to the brand's privacy policy").optional(), "data_subject_contestation": z.object({ "url": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL to a human-accessible contestation form or information page.").optional(), "email": z.string().email().describe("Email address for contestation requests. Deployer MUST monitor and respond within the timelines set by applicable law (e.g., 1 month under GDPR Art. 12(3)).").optional(), "languages": z.array(z.string()).describe("BCP 47 language tags the contestation channel supports (e.g., ['en', 'de', 'fr']). At minimum SHOULD include a language spoken in every jurisdiction where the brand runs regulated-vertical campaigns.").optional() }).strict().and(z.union([z.any(), z.any()])).describe("Contact point where a data subject can request human intervention, express their view, or contest an automated decision — satisfying GDPR Article 22(3) and EU AI Act Article 26(11) transparency obligations. This is a contact reference (URL, email, or both), not a machine-callable API. AdCP surfaces the pointer; the deployer runs the contestation workflow.").optional(), "disclaimers": z.array(z.object({ "text": z.string(), "context": z.string().optional(), "required": z.boolean().default(true) })).describe("Legal disclaimers for creatives").optional(), "trademarks": z.array(z.object({ "registry": z.string().describe("Trademark registry (e.g., 'USPTO', 'EUIPO', 'JPO', 'CNIPA')"), "number": z.string().describe("Registration number as issued by the registry"), "mark": z.string().describe("The registered mark as published"), "status": z.enum(["active","pending","abandoned","cancelled","expired"]).describe("Registration status. Omit for active marks if status tracking is not maintained.").optional(), "license_type": z.enum(["owned","licensed_in","licensed_out"]).describe("Whether the publisher owns the mark, licenses it from another entity, or licenses it to others. 'owned' is the default if omitted.").optional(), "licensor_domain": z.string().regex(new RegExp("^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$")).describe("Domain of the entity that licenses this mark to the publisher. Meaningful when license_type=licensed_in; omit otherwise.").optional(), "countries": z.array(z.string().min(2).max(2)).describe("ISO 3166-1 alpha-2 country codes where this registration applies. Omit for global or where the registry's jurisdiction is implicit.").optional(), "nice_classes": z.array(z.number().int().gte(1).lte(45)).describe("Nice Classification class numbers (1-45) covered by this registration. Disambiguates marks across industries (e.g., Delta-airline vs Delta-faucet). Omit if scope is implicit from registry.").optional() }).catchall(z.any()).describe("A registered trademark. May appear at house level (corporate marks, e.g., 'NIKE' owned by Nike, Inc.) or at brand level (brand-specific marks, e.g., 'CONVERSE' owned by Converse). Resolution between house- and brand-level trademarks is union — both lists are valid claims about marks the publisher controls.")).describe("Brand-level registered trademarks. Use for marks the brand owns or controls (e.g., a sub-brand's own marks distinct from the corporate parent). House-level trademarks live on the house object; resolution between the two is union — both lists are valid claims.").optional(), "voice_synthesis": z.object({ "provider": z.string().optional(), "voice_id": z.string().optional(), "settings": z.record(z.string(), z.any()).optional() }).catchall(z.any()).describe("TTS voice synthesis configuration for AI-generated audio").optional(), "avatar": z.object({ "provider": z.string().optional(), "avatar_id": z.string().optional(), "settings": z.record(z.string(), z.any()).optional() }).catchall(z.any()).describe("Visual avatar configuration").optional(), "visual_guidelines": z.object({ "photography": z.object({ "realism": z.enum(["natural","stylized","hyperreal","abstract"]).describe("Level of photographic realism").optional(), "lighting": z.string().describe("Lighting style (e.g., 'soft daylight', 'studio', 'golden hour', 'high-key', 'low-key')").optional(), "color_temperature": z.enum(["warm","neutral","cool"]).describe("Overall color temperature of photography").optional(), "contrast": z.enum(["low","medium","high"]).describe("Contrast level in photography").optional(), "depth_of_field": z.enum(["shallow","medium","deep"]).describe("Depth of field preference. shallow: blurred background with subject isolation, deep: everything in focus").optional(), "subject": z.object({ "people": z.object({ "age_range": z.string().describe("Target age range (e.g., '20-35')").optional(), "diversity": z.string().describe("Diversity representation (e.g., 'mixed', 'varied')").optional(), "mood": z.array(z.string()).describe("Mood descriptors (e.g., ['confident', 'relaxed'])").optional() }).catchall(z.any()).describe("People photography guidelines").optional(), "product_focus": z.enum(["in-use","isolated","lifestyle","detail"]).describe("How products are shown").optional(), "setting": z.string().describe("Environmental context for photography (e.g., 'indoor', 'outdoor', 'studio', 'urban', 'nature', 'workplace')").optional() }).catchall(z.any()).describe("Subject matter guidelines").optional(), "framing": z.object({ "subject_position": z.string().describe("Where the subject sits in frame (e.g., 'center', 'center-left', 'rule-of-thirds')").optional(), "crop_style": z.string().describe("Cropping convention (e.g., 'waist-up', 'full-body', 'close-up', 'wide')").optional(), "perspective": z.string().describe("Camera perspective (e.g., 'eye-level', 'overhead', 'low-angle')").optional() }).catchall(z.any()).describe("Camera framing rules").optional(), "preferred_aspect_ratios": z.array(z.string().regex(new RegExp("^\\d+:\\d+$"))).describe("Preferred aspect ratios for brand photography (e.g., '16:9', '4:5', '1:1')").optional(), "tags": z.array(z.string()).describe("Additional style descriptors").optional() }).catchall(z.any()).describe("Photography style rules for generative creative systems. Defines how brand photography should look when selected or generated.").optional(), "graphic_style": z.object({ "style_type": z.enum(["flat_illustration","geometric","gradient_mesh","editorial_collage","hand_drawn","minimal_line_art","3d_render","isometric","photographic_composite"]).describe("Primary graphic style").optional(), "stroke_style": z.enum(["rounded","square","mixed","none"]).describe("Stroke end/join style").optional(), "stroke_weight": z.string().describe("Stroke weight (e.g., '2px', 'thin', 'bold')").optional(), "corner_radius": z.string().describe("Default corner radius for graphic and illustration elements (e.g., '12px', '8px', 'sharp'). For UI component radii (buttons, cards, inputs), see visual_guidelines.border_radius.").optional(), "tags": z.array(z.string()).describe("Additional style descriptors").optional() }).catchall(z.any()).describe("Visual language for brand graphics and illustrations").optional(), "shapes": z.object({ "primary_shape": z.string().describe("Primary brand shape (e.g., 'rounded_rectangle', 'circle', 'hexagon')").optional(), "secondary_shapes": z.array(z.string()).describe("Secondary shapes in the brand vocabulary").optional(), "usage": z.object({ "max_per_layout": z.number().int().describe("Maximum distinct shapes per layout").optional(), "overlap_allowed": z.boolean().describe("Whether shapes may overlap").optional() }).catchall(z.any()).describe("Shape usage rules").optional() }).catchall(z.any()).describe("Distinctive shapes used as part of brand visual identity").optional(), "iconography": z.object({ "style": z.enum(["outline","filled","duotone","flat","glyph","hand_drawn"]).describe("Icon rendering style").optional(), "stroke_weight": z.string().describe("Icon stroke weight (e.g., '2px', '1.5px')").optional(), "corner_style": z.enum(["rounded","square","mixed"]).describe("Corner style for icon paths").optional(), "usage": z.object({ "max_per_frame": z.number().int().describe("Maximum icons per creative frame").optional(), "size_ratio": z.string().describe("Icon-to-layout size ratio (e.g., '1:8')").optional() }).catchall(z.any()).describe("Icon usage rules").optional() }).catchall(z.any()).describe("Icon style system and usage rules").optional(), "composition": z.object({ "overlays": z.object({ "gradient_style": z.enum(["linear","radial","conic","none"]).describe("Gradient type for overlays").optional(), "gradient_direction": z.string().describe("Gradient direction (e.g., '45deg', 'to-bottom-right')").optional(), "opacity": z.string().describe("Overlay opacity (e.g., '70%')").optional() }).catchall(z.any()).describe("Graphic overlay rules").optional(), "texture": z.object({ "style": z.enum(["none","subtle_grain","noise","paper","fabric","concrete"]).describe("Texture style applied to creative assets").optional(), "intensity": z.enum(["low","medium","high"]).describe("Texture intensity").optional() }).catchall(z.any()).describe("Texture treatment rules").optional(), "backgrounds": z.object({ "types_allowed": z.array(z.enum(["solid_color","gradient","blurred_photo","image","video","pattern","transparent"])).describe("Permitted background types").optional() }).catchall(z.any()).describe("Background treatment rules").optional() }).catchall(z.any()).describe("Layout composition rules including overlays, textures, and backgrounds").optional(), "border_radius": z.object({ "none": z.string().describe("Explicitly sharp corners (e.g., '0')").optional(), "default": z.string().describe("Default border radius for UI components (e.g., '8px', '12px', '0'). For graphic/illustration elements, see graphic_style.corner_radius.").optional(), "small": z.string().describe("Small border radius for compact elements (e.g., '4px')").optional(), "large": z.string().describe("Large border radius for cards and containers (e.g., '16px', '24px')").optional(), "pill": z.string().describe("Fully rounded / pill shape (e.g., '999px')").optional() }).catchall(z.string()).describe("Named border radius presets for UI components and layout elements. One of the most visible brand differentiators — Airbnb uses generous 20px, Stripe uses precise 4–8px, Spotify uses pill/999px.").optional(), "elevation": z.object({ "none": z.string().describe("No shadow (e.g., 'none')").optional(), "subtle": z.string().describe("Subtle shadow for slight lift (e.g., '0 1px 2px rgba(0,0,0,0.05)')").optional(), "card": z.string().describe("Card-level shadow (e.g., '0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -2px rgba(0,0,0,0.1)')").optional(), "modal": z.string().describe("Modal/overlay shadow (e.g., '0 20px 25px -5px rgba(0,0,0,0.1), 0 8px 10px -6px rgba(0,0,0,0.1)')").optional() }).catchall(z.string()).describe("Named shadow/elevation levels. Brands use elevation as identity — from Stripe's blue-tinted multi-layer shadows to Apple's single diffuse shadow. Values are CSS box-shadow syntax.").optional(), "spacing": z.object({ "unit": z.string().describe("Base grid unit this scale was designed from (e.g., '8px', '4px'). Informational — agents should use the named scale values, not compute from this.").optional(), "scale": z.object({ "xs": z.string().describe("Extra small spacing (e.g., '4px')").optional(), "sm": z.string().describe("Small spacing (e.g., '8px')").optional(), "md": z.string().describe("Medium spacing (e.g., '16px')").optional(), "lg": z.string().describe("Large spacing (e.g., '24px')").optional(), "xl": z.string().describe("Extra large spacing (e.g., '32px')").optional(), "2xl": z.string().describe("Section-level spacing (e.g., '48px', '64px')").optional() }).catchall(z.string()).describe("Named spacing scale built from the base unit").optional() }).strict().describe("Spacing system for consistent layout rhythm. Most design systems use an 8px base grid.").optional(), "graphic_elements": z.array(z.object({ "name": z.string().describe("Element name (e.g., 'Paper Tear', 'Brand Watermark', 'Section Divider')"), "type": z.enum(["border","divider","frame","watermark","pattern","texture_overlay","decorative"]).describe("Element type").optional(), "description": z.string().describe("How the element is used in layouts").optional(), "orientation": z.enum(["horizontal","vertical","any"]).describe("Preferred orientation when used in layouts").optional(), "colors": z.array(z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")).describe("A single hex color value")).describe("Colors this element may appear in").optional(), "max_per_layout": z.number().int().describe("Maximum instances per layout").optional() }).catchall(z.any()).describe("A reusable decorative or structural visual element that is part of the brand identity (e.g., torn paper edges, watermarks, dividers, background patterns)")).describe("Reusable decorative elements that are part of the brand visual identity (e.g., torn paper edges, watermarks, dividers)").optional(), "motion": z.object({ "transition_style": z.enum(["cut","dissolve","slide","wipe","zoom","fade"]).describe("Primary transition style between scenes").optional(), "animation_speed": z.enum(["slow","moderate","fast"]).describe("Overall animation pacing").optional(), "easing": z.string().describe("Default easing function (e.g., 'ease-in-out', 'spring', 'linear')").optional(), "text_entrance": z.enum(["fade","typewriter","slide_up","slide_left","scale","none"]).describe("How text enters the frame").optional(), "pacing": z.enum(["lingering","moderate","fast_cuts"]).describe("Overall editing rhythm").optional(), "kinetic_typography": z.boolean().describe("Whether animated/kinetic typography is allowed").optional(), "tags": z.array(z.string()).describe("Additional motion style descriptors").optional() }).catchall(z.any()).describe("Motion and animation rules for video, animated display, and interactive formats").optional(), "logo_placement": z.object({ "preferred_position": z.enum(["top-left","top-center","top-right","bottom-left","bottom-center","bottom-right","center"]).describe("Preferred logo position in layouts").optional(), "min_clear_space": z.string().describe("Minimum clear space around the logo, expressed as a multiple of logo height (e.g., '0.5x', '1x') or fixed value (e.g., '16px')").optional(), "min_height": z.string().describe("Minimum logo height to maintain legibility (e.g., '40px', '24px')").optional(), "background_contrast": z.enum(["light_only","dark_only","any"]).describe("Permitted background contrast behind logo").optional() }).catchall(z.any()).describe("Logo placement and clear space rules for automated creative production").optional(), "colorways": z.array(z.object({ "name": z.string().describe("Colorway name (e.g., 'primary', 'inverted', 'subtle')"), "foreground": z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")).describe("A single hex color value"), "background": z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")).describe("A single hex color value"), "accent": z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")).describe("A single hex color value").optional(), "border": z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")).describe("A single hex color value").optional(), "cta_foreground": z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")).describe("CTA text/icon color, if different from foreground").optional(), "cta_background": z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")).describe("CTA button/container color, if different from accent").optional(), "channels": z.array(z.string()).describe("Channels or contexts where this colorway applies (e.g., 'online', 'print', 'pos', 'social', 'outdoor'). Omit for universal colorways.").optional() }).catchall(z.any()).describe("A named color pairing that defines how colors work together. Colorways ensure foreground/background combinations are always on-brand and accessible.")).describe("Named color pairings for consistent foreground/background combinations").optional(), "color_constraints": z.array(z.object({ "color": z.object({ "kind": z.enum(["name","value","surface"]).describe("Discriminator for the color reference variant.").optional(), "name": z.string().describe("Key from the colors object, such as market_yellow, river_green, or any custom palette key. Unambiguous lookup into colors{}.").optional(), "value": z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")).describe("Literal hex color when no palette key exists.").optional(), "surface": z.enum(["background","foreground","text","logo_background","cta","accent","border","icon","graphic_element"]).describe("Usage surface rather than a specific color value.").optional() }).strict().and(z.union([z.object({ "kind": z.literal("name") }), z.object({ "kind": z.literal("value") }), z.object({ "kind": z.literal("surface") })])).describe("Color role or value this constraint governs."), "applies_to": z.array(z.enum(["background","foreground","text","logo_background","cta","accent","border","icon","graphic_element"])).describe("Surfaces where this color may be used.").optional(), "allowed_on": z.array(z.object({ "kind": z.enum(["name","value","surface"]).describe("Discriminator for the color reference variant.").optional(), "name": z.string().describe("Key from the colors object, such as market_yellow, river_green, or any custom palette key. Unambiguous lookup into colors{}.").optional(), "value": z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")).describe("Literal hex color when no palette key exists.").optional(), "surface": z.enum(["background","foreground","text","logo_background","cta","accent","border","icon","graphic_element"]).describe("Usage surface rather than a specific color value.").optional() }).strict().and(z.union([z.object({ "kind": z.literal("name") }), z.object({ "kind": z.literal("value") }), z.object({ "kind": z.literal("surface") })])).describe("Reference to a brand color or usage surface for machine-readable guideline constraints. Use kind=name for palette lookup keys in colors, kind=value for a literal hex color, and kind=surface for layout surfaces such as background or text.")).describe("Backgrounds, surfaces, or color roles where this color is allowed.").optional(), "forbidden_on": z.array(z.object({ "kind": z.enum(["name","value","surface"]).describe("Discriminator for the color reference variant.").optional(), "name": z.string().describe("Key from the colors object, such as market_yellow, river_green, or any custom palette key. Unambiguous lookup into colors{}.").optional(), "value": z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")).describe("Literal hex color when no palette key exists.").optional(), "surface": z.enum(["background","foreground","text","logo_background","cta","accent","border","icon","graphic_element"]).describe("Usage surface rather than a specific color value.").optional() }).strict().and(z.union([z.object({ "kind": z.literal("name") }), z.object({ "kind": z.literal("value") }), z.object({ "kind": z.literal("surface") })])).describe("Reference to a brand color or usage surface for machine-readable guideline constraints. Use kind=name for palette lookup keys in colors, kind=value for a literal hex color, and kind=surface for layout surfaces such as background or text.")).describe("Backgrounds, surfaces, or color roles where this color is forbidden.").optional(), "never_pair_with": z.array(z.object({ "kind": z.enum(["name","value","surface"]).describe("Discriminator for the color reference variant.").optional(), "name": z.string().describe("Key from the colors object, such as market_yellow, river_green, or any custom palette key. Unambiguous lookup into colors{}.").optional(), "value": z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")).describe("Literal hex color when no palette key exists.").optional(), "surface": z.enum(["background","foreground","text","logo_background","cta","accent","border","icon","graphic_element"]).describe("Usage surface rather than a specific color value.").optional() }).strict().and(z.union([z.object({ "kind": z.literal("name") }), z.object({ "kind": z.literal("value") }), z.object({ "kind": z.literal("surface") })])).describe("Reference to a brand color or usage surface for machine-readable guideline constraints. Use kind=name for palette lookup keys in colors, kind=value for a literal hex color, and kind=surface for layout surfaces such as background or text.")).describe("Color roles or values that must not be paired with this color.").optional(), "contexts": z.array(z.string()).describe("Channels or creative contexts where this constraint applies, such as digital, print, social, or ctv_end_card.").optional(), "severity": z.enum(["must","should"]).describe("Strength of a machine-readable brand guideline constraint.").optional(), "description": z.string().describe("Human-readable rationale or source-language summary for the rule.").optional() }).catchall(z.any()).describe("Machine-readable rule constraining how a brand color may be used or paired. Use for accent-only colors, forbidden foreground/background combinations, and palette pairs that should never appear together.")).describe("Machine-readable constraints for color usage and pairings, such as accent-only rules or forbidden foreground/background combinations.").optional(), "logo_usage_rules": z.array(z.object({ "logo_url": z.string().url().describe("Specific logo asset URL this rule applies to. Omit when the rule applies by variant or tags.").optional(), "logo_id": z.string().regex(new RegExp("^[a-z0-9][a-z0-9_-]*$")).describe("Stable `logos[].id` this rule applies to. Prefer this over logo_url when the rule targets a specific logo entry.").optional(), "logo_variant": z.enum(["primary","secondary","icon","wordmark","full-lockup"]).describe("Logo variant this rule applies to.").optional(), "logo_tags": z.array(z.string()).describe("Logo tags this rule applies to.").optional(), "contexts": z.array(z.string()).describe("Creative contexts where this rule applies.").optional(), "slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Canonical renderer slots where this rule applies. Use this for deterministic logo-card, profile-mark, end-card, and lockup selection.").optional(), "minimum_size": z.object({ "width": z.string().describe("Minimum width, such as 48px or 12mm.").optional(), "height": z.string().describe("Minimum height, such as 18px or 6mm.").optional() }).strict().describe("Minimum rendered size needed for legibility.").optional(), "clear_space": z.string().describe("Minimum clear space around the logo, expressed in brand terms or units.").optional(), "allowed_backgrounds": z.array(z.object({ "kind": z.enum(["name","value","surface"]).describe("Discriminator for the color reference variant.").optional(), "name": z.string().describe("Key from the colors object, such as market_yellow, river_green, or any custom palette key. Unambiguous lookup into colors{}.").optional(), "value": z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")).describe("Literal hex color when no palette key exists.").optional(), "surface": z.enum(["background","foreground","text","logo_background","cta","accent","border","icon","graphic_element"]).describe("Usage surface rather than a specific color value.").optional() }).strict().and(z.union([z.object({ "kind": z.literal("name") }), z.object({ "kind": z.literal("value") }), z.object({ "kind": z.literal("surface") })])).describe("Reference to a brand color or usage surface for machine-readable guideline constraints. Use kind=name for palette lookup keys in colors, kind=value for a literal hex color, and kind=surface for layout surfaces such as background or text.")).describe("Background color roles, values, or surfaces where this logo may be placed.").optional(), "forbidden_backgrounds": z.array(z.object({ "kind": z.enum(["name","value","surface"]).describe("Discriminator for the color reference variant.").optional(), "name": z.string().describe("Key from the colors object, such as market_yellow, river_green, or any custom palette key. Unambiguous lookup into colors{}.").optional(), "value": z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")).describe("Literal hex color when no palette key exists.").optional(), "surface": z.enum(["background","foreground","text","logo_background","cta","accent","border","icon","graphic_element"]).describe("Usage surface rather than a specific color value.").optional() }).strict().and(z.union([z.object({ "kind": z.literal("name") }), z.object({ "kind": z.literal("value") }), z.object({ "kind": z.literal("surface") })])).describe("Reference to a brand color or usage surface for machine-readable guideline constraints. Use kind=name for palette lookup keys in colors, kind=value for a literal hex color, and kind=surface for layout surfaces such as background or text.")).describe("Background color roles, values, or surfaces where this logo must not be placed.").optional(), "forbidden_contexts": z.array(z.string()).describe("Contexts where this logo must not be used, such as photography_without_knockout.").optional(), "severity": z.enum(["must","should"]).describe("Strength of a machine-readable brand guideline constraint.").optional(), "description": z.string().describe("Human-readable rationale or source-language summary for the rule.").optional() }).catchall(z.any()).and(z.union([z.any(), z.any(), z.any(), z.any(), z.any()])).describe("Machine-readable logo selection and placement rule. Complements logos[].usage by making enforceable minimum size, clear-space, background, and context constraints queryable.")).describe("Machine-readable logo selection and placement constraints for minimum size, clear space, backgrounds, and contexts.").optional(), "mark_lockups": z.array(z.object({ "lockup_type": z.enum(["co_brand","secondary_mark","partner","sponsor","program","talent","custom"]).describe("Type of mark relationship governed by this lockup rule."), "ordering": z.enum(["brand_first","partner_first","equal","contextual"]).describe("Required visual ordering of the brand mark relative to partner or secondary marks.").optional(), "contexts": z.array(z.string()).describe("Creative contexts where this lockup rule applies.").optional(), "brand_logo_id": z.string().regex(new RegExp("^[a-z0-9][a-z0-9_-]*$")).describe("Stable `logos[].id` for the brand logo this lockup rule is anchored on.").optional(), "secondary_logo_ids": z.array(z.string().regex(new RegExp("^[a-z0-9][a-z0-9_-]*$")).describe("Stable identifier for a logo entry within this brand.json document. Use lowercase words separated by underscores or hyphens; do not key integrations on mutable asset URLs.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Stable `logos[].id` values for secondary, program, sponsor, or partner marks governed by this lockup rule when those marks are represented in this brand.json.").optional(), "separator": z.object({ "type": z.enum(["none","keyline","space","divider"]).describe("Separator style."), "color": z.object({ "kind": z.enum(["name","value","surface"]).describe("Discriminator for the color reference variant.").optional(), "name": z.string().describe("Key from the colors object, such as market_yellow, river_green, or any custom palette key. Unambiguous lookup into colors{}.").optional(), "value": z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")).describe("Literal hex color when no palette key exists.").optional(), "surface": z.enum(["background","foreground","text","logo_background","cta","accent","border","icon","graphic_element"]).describe("Usage surface rather than a specific color value.").optional() }).strict().and(z.union([z.object({ "kind": z.literal("name") }), z.object({ "kind": z.literal("value") }), z.object({ "kind": z.literal("surface") })])).describe("Reference to a brand color or usage surface for machine-readable guideline constraints. Use kind=name for palette lookup keys in colors, kind=value for a literal hex color, and kind=surface for layout surfaces such as background or text.").optional(), "width": z.string().describe("Separator width, such as 1px.").optional() }).catchall(z.any()).describe("Separator between marks, when required.").optional(), "min_gap": z.string().describe("Minimum gap between marks, expressed in brand terms or units.").optional(), "brand_min_optical_weight_ratio": z.number().gt(0).describe("Minimum optical weight of the brand mark relative to partner marks. 1 means at least equal.").optional(), "partner_max_optical_weight_ratio": z.number().gt(0).describe("Maximum optical weight of partner marks relative to the brand mark. 1 means no larger than the brand mark. Enforcement is at layout time, not parse time — this value signals to renderers and creative agents how much space to provision for each mark.").optional(), "severity": z.enum(["must","should"]).describe("Strength of a machine-readable brand guideline constraint.").optional(), "description": z.string().describe("Human-readable rationale or source-language summary for the lockup rule.").optional() }).catchall(z.any()).describe("Machine-readable layout constraints for co-brand, partner, sponsor, program, or secondary-mark lockups.")).describe("Machine-readable co-brand, partner, sponsor, program, or secondary-mark lockup rules.").optional(), "type_scale": z.object({ "base_width": z.string().describe("Reference canvas width these sizes were designed for (e.g., '1080px'). Generative systems should scale proportionally for other canvas sizes.").optional(), "heading": z.object({ "font": z.string().describe("Font reference. Use a key from the fonts object (e.g., 'primary', 'secondary') to reference a defined font role, or a literal CSS font-family string as a fallback.").optional(), "size": z.string().describe("Font size (e.g., '48px', '2rem')").optional(), "weight": z.string().describe("Font weight (e.g., '700', 'bold')").optional(), "line_height": z.string().describe("Line height (e.g., '1.2', '56px')").optional(), "letter_spacing": z.string().describe("Letter spacing (e.g., '-0.02em', '0.5px')").optional(), "text_transform": z.enum(["none","uppercase","lowercase","capitalize"]).describe("Text transformation").optional() }).catchall(z.any()).describe("A single entry in the type scale").optional(), "subheading": z.object({ "font": z.string().describe("Font reference. Use a key from the fonts object (e.g., 'primary', 'secondary') to reference a defined font role, or a literal CSS font-family string as a fallback.").optional(), "size": z.string().describe("Font size (e.g., '48px', '2rem')").optional(), "weight": z.string().describe("Font weight (e.g., '700', 'bold')").optional(), "line_height": z.string().describe("Line height (e.g., '1.2', '56px')").optional(), "letter_spacing": z.string().describe("Letter spacing (e.g., '-0.02em', '0.5px')").optional(), "text_transform": z.enum(["none","uppercase","lowercase","capitalize"]).describe("Text transformation").optional() }).catchall(z.any()).describe("A single entry in the type scale").optional(), "body": z.object({ "font": z.string().describe("Font reference. Use a key from the fonts object (e.g., 'primary', 'secondary') to reference a defined font role, or a literal CSS font-family string as a fallback.").optional(), "size": z.string().describe("Font size (e.g., '48px', '2rem')").optional(), "weight": z.string().describe("Font weight (e.g., '700', 'bold')").optional(), "line_height": z.string().describe("Line height (e.g., '1.2', '56px')").optional(), "letter_spacing": z.string().describe("Letter spacing (e.g., '-0.02em', '0.5px')").optional(), "text_transform": z.enum(["none","uppercase","lowercase","capitalize"]).describe("Text transformation").optional() }).catchall(z.any()).describe("A single entry in the type scale").optional(), "caption": z.object({ "font": z.string().describe("Font reference. Use a key from the fonts object (e.g., 'primary', 'secondary') to reference a defined font role, or a literal CSS font-family string as a fallback.").optional(), "size": z.string().describe("Font size (e.g., '48px', '2rem')").optional(), "weight": z.string().describe("Font weight (e.g., '700', 'bold')").optional(), "line_height": z.string().describe("Line height (e.g., '1.2', '56px')").optional(), "letter_spacing": z.string().describe("Letter spacing (e.g., '-0.02em', '0.5px')").optional(), "text_transform": z.enum(["none","uppercase","lowercase","capitalize"]).describe("Text transformation").optional() }).catchall(z.any()).describe("A single entry in the type scale").optional(), "cta": z.object({ "font": z.string().describe("Font reference. Use a key from the fonts object (e.g., 'primary', 'secondary') to reference a defined font role, or a literal CSS font-family string as a fallback.").optional(), "size": z.string().describe("Font size (e.g., '48px', '2rem')").optional(), "weight": z.string().describe("Font weight (e.g., '700', 'bold')").optional(), "line_height": z.string().describe("Line height (e.g., '1.2', '56px')").optional(), "letter_spacing": z.string().describe("Letter spacing (e.g., '-0.02em', '0.5px')").optional(), "text_transform": z.enum(["none","uppercase","lowercase","capitalize"]).describe("Text transformation").optional() }).catchall(z.any()).describe("A single entry in the type scale").optional() }).catchall(z.object({ "font": z.string().describe("Font reference. Use a key from the fonts object (e.g., 'primary', 'secondary') to reference a defined font role, or a literal CSS font-family string as a fallback.").optional(), "size": z.string().describe("Font size (e.g., '48px', '2rem')").optional(), "weight": z.string().describe("Font weight (e.g., '700', 'bold')").optional(), "line_height": z.string().describe("Line height (e.g., '1.2', '56px')").optional(), "letter_spacing": z.string().describe("Letter spacing (e.g., '-0.02em', '0.5px')").optional(), "text_transform": z.enum(["none","uppercase","lowercase","capitalize"]).describe("Text transformation").optional() }).catchall(z.any()).describe("A single entry in the type scale")).describe("Typography scale defining sizes and weights for different text roles. When sizes are in px, use base_width to indicate the reference canvas.").optional(), "asset_libraries": z.array(z.object({ "name": z.string().describe("Display name of the asset library"), "type": z.enum(["icon_set","illustration_system","image_library","video_library","template_library"]).describe("Type of asset library").optional(), "url": z.string().url().describe("URL to the asset library (for human access)"), "description": z.string().describe("Description of the library contents and usage").optional(), "color_guide": z.object({ "roles": z.array(z.string()).describe("Named color roles used in the library (e.g., base, shadow_1, highlight_1, stroke)").optional(), "palettes": z.array(z.object({ "name": z.string().describe("Palette name"), "colors": z.record(z.string(), z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")).describe("A single hex color value")).describe("Map of role names to hex color values") }).catchall(z.any())).describe("Named color palettes mapping roles to specific colors").optional() }).catchall(z.any()).describe("Color guide for the asset library defining roles and palettes").optional() }).catchall(z.any()).describe("A managed asset library (icon set, illustration system, image collection). The URL is for human access; agent-facing DAM integration is under investigation.")).describe("References to managed asset libraries (icon sets, illustration systems, image collections). URLs are intended for human access; agent-facing DAM integration is under investigation.").optional(), "restrictions": z.array(z.string()).describe("Visual prohibitions and guardrails (e.g., 'Never use black backgrounds', 'Do not crop the logo', 'No stock photography of people on phones')").optional() }).catchall(z.any()).describe("Structured visual rules for generative creative systems").optional(), "agents": z.array(z.object({ "type": z.enum(["brand","rights","measurement","governance","creative","sales","buying","signals"]).describe("Functional role of this agent"), "url": z.string().url().regex(new RegExp("^https://")).describe("Agent endpoint URL (MCP or A2A). Callers comparing a brand's declared agent URL against another value (e.g., resolving 'is this the agent that signed this artifact?' or matching against a discovery cache) MUST canonicalize both sides per the AdCP URL canonicalization rules, not byte-equality. See docs/reference/url-canonicalization."), "id": z.string().regex(new RegExp("^[a-z0-9_]+$")).max(100).describe("Agent identifier (useful for logging, multi-tenant platforms)"), "description": z.string().max(500).describe("Human-readable description of this agent's capabilities or scope").optional(), "jwks_uri": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL of the agent's JWKS (RFC 7517) containing public keys used to verify artifacts this agent signs or requests it sends. Verified artifacts include signed governance_context tokens (for governance agents) and RFC 9421 HTTP Signatures on outgoing requests (for any agent). When absent, verifiers MUST default to /.well-known/jwks.json on the origin of `url`. Keys are identified by `kid` in the JWS header or RFC 9421 `keyid` parameter; JWKS MAY contain multiple keys to support rotation and per-purpose separation via `key_ops` and `use`.").optional(), "available_uses": z.array(z.enum(["likeness","voice","name","endorsement","motion_capture","signature","catchphrase","sync","background_music","editorial","commercial","ai_generated_image"]).describe("Types of rights usage that can be licensed through the brand protocol. Aligned with DDEX UseType direction for interoperability with music and media rights systems.")).describe("For rights agents: rights uses available for licensing").optional(), "right_types": z.array(z.enum(["talent","character","brand_ip","music","stock_media"]).describe("Categories of intellectual property rights that can be licensed through the brand protocol.")).describe("For rights agents: types of rights available").optional(), "countries": z.array(z.string().regex(new RegExp("^[A-Z]{2}$"))).describe("ISO 3166-1 alpha-2 country codes where this agent operates. Omit for global scope.").optional() }).catchall(z.any()).describe("An agent declared by a brand or house. Each entry identifies one agent endpoint and its functional role in the advertising ecosystem.")).describe("Agents authorized to act on behalf of this brand. Consumers resolving an agent by URL use the matching brand-level entry; do not infer a type-wide override of unrelated house-level entries when multiple same-type entries exist.").optional(), "brand_agent": z.object({ "url": z.string().url().regex(new RegExp("^https://")).describe("Brand agent MCP endpoint URL. Callers comparing this URL against another value (e.g., resolving 'is this the brand's declared agent?' against a discovery cache) MUST canonicalize both sides per the AdCP URL canonicalization rules, not byte-equality. See docs/reference/url-canonicalization."), "id": z.string().regex(new RegExp("^[a-z0-9_]+$")).describe("Agent identifier (useful for logging, multi-tenant DAMs)") }).catchall(z.any()).describe("Deprecated: use agents array with type 'brand' instead. Brand agent that provides dynamic brand data via MCP.").optional(), "rights_agent": z.object({ "url": z.string().url().regex(new RegExp("^https://")).describe("Rights agent MCP endpoint URL. Callers comparing this URL against another value (e.g., matching against a brand's declared rights endpoint) MUST canonicalize both sides per the AdCP URL canonicalization rules, not byte-equality. See docs/reference/url-canonicalization."), "id": z.string().regex(new RegExp("^[a-z0-9_]+$")).describe("Agent identifier"), "available_uses": z.array(z.enum(["likeness","voice","name","endorsement","motion_capture","signature","catchphrase","sync","background_music","editorial","commercial","ai_generated_image"]).describe("Types of rights usage that can be licensed through the brand protocol. Aligned with DDEX UseType direction for interoperability with music and media rights systems.")).describe("Rights uses available for licensing through this agent"), "right_types": z.array(z.enum(["talent","character","brand_ip","music","stock_media"]).describe("Categories of intellectual property rights that can be licensed through the brand protocol.")).describe("Types of rights available").optional(), "countries": z.array(z.string().regex(new RegExp("^[A-Z]{2}$"))).describe("Countries where rights are available (ISO 3166-1 alpha-2)").optional() }).catchall(z.any()).describe("Deprecated: use agents array with type 'rights' instead. Rights licensing agent for this brand.").optional(), "contact": z.object({ "email": z.string().email().describe("Contact email").optional(), "phone": z.string().describe("Contact phone number").optional() }).describe("Brand-level contact information").optional(), "collections": z.array(z.object({ "collection_id": z.string().describe("Collection identifier as used in the seller's get_products responses").optional(), "name": z.string().describe("Human-readable collection name"), "role": z.enum(["host","guest","creator","cast","narrator","producer","correspondent","commentator","analyst"]).describe("This person's role on the collection").optional(), "seller_agent_url": z.string().url().describe("URL of the sales agent that sells inventory for this collection. Buyer agents can query this agent for collection products.").optional() }).catchall(z.any())).describe("Collections this person or brand is associated with. Enables bidirectional linking: a collection's talent references brand.json via brand_url, and brand.json links back to collections.").optional() }).catchall(z.any()).describe("A brand within a house portfolio. Combines identity (who) with creative assets (how to represent). Referenced as domain + brand_id.")).describe("Inline brands owned by this house (parent-owned data). Use for sub-brands without their own canonical document — typically those without a dedicated domain or that the holdco wants to manage centrally. A brand_id MUST NOT appear in both brands[] and brand_refs[].").optional(), "brand_refs": z.array(z.object({ "domain": z.string().regex(new RegExp("^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$")).describe("Domain where the child's canonical brand.json lives"), "brand_id": z.string().regex(new RegExp("^[a-z0-9_]+$")).describe("Stable brand identifier within the house portfolio. Required so the cross-array uniqueness invariant (brand_id MUST NOT appear in both brands[] and brand_refs[]) is enforceable."), "managed_by": z.string().regex(new RegExp("^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$")).describe("Optional domain of the entity that operationally manages this brand (e.g., an agency network within a holdco). House-declared. Consumers MUST NOT use it for trust or authorization decisions. Aggregation across houses ('show me everything BBH manages') is the intended use; trust is unaffected.").optional(), "effective_at": z.string().datetime().describe("ISO 8601 timestamp when the house established this ownership claim. Consumers age mutual-assertion edges from this date for TTL purposes and MUST NOT extend relationship trust before a future effective_at is reached. Optional; absent means the consumer ages from its own durable first observation.").optional() }).strict().describe("A house's ownership entry for a brand that publishes its own canonical brand.json elsewhere. The publisher (the house) asserts 'I own this brand, hosted at this domain, effective on this date.' Mutual-assertion trust requires the child's house_domain to reciprocate. Distinct from core/brand-ref.json (which identifies brands in media-buy plans). See docs/brand-protocol/brand-json.mdx")).describe("Portfolio entries for brands owned by this house that publish their own canonical brand.json elsewhere (child-owned data). Each entry asserts ownership plus where the child's document lives. Mutual-assertion trust: the pointed-to document's house_domain must equal this house's domain. Invariants: a brand_id MUST NOT appear in both brands[] and brand_refs[]; brand_id and domain MUST each be unique within brand_refs[]. See docs/brand-protocol/brand-json.mdx").optional(), "contact": z.object({ "name": z.string().min(1).max(255), "email": z.string().email().max(255).optional(), "domain": z.string().regex(new RegExp("^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$")).describe("A valid domain name").optional() }).catchall(z.any()).describe("Contact information").optional(), "authorized_operators": z.array(z.object({ "domain": z.string().regex(new RegExp("^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$")).describe("Domain of the authorized operator (e.g., 'groupm.com')"), "brands": z.array(z.string().regex(new RegExp("^([a-z0-9_]+|\\*)$"))).describe("Brand IDs this operator is authorized for. Use ['*'] for all brands in the portfolio."), "countries": z.array(z.string().regex(new RegExp("^[A-Z]{2}$"))).describe("ISO 3166-1 alpha-2 country codes where this authorization applies. Omit for global authorization.").optional(), "scopes": z.array(z.enum(["all","media_buying","creative_generation","rights_clearance","governance","measurement","agent_operations"])).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Activities this operator is authorized to perform for the listed brands and countries. Omit for backwards-compatible broad authorization. Use ['all'] only when every listed scope is delegated.").optional(), "valid_from": z.string().datetime().describe("ISO 8601 timestamp when this operator authorization starts. Omit when authorization is already active or the start date is not tracked.").optional(), "valid_until": z.string().datetime().describe("ISO 8601 timestamp when this operator authorization expires. Consumers MUST treat entries at or after this timestamp as inactive for authorization decisions.").optional() }).catchall(z.any()).describe("An entity authorized to represent brands from this house. Verified by resolving the operator's domain. Optional validity fields let houses time-box agency-of-record and delegated-operator relationships without changing historical entries.")).describe("Entities authorized to represent brands from this house. Third parties (sellers, platforms) can verify an operator's authorization by checking this list. Operators are identified by domain.").optional(), "trademarks": z.array(z.object({ "registry": z.string().describe("Trademark registry (e.g., 'USPTO', 'EUIPO', 'JPO', 'CNIPA')"), "number": z.string().describe("Registration number as issued by the registry"), "mark": z.string().describe("The registered mark as published"), "status": z.enum(["active","pending","abandoned","cancelled","expired"]).describe("Registration status. Omit for active marks if status tracking is not maintained.").optional(), "license_type": z.enum(["owned","licensed_in","licensed_out"]).describe("Whether the publisher owns the mark, licenses it from another entity, or licenses it to others. 'owned' is the default if omitted.").optional(), "licensor_domain": z.string().regex(new RegExp("^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$")).describe("Domain of the entity that licenses this mark to the publisher. Meaningful when license_type=licensed_in; omit otherwise.").optional(), "countries": z.array(z.string().min(2).max(2)).describe("ISO 3166-1 alpha-2 country codes where this registration applies. Omit for global or where the registry's jurisdiction is implicit.").optional(), "nice_classes": z.array(z.number().int().gte(1).lte(45)).describe("Nice Classification class numbers (1-45) covered by this registration. Disambiguates marks across industries (e.g., Delta-airline vs Delta-faucet). Omit if scope is implicit from registry.").optional() }).catchall(z.any()).describe("A registered trademark. May appear at house level (corporate marks, e.g., 'NIKE' owned by Nike, Inc.) or at brand level (brand-specific marks, e.g., 'CONVERSE' owned by Converse). Resolution between house- and brand-level trademarks is union — both lists are valid claims about marks the publisher controls.")).describe("House-level (corporate) registered trademarks. Brand-level marks live on individual brand entries; resolution is union.").optional(), "last_updated": z.string().datetime().optional() }).strict().and(z.union([z.any(), z.any()])).describe("Full house/brand portfolio with hierarchy, creative assets, and properties. May carry inline brands (parent-owned, brands[]) and/or pointer brands (child-owned canonical documents, brand_refs[]). At least one of brands[] or brand_refs[] is required. A brand_id MUST NOT appear in both. See docs/brand-protocol/brand-json.mdx"), z.record(z.string(), z.any()).and(z.intersection(z.object({ "$schema": z.string().optional(), "version": z.string().optional(), "default_language": z.string().regex(new RegExp("^(?:[a-z]{2,8}(?:-[A-Z][a-z]{3})?(?:-(?:[A-Z]{2}|[0-9]{3}))?(?:-(?:[a-z0-9]{5,8}|[0-9][a-z0-9]{3}))*(?:-[0-9a-wy-z](?:-[a-z0-9]{2,8})+)*(?:-x(?:-[a-z0-9]{1,8})+)?|x(?:-[a-z0-9]{1,8})+)$")).min(2).max(63).describe("A well-formed BCP 47 language tag used by AdCP only as language identity. Script and region may refine that identity; other valid BCP 47 subtags remain part of tag matching but do not make this a general locale-settings object. It does not determine currency, time zone, number/date formatting, market, or legal jurisdiction. The AdCP canonical wire profile requires lower-case language and variants, title-case script, and upper-case region (for example `en-US`, `zh-Hant-TW`, or `x-private`). RFC 5646 comparisons are case-insensitive and its case regularization is optional; AdCP intentionally requires this stricter single wire spelling and receivers MUST reject differently cased tags rather than silently normalizing them. The schema pattern enforces the AdCP casing profile and extension structure for commonly used tags; conforming receivers additionally validate the complete RFC 5646 grammar and registry rules. Every new AdCP field carrying BCP 47 language identity or a concrete language range MUST reference this schema instead of declaring independent string constraints.").describe("BCP 47 language tag for legacy plain-string and plain-array content in this document. Readers assume en when omitted.").default("en"), "house_domain": z.string().regex(new RegExp("^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$")).describe("Optional pointer to the corporate house this brand belongs to. The named house's brand_refs[] MUST reciprocate for mutual-assertion trust. Single-hop only — a brand cannot itself declare brand_refs[]. Omit for standalone brands (no house).").optional(), "last_updated": z.string().datetime().optional() }), z.object({ "id": z.string().regex(new RegExp("^[a-z0-9_]+$")).describe("Brand identifier within the house. House chooses this ID."), "url": z.string().url().describe("Primary brand URL for context and asset discovery").optional(), "identity_relying_parties": z.array(z.object({ "issuer": z.object({ "domain": z.string().regex(new RegExp("^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$")).describe("Domain where /.well-known/brand.json is hosted, or the brand's operating domain"), "brand_id": z.string().regex(new RegExp("^[a-z0-9_]+$")).describe("Brand identifier within the house portfolio. Optional for single-brand domains.").optional(), "countries": z.array(z.string().regex(new RegExp("^[A-Z]{2}$"))).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Canonical set of ISO 3166-1 alpha-2 countries for this advertiser identity. Omit for a global/default identity. Array order is not meaningful; producers MUST sort codes lexicographically before computing keys or signatures. This qualifies account identity and is not delivery targeting.").optional(), "industries": z.array(z.string()).describe("Inline override for the brand's industries. Useful when the caller cannot modify the brand's canonical brand.json but needs to declare industries for governance (e.g., Annex III vertical detection). brand.json remains the canonical source; when omitted here, governance agents SHOULD resolve from brand.json.").optional(), "data_subject_contestation": z.object({ "url": z.string().url().regex(new RegExp("^https://")).optional(), "email": z.string().email().optional(), "languages": z.array(z.string()).optional() }).strict().and(z.union([z.any(), z.any()])).describe("Inline override for the brand's contestation contact point. Useful when the operator does not control brand.json but needs to discharge Art 22(3) for this plan. brand.json is canonical; when omitted, governance agents resolve brand → house → missing.").optional(), "brand_kit_override": z.object({ "logo": z.object({ "asset_type": z.literal("image").describe("Discriminator identifying this as an image asset. See /schemas/creative/asset-types for the registry."), "url": z.string().url().describe("URL to the image asset"), "width": z.number().int().gte(1).describe("Width in pixels"), "height": z.number().int().gte(1).describe("Height in pixels"), "file_size_bytes": z.number().int().gte(1).describe("Image file size in bytes. Required by agents that advertise a max_file_size_kb constraint.").optional(), "pixel_ratio": z.number().gt(0).describe("Intrinsic pixels per logical render pixel (for example `2` for a 600×500 image intended to render at 300×250). Optional because a validator can infer the ratio when the target format declares logical dimensions. When supplied, it MUST agree with both `width / logical_width` and `height / logical_height`; it is never a substitute for the intrinsic `width` and `height` fields.").optional(), "format": z.string().describe("Image file format (jpg, png, gif, webp, etc.)").optional(), "alt_text": z.string().describe("Alternative text for accessibility").optional(), "provenance": z.object({ "digital_source_type": z.enum(["digital_capture","digital_creation","trained_algorithmic_media","composite_with_trained_algorithmic_media","algorithmic_media","composite_capture","composite_synthetic","human_edits","data_driven_media"]).describe("IPTC-aligned classification of AI involvement in producing this content").optional(), "synthetic_depiction": z.boolean().describe("Assessed declaration of whether the content synthetically depicts a real or fictional person performing or appearing in a way that was generated or materially manipulated rather than captured as depicted. `true` covers both a fully synthetic performer and material manipulation of a real performer; `false` is an assessed declaration that the content does not contain such a depiction. Absence means the content has not been assessed for synthetic depiction. This field does not claim consent, legality, or independent verification, and receivers MUST NOT derive it solely from `digital_source_type`.").optional(), "ai_tool": z.object({ "name": z.string().describe("Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')"), "version": z.string().describe("Version identifier for the AI tool or model (e.g., '25.1', '0125', '2.1'). For generative models, use the model version rather than the API version.").optional(), "provider": z.string().describe("Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')").optional() }).describe("AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.").optional(), "human_oversight": z.enum(["none","prompt_only","selected","edited","directed"]).describe("Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` — the protocol does not derive disclosure obligations from oversight level. Some regulations include carve-outs for human-edited or human-directed AI output, but those carve-outs have factual prerequisites the schema cannot evaluate. Asserting `edited` or `directed` does not by itself justify `disclosure.required: false`.").optional(), "declared_by": z.object({ "agent_url": z.string().url().describe("URL of the agent or service that declared this provenance").optional(), "role": z.enum(["creator","advertiser","agency","platform","tool"]).describe("Role of the declaring party in the supply chain") }).describe("Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.").optional(), "declared_at": z.string().datetime().describe("When this provenance claim was made (ISO 8601). Distinct from created_time, which records when the content itself was produced. A provenance claim may be attached well after content creation, for example when retroactively declaring AI involvement for regulatory compliance.").optional(), "created_time": z.string().datetime().describe("When this content was created or generated (ISO 8601)").optional(), "c2pa": z.object({ "manifest_url": z.string().url().describe("URL to the C2PA manifest store for this content") }).describe("C2PA sidecar manifest reference. Links to a detached cryptographic provenance manifest for this content. Note: file-level C2PA bindings break when ad servers transcode, resize, or re-encode assets. For pipelines with intermediaries, consider embedded_provenance as the primary provenance mechanism.").optional(), "embedded_provenance": z.array(z.object({ "method": z.enum(["manifest_wrapper","provenance_markers"]).describe("How provenance data is carried within the content"), "standard": z.string().describe("Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)").optional(), "provider": z.string().describe("Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context — not a wire identifier."), "verify_agent": z.object({ "agent_url": z.string().url().regex(new RegExp("^https://")).describe("URL of the governance agent the buyer represents was used to embed/verify this layer. MUST use the `https://` scheme and MUST appear in the seller's `creative_policy.accepted_verifiers[].agent_url` list (canonicalized per /docs/reference/url-canonicalization: lowercase scheme and host, strip default port, normalize path dot-segments). Sellers MUST NOT call this URL until the canonicalized match is confirmed."), "feature_id": z.string().describe("Optional `feature_id` the buyer represents the seller should request via `get_creative_features` (e.g., `encypher.markers_present_v2`). SHOULD match the `feature_id` declared on the matching `accepted_verifiers[]` entry, or be omitted to defer the selector to the seller. When the seller's entry pins a `feature_id`, that value wins; when neither side pins, the seller selects from the agent's `governance.creative_features` catalog.").optional() }).strict().describe("Buyer's representation that this embedding can be verified by a governance agent on the seller's `creative_policy.accepted_verifiers` list. The `agent_url` MUST match (canonicalized) one of the seller's published `accepted_verifiers[].agent_url` entries; sellers reject `sync_creatives` submissions whose `verify_agent.agent_url` is off-list with `PROVENANCE_VERIFIER_NOT_ACCEPTED`. This is buyer-supplied evidence, not buyer-driven routing — the seller is the verifier-of-record and the seller controls which agent it actually calls (the seller MAY use a different on-list agent if it determines this is more appropriate; the seller does not call buyer-asserted endpoints outside its allowlist). MAY be omitted for self-verifiable embeddings (e.g., a C2PA text manifest with a public key the seller already trusts).").optional(), "embedded_at": z.string().datetime().describe("When the provenance data was embedded (ISO 8601)").optional() })).describe("Provenance metadata embedded within the content stream. Each entry declares one embedding layer: structured provenance data carried inside the content itself, as distinct from sidecar references (c2pa.manifest_url). Embedded provenance survives operations that break sidecar and file-level bindings: ad-server transcoding, CMS ingestion, copy-paste, reformatting, and CDN re-encoding. For ad-tech pipelines where content passes through multiple intermediaries, embedded provenance is the reliable path for provenance that persists from declaration through delivery. This is a declaration by the embedding party. The receiving party (the seller) is the verifier-of-record: it confirms the claim by calling a governance agent it trusts (typically one published in `creative_policy.accepted_verifiers`).").optional(), "watermarks": z.array(z.object({ "media_type": z.enum(["audio","image","video","text"]).describe("Media category of the watermarked content"), "provider": z.string().describe("Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context — not a wire identifier."), "verify_agent": z.object({ "agent_url": z.string().url().regex(new RegExp("^https://")).describe("URL of the governance agent the buyer represents was used to apply/detect this watermark. MUST use the `https://` scheme and MUST appear in the seller's `creative_policy.accepted_verifiers[].agent_url` list (canonicalized per /docs/reference/url-canonicalization: lowercase scheme and host, strip default port, normalize path dot-segments). Sellers MUST NOT call this URL until the canonicalized match is confirmed."), "feature_id": z.string().describe("Optional `feature_id` the buyer represents the seller should request via `get_creative_features` (e.g., `imatag.watermark_detected`). SHOULD match the `feature_id` declared on the matching `accepted_verifiers[]` entry, or be omitted to defer the selector to the seller. When the seller's entry pins a `feature_id`, that value wins; when neither side pins, the seller selects from the agent's `governance.creative_features` catalog.").optional() }).strict().describe("Buyer's representation that this watermark can be detected by a governance agent on the seller's `creative_policy.accepted_verifiers` list. The `agent_url` MUST match (canonicalized) one of the seller's published `accepted_verifiers[].agent_url` entries; sellers reject `sync_creatives` submissions whose `verify_agent.agent_url` is off-list with `PROVENANCE_VERIFIER_NOT_ACCEPTED`. This is buyer-supplied evidence, not buyer-driven routing — the seller is the verifier-of-record and the seller controls which agent it actually calls (the seller MAY use a different on-list agent if it determines this is more appropriate; the seller does not call buyer-asserted endpoints outside its allowlist).").optional(), "c2pa_action": z.enum(["c2pa.watermarked.bound","c2pa.watermarked.unbound"]).describe("C2PA action classification for this watermark").optional(), "embedded_at": z.string().datetime().describe("When the watermark was applied (ISO 8601)").optional() })).describe("Content watermarks applied to this asset. Each entry declares one watermarking layer: a content modification that encodes an identifier or fingerprint within the asset. Watermarks differ from embedded provenance: a watermark encodes an identifier (who generated it, who owns it), while embedded provenance carries or references a structured provenance record (the full chain of custody). A single asset may carry both. Aligns with C2PA action taxonomy: c2pa.watermarked.bound (watermark linked to a C2PA manifest) and c2pa.watermarked.unbound (watermark independent of any manifest). This is a declaration by the watermarking party. The receiving party (the seller) is the verifier-of-record: it confirms the claim by calling a governance agent it trusts (typically one published in `creative_policy.accepted_verifiers`).").optional(), "disclosure": z.object({ "required": z.boolean().describe("The declaring party's claim that AI disclosure is required for this content under applicable regulations. This is a declared signal carried through the supply chain — useful as a routing and audit input — not a regulatory determination made by the protocol. Receiving parties remain responsible for their own jurisdictional analysis and should not treat `required: false` as compliance cover."), "jurisdictions": z.array(z.object({ "country": z.string().describe("ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')"), "region": z.string().describe("Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)").optional(), "regulation": z.string().describe("Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')"), "label_text": z.string().describe("Required disclosure label text for this jurisdiction, in the local language").optional(), "render_guidance": z.object({ "persistence": z.enum(["continuous","initial","flexible"]).describe("How long the disclosure must persist during content playback or display").optional(), "min_duration_ms": z.number().int().gte(1).describe("Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial — without it, the duration is at the publisher's discretion. At serve time the publisher reads this from provenance since the brief is not available.").optional(), "positions": z.array(z.enum(["prominent","footer","audio","subtitle","overlay","end_card","pre_roll","companion"]).describe("Where a required disclosure should appear within a creative. Used by creative briefs to specify disclosure placement and by formats to declare which positions they can render.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Preferred disclosure positions in priority order. The first position a format supports should be used.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension object for platform-specific, vendor-namespaced parameters. Extensions are always optional and must be namespaced under a vendor/platform key (e.g., ext.gam, ext.roku). Used for custom capabilities, partner-specific configuration, and features being proposed for standardization.").optional() }).describe("How the disclosure should be rendered for this jurisdiction. Expresses the declaring party's intent for persistence and position based on regulatory requirements. Publishers control actual rendering but governance agents can audit whether guidance was followed.").optional() })).describe("Jurisdictions where disclosure obligations apply").optional() }).describe("Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.").optional(), "verification": z.array(z.object({ "verified_by": z.string().describe("Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')"), "verified_time": z.string().datetime().describe("When the verification was performed (ISO 8601)").optional(), "result": z.enum(["authentic","ai_generated","ai_modified","inconclusive"]).describe("Verification outcome"), "confidence": z.number().gte(0).lte(1).describe("Confidence score of the verification result (0.0 to 1.0)").optional(), "details_url": z.string().url().describe("URL to the full verification report").optional() })).describe("Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim — verification results attached by the declaring party are supplementary. The enforcing party (e.g., seller/publisher) should run its own verification via get_creative_features or calibrate_content.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension object for platform-specific, vendor-namespaced parameters. Extensions are always optional and must be namespaced under a vendor/platform key (e.g., ext.gam, ext.roku). Used for custom capabilities, partner-specific configuration, and features being proposed for standardization.").optional() }).describe("Provenance metadata for this asset, overrides manifest-level provenance").optional() }).catchall(z.any()).describe("Override logo asset.").optional(), "colors": z.object({ "primary": z.string().regex(new RegExp("^#[0-9a-fA-F]{6}$")).optional(), "secondary": z.string().regex(new RegExp("^#[0-9a-fA-F]{6}$")).optional(), "accent": z.string().regex(new RegExp("^#[0-9a-fA-F]{6}$")).optional() }).catchall(z.any()).describe("Override brand colors (hex strings).").optional(), "voice": z.string().describe("Override brand-voice description for surface-composed text/audio output.").optional(), "tagline": z.string().describe("Override tagline.").optional() }).catchall(z.any()).describe("Inline override for brand-kit fields normally resolved from `/.well-known/brand.json` on `domain` (logo, colors, voice, tagline). Use when brand.json is missing, stale, or inappropriate for this specific call — e.g., a campaign-scoped tagline, a co-branded creative, a freshly-rebranded color palette the brand.json hasn't shipped yet. Same inline-override pattern as `industries` and `data_subject_contestation` above: brand.json is canonical, the override is per-call. Adopters needing to override fields outside this subset (`voice_attributes`, `prohibited_terms`, etc.) MUST publish a different brand.json and reference it via a different `domain` — the inline override is intentionally narrow to a small high-traffic subset.\n\n**Merge semantics (normative).** The merge is **field-level**, not whole-object replacement. Each field within `brand_kit_override` (`logo`, `colors`, `voice`, `tagline`) is evaluated independently — when a field is present on the override the override value applies; when a field is absent the brand.json value applies (or is absent if brand.json doesn't carry one either). For composite fields (`colors.primary`, `colors.secondary`, `colors.accent`), the merge is one level deeper: each color slot is evaluated independently — a producer can override `colors.primary` while still inheriting `colors.secondary` from brand.json. SDKs MUST NOT treat a present `brand_kit_override.colors` as wiping the brand.json `colors` block entirely; only the per-slot fields present in the override take precedence. Without this rule, a partial-override semantics would diverge across SDKs and produce inconsistent rendering for the same payload.").optional() }).strict().describe("Identity issuer / attestation authority, referenced as a vendor BrandRef (e.g. {\"domain\": \"world.org\"}) — the same vendor-reference shape AdCP uses for measurement and signals vendors. The issuer's canonical domain is the anchor; it need not host a brand.json, but if it does, that is where its verifier metadata (scheme versions, verify endpoint, JWKs) lives. Issuer-agnostic: World ID, ISO 18013-5 mDL, and W3C-VC issuers all reference by domain. The relying party is namespaced by the issuer — identity is the tuple (issuer.domain, issuer.brand_id, relying_party_id), mirroring (vendor.domain, vendor.brand_id, metric_id)."), "scheme": z.string().describe("Proof scheme and version, e.g. \"world_id_v4\".").optional(), "relying_party_id": z.string().describe("The relying-party id registered with the issuer (and, for on-chain issuers like World ID, with the issuer's registry). One entity may operate many relying parties (scope=entity vs scope=property); this is the attestation's audience / linkability boundary, not an entity identifier."), "scope": z.enum(["entity","property"]).describe("Whether this relying_party_id is shared across the entity's properties (entity → a within-entity unique-human graph) or scoped to a single property (property → per-property pseudonyms, unlinkable across the entity).").optional() }).strict().describe("A verified-identity relying party an entity or brand operates. Used for attestation provenance in TMP Identity Match (the buyer checks a forwarded attestation's relying_party_id against the owner's published list). Issuer-agnostic; World ID is the first issuer.")).describe("Verified-identity relying parties scoped to this brand/property, for attestation provenance in TMP Identity Match. Use when a brand or property runs its own relying_party_id (per-property pseudonyms); entity-wide relying parties live on the house object. See specs/tmp-verified-identity-attestation.md.").optional(), "names": z.array(z.record(z.string(), z.string().min(1)).describe("A localized name using the legacy underscore-separated language/region key convention (for example 'en_US', 'fr_CA', or 'zh_CN') and a name value. Bare language codes such as 'en' are also accepted for backwards compatibility. These keys are not AdCP locale-tag values: a bridge to /schemas/core/locale-tag.json replaces '_' with '-', applies the AdCP canonical wire profile, and validates the result rather than passing the key through unchanged.")).describe("Localized brand names. Multiple entries per language allowed for aliases."), "keller_type": z.enum(["master","sub_brand","endorsed","independent"]).describe("Brand architecture type from Keller's theory. master: primary brand of house. sub_brand: carries parent name (Nike SB). endorsed: independent identity backed by parent (Air Jordan 'by Nike'). independent: operates separately (Converse under Nike, Inc.)").optional(), "parent_brand": z.string().regex(new RegExp("^[a-z0-9_]+$")).describe("Parent brand ID for sub-brands and endorsed brands").optional(), "description": z.string().describe("Brand description").optional(), "industries": z.array(z.string()).describe("Brand industries (e.g., ['automotive'] or ['pharmaceutical', 'cpg'] for a consumer health company). Describes what the company does — not what regulatory regimes apply (use policy_categories for that).").optional(), "target_audience": z.string().describe("Primary target audience").optional(), "logos": z.array(z.object({ "id": z.string().regex(new RegExp("^[a-z0-9][a-z0-9_-]*$")).describe("Stable identifier for this logo entry. Recommended when logo usage rules or mark lockups need to bind to a specific logo asset.").optional(), "url": z.string().url().describe("URL to the logo asset"), "orientation": z.enum(["square","horizontal","vertical","stacked"]).describe("Logo aspect ratio orientation. square: ~1:1, horizontal: wide, vertical: tall, stacked: vertically arranged elements").optional(), "background": z.enum(["dark-bg","light-bg","transparent-bg"]).describe("Background compatibility. dark-bg: use on dark backgrounds, light-bg: use on light backgrounds, transparent-bg: has transparent background").optional(), "variant": z.enum(["primary","secondary","icon","wordmark","full-lockup"]).describe("Logo variant type. primary: main logo, secondary: alternative, icon: symbol only, wordmark: text only, full-lockup: complete logo").optional(), "tags": z.array(z.string()).describe("Additional semantic tags for custom categorization beyond the standard orientation, background, and variant fields").optional(), "slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Canonical renderer slots where this logo is appropriate. Consumers SHOULD prefer this over inferring from tags or usage prose when selecting a logo for a specific UI surface.").optional(), "usage": z.string().describe("Human-readable description of when to use this logo variant (e.g., 'Primary logo for use on light backgrounds')").optional(), "width": z.number().int().describe("Width in pixels").optional(), "height": z.number().int().describe("Height in pixels").optional() }).catchall(z.any()).describe("Brand logo asset with structured fields for orientation, background compatibility, and variant type")).describe("Brand logo assets").optional(), "colors": z.object({ "primary": z.union([z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")), z.array(z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")))]).optional(), "secondary": z.union([z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")), z.array(z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")))]).optional(), "accent": z.union([z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")), z.array(z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")))]).optional(), "background": z.union([z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")), z.array(z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")))]).optional(), "text": z.union([z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")), z.array(z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")))]).optional(), "heading": z.union([z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")), z.array(z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")))]).optional(), "body": z.union([z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")), z.array(z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")))]).optional(), "label": z.union([z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")), z.array(z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")))]).optional(), "border": z.union([z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")), z.array(z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")))]).optional(), "divider": z.union([z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")), z.array(z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")))]).optional(), "surface_1": z.union([z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")), z.array(z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")))]).optional(), "surface_2": z.union([z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")), z.array(z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")))]).optional() }).catchall(z.union([z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")), z.array(z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")))])).describe("Brand color palette. Each role accepts a single hex color or an array of hex colors for brands with multiple values per role. Beyond the core five roles, brands can provide additional color roles for finer granularity — heading, body, label, border, divider, surface_1, surface_2, etc.").optional(), "fonts": z.object({ "primary": z.union([z.string().describe("CSS font-family name (e.g., 'Montserrat', 'Arial, sans-serif')"), z.object({ "family": z.string().describe("CSS font-family name (e.g., 'Brand Sans')"), "files": z.array(z.object({ "url": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL to the font file (WOFF2, TTF, or OTF)"), "weight": z.number().int().gte(100).lte(900).describe("CSS numeric font-weight for static fonts (100-900)").optional(), "weight_range": z.array(z.number().int().gte(100).lte(900)).max(2).describe("Variable font weight axis range as [min, max] (e.g., [100, 900]). Use instead of weight for variable fonts.").optional(), "style": z.enum(["normal","italic","oblique"]).describe("CSS font-style").optional() }).catchall(z.any()).describe("A font file with weight, style, and variable font metadata")).max(36).describe("Font files for different weights and styles").optional(), "opentype_features": z.array(z.string().regex(new RegExp("^[a-z0-9]{4}$"))).max(20).describe("OpenType feature tags to enable (e.g., ['ss01', 'tnum', 'cv01']). These are four-character tags per the OpenType spec.").optional(), "fallbacks": z.array(z.string().max(100)).max(10).describe("Ordered fallback font-family names for when the primary font is unavailable or does not support the required script (e.g., ['Noto Sans Arabic', 'Noto Sans SC', 'sans-serif'])").optional() }).catchall(z.any()).describe("Structured font with family name, downloadable font files, and typographic metadata")]).describe("Primary font family").optional(), "secondary": z.union([z.string().describe("CSS font-family name (e.g., 'Montserrat', 'Arial, sans-serif')"), z.object({ "family": z.string().describe("CSS font-family name (e.g., 'Brand Sans')"), "files": z.array(z.object({ "url": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL to the font file (WOFF2, TTF, or OTF)"), "weight": z.number().int().gte(100).lte(900).describe("CSS numeric font-weight for static fonts (100-900)").optional(), "weight_range": z.array(z.number().int().gte(100).lte(900)).max(2).describe("Variable font weight axis range as [min, max] (e.g., [100, 900]). Use instead of weight for variable fonts.").optional(), "style": z.enum(["normal","italic","oblique"]).describe("CSS font-style").optional() }).catchall(z.any()).describe("A font file with weight, style, and variable font metadata")).max(36).describe("Font files for different weights and styles").optional(), "opentype_features": z.array(z.string().regex(new RegExp("^[a-z0-9]{4}$"))).max(20).describe("OpenType feature tags to enable (e.g., ['ss01', 'tnum', 'cv01']). These are four-character tags per the OpenType spec.").optional(), "fallbacks": z.array(z.string().max(100)).max(10).describe("Ordered fallback font-family names for when the primary font is unavailable or does not support the required script (e.g., ['Noto Sans Arabic', 'Noto Sans SC', 'sans-serif'])").optional() }).catchall(z.any()).describe("Structured font with family name, downloadable font files, and typographic metadata")]).describe("Secondary font family").optional() }).catchall(z.union([z.string().describe("CSS font-family name (e.g., 'Montserrat', 'Arial, sans-serif')"), z.object({ "family": z.string().describe("CSS font-family name (e.g., 'Brand Sans')"), "files": z.array(z.object({ "url": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL to the font file (WOFF2, TTF, or OTF)"), "weight": z.number().int().gte(100).lte(900).describe("CSS numeric font-weight for static fonts (100-900)").optional(), "weight_range": z.array(z.number().int().gte(100).lte(900)).max(2).describe("Variable font weight axis range as [min, max] (e.g., [100, 900]). Use instead of weight for variable fonts.").optional(), "style": z.enum(["normal","italic","oblique"]).describe("CSS font-style").optional() }).catchall(z.any()).describe("A font file with weight, style, and variable font metadata")).max(36).describe("Font files for different weights and styles").optional(), "opentype_features": z.array(z.string().regex(new RegExp("^[a-z0-9]{4}$"))).max(20).describe("OpenType feature tags to enable (e.g., ['ss01', 'tnum', 'cv01']). These are four-character tags per the OpenType spec.").optional(), "fallbacks": z.array(z.string().max(100)).max(10).describe("Ordered fallback font-family names for when the primary font is unavailable or does not support the required script (e.g., ['Noto Sans Arabic', 'Noto Sans SC', 'sans-serif'])").optional() }).catchall(z.any()).describe("Structured font with family name, downloadable font files, and typographic metadata")]).describe("A font role entry. Either a CSS font-family string (simple) or a structured object with family name and font files (rich).")).describe("Brand typography. Each key is a role name (e.g., 'primary', 'secondary') referenced by type_scale entries. Values are either a CSS font-family string or a structured object with font files for reliable resolution.").optional(), "tone": z.union([z.string().describe("Simple tone descriptors for backwards compatibility"), z.object({ "voice": z.union([z.string(), z.array(z.record(z.string(), z.string().min(1)).describe("A localized name using the legacy underscore-separated language/region key convention (for example 'en_US', 'fr_CA', or 'zh_CN') and a name value. Bare language codes such as 'en' are also accepted for backwards compatibility. These keys are not AdCP locale-tag values: a bridge to /schemas/core/locale-tag.json replaces '_' with '-', applies the AdCP canonical wire profile, and validates the result rather than passing the key through unchanged."))]).describe("High-level voice descriptor, either a legacy plain string or localized values.").optional(), "attributes": z.union([z.array(z.string()), z.array(z.record(z.string(), z.array(z.string())).describe("One locale mapped to a complete ordered string list. The list is selected as a unit so readers never assemble mixed-language output from independently localized elements."))]).describe("Personality traits that characterize the brand voice. Localized lists are selected as a whole.").optional(), "dos": z.union([z.array(z.string()), z.array(z.record(z.string(), z.array(z.string())).describe("One locale mapped to a complete ordered string list. The list is selected as a unit so readers never assemble mixed-language output from independently localized elements."))]).describe("Guidance for copy generation - what TO do. Localized lists are selected as a whole.").optional(), "donts": z.union([z.array(z.string()), z.array(z.record(z.string(), z.array(z.string())).describe("One locale mapped to a complete ordered string list. The list is selected as a unit so readers never assemble mixed-language output from independently localized elements."))]).describe("Guardrails to avoid brand violations - what NOT to do. Localized lists are selected as a whole.").optional() }).describe("Structured brand voice guidelines")]).describe("Brand voice and messaging tone guidelines").optional(), "tagline": z.union([z.string().describe("Plain tagline string for backwards compatibility"), z.array(z.record(z.string(), z.string().min(1)).describe("A localized name using the legacy underscore-separated language/region key convention (for example 'en_US', 'fr_CA', or 'zh_CN') and a name value. Bare language codes such as 'en' are also accepted for backwards compatibility. These keys are not AdCP locale-tag values: a bridge to /schemas/core/locale-tag.json replaces '_' with '-', applies the AdCP canonical wire profile, and validates the result rather than passing the key through unchanged.")).describe("Localized taglines with BCP 47 locale codes")]).describe("Brand tagline or slogan. Accepts a plain string or a localized array matching the names pattern.").optional(), "assets": z.array(z.object({ "asset_id": z.string().describe("Unique identifier"), "asset_type": z.enum(["image","video","audio","text","markdown","html","css","javascript","zip","vast","daast","url","webhook","brief","catalog","published_post"]).describe("Type of asset content"), "url": z.string().url().describe("URL to CDN-hosted asset file"), "tags": z.array(z.string()).describe("Tags for discovery (e.g., 'hero', 'lifestyle', 'product', 'holiday')").optional(), "name": z.union([z.string(), z.array(z.record(z.string(), z.string().min(1)).describe("A localized name using the legacy underscore-separated language/region key convention (for example 'en_US', 'fr_CA', or 'zh_CN') and a name value. Bare language codes such as 'en' are also accepted for backwards compatibility. These keys are not AdCP locale-tag values: a bridge to /schemas/core/locale-tag.json replaces '_' with '-', applies the AdCP canonical wire profile, and validates the result rather than passing the key through unchanged."))]).describe("Human-readable name, either a legacy plain string or localized values.").optional(), "description": z.union([z.string(), z.array(z.record(z.string(), z.string().min(1)).describe("A localized name using the legacy underscore-separated language/region key convention (for example 'en_US', 'fr_CA', or 'zh_CN') and a name value. Bare language codes such as 'en' are also accepted for backwards compatibility. These keys are not AdCP locale-tag values: a bridge to /schemas/core/locale-tag.json replaces '_' with '-', applies the AdCP canonical wire profile, and validates the result rather than passing the key through unchanged."))]).describe("Asset description or usage notes, either a legacy plain string or localized values.").optional(), "width": z.number().int().describe("Image/video width in pixels").optional(), "height": z.number().int().describe("Image/video height in pixels").optional(), "duration_seconds": z.number().describe("Video/audio duration in seconds").optional(), "file_size_bytes": z.number().int().describe("File size in bytes").optional(), "format": z.string().describe("File format (e.g., 'jpg', 'mp4', 'mp3')").optional(), "metadata": z.record(z.string(), z.any()).describe("Additional asset-specific metadata").optional() }).catchall(z.any()).describe("Brand asset (image, video, audio, text)")).describe("Brand asset library").optional(), "properties": z.array(z.object({ "type": z.enum(["website","mobile_app","ctv_app","desktop_app","dooh","podcast","radio","linear_tv","streaming_audio","ai_assistant"]).describe("Property type"), "identifier": z.string().min(1).describe("Property identifier - domain for websites, bundle ID for apps"), "store": z.enum(["apple","google","amazon","roku","samsung","lg","other"]).describe("App store for mobile/CTV apps").optional(), "region": z.string().regex(new RegExp("^([A-Z]{2}|global)$")).describe("ISO 3166-1 alpha-2 country code or 'global'").optional(), "primary": z.boolean().describe("Whether this is the primary property for the brand").default(false), "relationship": z.enum(["owned","direct","delegated","ad_network"]).describe("How this brand relates to the property. 'owned': the brand owns and operates this property (default) and has no adagents.json delegation_type counterpart. 'direct': the brand is the direct sales path for this property, even if a third party operates the software (e.g., a publisher's in-house ad team using a vendor's tech). 'delegated': the brand manages monetization for this property — they are in charge of ad sales (e.g., Mediavine managing a food blog). 'ad_network': the brand sells this property's inventory as part of a network or exchange — they are a path to the inventory, not the path (e.g., PubMatic as an SSP). For non-owned properties, the publisher confirms the relationship by setting the matching delegation_type on the agent's authorization in their adagents.json.").default("owned") }).catchall(z.any()).describe("A digital property associated with a brand. Defaults to owned; use 'relationship' to declare direct, delegated, or ad_network properties. For delegated and network paths, these values match the delegation_type field in adagents.json, creating a bilateral verification chain: the operator declares the relationship here, the publisher confirms by setting the same delegation_type on the agent's authorization in their adagents.json. 'owned' is an inline ownership declaration with no adagents.json counterpart.")).describe("Digital properties associated with this brand — owned, managed, or represented").optional(), "product_catalog": z.object({ "feed_url": z.string().url().describe("URL to product catalog feed"), "feed_format": z.enum(["google_merchant_center","facebook_catalog","shopify","linkedin_jobs","tiktok_shop","pinterest_catalog","openai_product_feed","custom"]).describe("Format of the product feed").optional(), "categories": z.array(z.string()).describe("Product categories available in the catalog").optional(), "last_updated": z.string().datetime().describe("When the product catalog was last updated").optional(), "update_frequency": z.enum(["realtime","hourly","daily","weekly"]).describe("How frequently the product catalog is updated").optional(), "agentic_checkout": z.object({ "endpoint": z.string().url().describe("Base URL for checkout session API"), "spec": z.string().describe("Checkout API specification identifier. Use a namespaced string to identify the checkout protocol (e.g., vendor-prefixed or custom). Vendor-specific values belong under ext.{vendor}."), "supported_payment_providers": z.array(z.string()).describe("Payment providers supported by this checkout endpoint").optional() }).describe("Agentic checkout endpoint configuration").optional() }).catchall(z.any()).describe("Product catalog for e-commerce brands").optional(), "privacy_policy_url": z.string().url().describe("URL to the brand's privacy policy").optional(), "data_subject_contestation": z.object({ "url": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL to a human-accessible contestation form or information page.").optional(), "email": z.string().email().describe("Email address for contestation requests. Deployer MUST monitor and respond within the timelines set by applicable law (e.g., 1 month under GDPR Art. 12(3)).").optional(), "languages": z.array(z.string()).describe("BCP 47 language tags the contestation channel supports (e.g., ['en', 'de', 'fr']). At minimum SHOULD include a language spoken in every jurisdiction where the brand runs regulated-vertical campaigns.").optional() }).strict().and(z.union([z.any(), z.any()])).describe("Contact point where a data subject can request human intervention, express their view, or contest an automated decision — satisfying GDPR Article 22(3) and EU AI Act Article 26(11) transparency obligations. This is a contact reference (URL, email, or both), not a machine-callable API. AdCP surfaces the pointer; the deployer runs the contestation workflow.").optional(), "disclaimers": z.array(z.object({ "text": z.string(), "context": z.string().optional(), "required": z.boolean().default(true) })).describe("Legal disclaimers for creatives").optional(), "trademarks": z.array(z.object({ "registry": z.string().describe("Trademark registry (e.g., 'USPTO', 'EUIPO', 'JPO', 'CNIPA')"), "number": z.string().describe("Registration number as issued by the registry"), "mark": z.string().describe("The registered mark as published"), "status": z.enum(["active","pending","abandoned","cancelled","expired"]).describe("Registration status. Omit for active marks if status tracking is not maintained.").optional(), "license_type": z.enum(["owned","licensed_in","licensed_out"]).describe("Whether the publisher owns the mark, licenses it from another entity, or licenses it to others. 'owned' is the default if omitted.").optional(), "licensor_domain": z.string().regex(new RegExp("^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$")).describe("Domain of the entity that licenses this mark to the publisher. Meaningful when license_type=licensed_in; omit otherwise.").optional(), "countries": z.array(z.string().min(2).max(2)).describe("ISO 3166-1 alpha-2 country codes where this registration applies. Omit for global or where the registry's jurisdiction is implicit.").optional(), "nice_classes": z.array(z.number().int().gte(1).lte(45)).describe("Nice Classification class numbers (1-45) covered by this registration. Disambiguates marks across industries (e.g., Delta-airline vs Delta-faucet). Omit if scope is implicit from registry.").optional() }).catchall(z.any()).describe("A registered trademark. May appear at house level (corporate marks, e.g., 'NIKE' owned by Nike, Inc.) or at brand level (brand-specific marks, e.g., 'CONVERSE' owned by Converse). Resolution between house- and brand-level trademarks is union — both lists are valid claims about marks the publisher controls.")).describe("Brand-level registered trademarks. Use for marks the brand owns or controls (e.g., a sub-brand's own marks distinct from the corporate parent). House-level trademarks live on the house object; resolution between the two is union — both lists are valid claims.").optional(), "voice_synthesis": z.object({ "provider": z.string().optional(), "voice_id": z.string().optional(), "settings": z.record(z.string(), z.any()).optional() }).catchall(z.any()).describe("TTS voice synthesis configuration for AI-generated audio").optional(), "avatar": z.object({ "provider": z.string().optional(), "avatar_id": z.string().optional(), "settings": z.record(z.string(), z.any()).optional() }).catchall(z.any()).describe("Visual avatar configuration").optional(), "visual_guidelines": z.object({ "photography": z.object({ "realism": z.enum(["natural","stylized","hyperreal","abstract"]).describe("Level of photographic realism").optional(), "lighting": z.string().describe("Lighting style (e.g., 'soft daylight', 'studio', 'golden hour', 'high-key', 'low-key')").optional(), "color_temperature": z.enum(["warm","neutral","cool"]).describe("Overall color temperature of photography").optional(), "contrast": z.enum(["low","medium","high"]).describe("Contrast level in photography").optional(), "depth_of_field": z.enum(["shallow","medium","deep"]).describe("Depth of field preference. shallow: blurred background with subject isolation, deep: everything in focus").optional(), "subject": z.object({ "people": z.object({ "age_range": z.string().describe("Target age range (e.g., '20-35')").optional(), "diversity": z.string().describe("Diversity representation (e.g., 'mixed', 'varied')").optional(), "mood": z.array(z.string()).describe("Mood descriptors (e.g., ['confident', 'relaxed'])").optional() }).catchall(z.any()).describe("People photography guidelines").optional(), "product_focus": z.enum(["in-use","isolated","lifestyle","detail"]).describe("How products are shown").optional(), "setting": z.string().describe("Environmental context for photography (e.g., 'indoor', 'outdoor', 'studio', 'urban', 'nature', 'workplace')").optional() }).catchall(z.any()).describe("Subject matter guidelines").optional(), "framing": z.object({ "subject_position": z.string().describe("Where the subject sits in frame (e.g., 'center', 'center-left', 'rule-of-thirds')").optional(), "crop_style": z.string().describe("Cropping convention (e.g., 'waist-up', 'full-body', 'close-up', 'wide')").optional(), "perspective": z.string().describe("Camera perspective (e.g., 'eye-level', 'overhead', 'low-angle')").optional() }).catchall(z.any()).describe("Camera framing rules").optional(), "preferred_aspect_ratios": z.array(z.string().regex(new RegExp("^\\d+:\\d+$"))).describe("Preferred aspect ratios for brand photography (e.g., '16:9', '4:5', '1:1')").optional(), "tags": z.array(z.string()).describe("Additional style descriptors").optional() }).catchall(z.any()).describe("Photography style rules for generative creative systems. Defines how brand photography should look when selected or generated.").optional(), "graphic_style": z.object({ "style_type": z.enum(["flat_illustration","geometric","gradient_mesh","editorial_collage","hand_drawn","minimal_line_art","3d_render","isometric","photographic_composite"]).describe("Primary graphic style").optional(), "stroke_style": z.enum(["rounded","square","mixed","none"]).describe("Stroke end/join style").optional(), "stroke_weight": z.string().describe("Stroke weight (e.g., '2px', 'thin', 'bold')").optional(), "corner_radius": z.string().describe("Default corner radius for graphic and illustration elements (e.g., '12px', '8px', 'sharp'). For UI component radii (buttons, cards, inputs), see visual_guidelines.border_radius.").optional(), "tags": z.array(z.string()).describe("Additional style descriptors").optional() }).catchall(z.any()).describe("Visual language for brand graphics and illustrations").optional(), "shapes": z.object({ "primary_shape": z.string().describe("Primary brand shape (e.g., 'rounded_rectangle', 'circle', 'hexagon')").optional(), "secondary_shapes": z.array(z.string()).describe("Secondary shapes in the brand vocabulary").optional(), "usage": z.object({ "max_per_layout": z.number().int().describe("Maximum distinct shapes per layout").optional(), "overlap_allowed": z.boolean().describe("Whether shapes may overlap").optional() }).catchall(z.any()).describe("Shape usage rules").optional() }).catchall(z.any()).describe("Distinctive shapes used as part of brand visual identity").optional(), "iconography": z.object({ "style": z.enum(["outline","filled","duotone","flat","glyph","hand_drawn"]).describe("Icon rendering style").optional(), "stroke_weight": z.string().describe("Icon stroke weight (e.g., '2px', '1.5px')").optional(), "corner_style": z.enum(["rounded","square","mixed"]).describe("Corner style for icon paths").optional(), "usage": z.object({ "max_per_frame": z.number().int().describe("Maximum icons per creative frame").optional(), "size_ratio": z.string().describe("Icon-to-layout size ratio (e.g., '1:8')").optional() }).catchall(z.any()).describe("Icon usage rules").optional() }).catchall(z.any()).describe("Icon style system and usage rules").optional(), "composition": z.object({ "overlays": z.object({ "gradient_style": z.enum(["linear","radial","conic","none"]).describe("Gradient type for overlays").optional(), "gradient_direction": z.string().describe("Gradient direction (e.g., '45deg', 'to-bottom-right')").optional(), "opacity": z.string().describe("Overlay opacity (e.g., '70%')").optional() }).catchall(z.any()).describe("Graphic overlay rules").optional(), "texture": z.object({ "style": z.enum(["none","subtle_grain","noise","paper","fabric","concrete"]).describe("Texture style applied to creative assets").optional(), "intensity": z.enum(["low","medium","high"]).describe("Texture intensity").optional() }).catchall(z.any()).describe("Texture treatment rules").optional(), "backgrounds": z.object({ "types_allowed": z.array(z.enum(["solid_color","gradient","blurred_photo","image","video","pattern","transparent"])).describe("Permitted background types").optional() }).catchall(z.any()).describe("Background treatment rules").optional() }).catchall(z.any()).describe("Layout composition rules including overlays, textures, and backgrounds").optional(), "border_radius": z.object({ "none": z.string().describe("Explicitly sharp corners (e.g., '0')").optional(), "default": z.string().describe("Default border radius for UI components (e.g., '8px', '12px', '0'). For graphic/illustration elements, see graphic_style.corner_radius.").optional(), "small": z.string().describe("Small border radius for compact elements (e.g., '4px')").optional(), "large": z.string().describe("Large border radius for cards and containers (e.g., '16px', '24px')").optional(), "pill": z.string().describe("Fully rounded / pill shape (e.g., '999px')").optional() }).catchall(z.string()).describe("Named border radius presets for UI components and layout elements. One of the most visible brand differentiators — Airbnb uses generous 20px, Stripe uses precise 4–8px, Spotify uses pill/999px.").optional(), "elevation": z.object({ "none": z.string().describe("No shadow (e.g., 'none')").optional(), "subtle": z.string().describe("Subtle shadow for slight lift (e.g., '0 1px 2px rgba(0,0,0,0.05)')").optional(), "card": z.string().describe("Card-level shadow (e.g., '0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -2px rgba(0,0,0,0.1)')").optional(), "modal": z.string().describe("Modal/overlay shadow (e.g., '0 20px 25px -5px rgba(0,0,0,0.1), 0 8px 10px -6px rgba(0,0,0,0.1)')").optional() }).catchall(z.string()).describe("Named shadow/elevation levels. Brands use elevation as identity — from Stripe's blue-tinted multi-layer shadows to Apple's single diffuse shadow. Values are CSS box-shadow syntax.").optional(), "spacing": z.object({ "unit": z.string().describe("Base grid unit this scale was designed from (e.g., '8px', '4px'). Informational — agents should use the named scale values, not compute from this.").optional(), "scale": z.object({ "xs": z.string().describe("Extra small spacing (e.g., '4px')").optional(), "sm": z.string().describe("Small spacing (e.g., '8px')").optional(), "md": z.string().describe("Medium spacing (e.g., '16px')").optional(), "lg": z.string().describe("Large spacing (e.g., '24px')").optional(), "xl": z.string().describe("Extra large spacing (e.g., '32px')").optional(), "2xl": z.string().describe("Section-level spacing (e.g., '48px', '64px')").optional() }).catchall(z.string()).describe("Named spacing scale built from the base unit").optional() }).strict().describe("Spacing system for consistent layout rhythm. Most design systems use an 8px base grid.").optional(), "graphic_elements": z.array(z.object({ "name": z.string().describe("Element name (e.g., 'Paper Tear', 'Brand Watermark', 'Section Divider')"), "type": z.enum(["border","divider","frame","watermark","pattern","texture_overlay","decorative"]).describe("Element type").optional(), "description": z.string().describe("How the element is used in layouts").optional(), "orientation": z.enum(["horizontal","vertical","any"]).describe("Preferred orientation when used in layouts").optional(), "colors": z.array(z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")).describe("A single hex color value")).describe("Colors this element may appear in").optional(), "max_per_layout": z.number().int().describe("Maximum instances per layout").optional() }).catchall(z.any()).describe("A reusable decorative or structural visual element that is part of the brand identity (e.g., torn paper edges, watermarks, dividers, background patterns)")).describe("Reusable decorative elements that are part of the brand visual identity (e.g., torn paper edges, watermarks, dividers)").optional(), "motion": z.object({ "transition_style": z.enum(["cut","dissolve","slide","wipe","zoom","fade"]).describe("Primary transition style between scenes").optional(), "animation_speed": z.enum(["slow","moderate","fast"]).describe("Overall animation pacing").optional(), "easing": z.string().describe("Default easing function (e.g., 'ease-in-out', 'spring', 'linear')").optional(), "text_entrance": z.enum(["fade","typewriter","slide_up","slide_left","scale","none"]).describe("How text enters the frame").optional(), "pacing": z.enum(["lingering","moderate","fast_cuts"]).describe("Overall editing rhythm").optional(), "kinetic_typography": z.boolean().describe("Whether animated/kinetic typography is allowed").optional(), "tags": z.array(z.string()).describe("Additional motion style descriptors").optional() }).catchall(z.any()).describe("Motion and animation rules for video, animated display, and interactive formats").optional(), "logo_placement": z.object({ "preferred_position": z.enum(["top-left","top-center","top-right","bottom-left","bottom-center","bottom-right","center"]).describe("Preferred logo position in layouts").optional(), "min_clear_space": z.string().describe("Minimum clear space around the logo, expressed as a multiple of logo height (e.g., '0.5x', '1x') or fixed value (e.g., '16px')").optional(), "min_height": z.string().describe("Minimum logo height to maintain legibility (e.g., '40px', '24px')").optional(), "background_contrast": z.enum(["light_only","dark_only","any"]).describe("Permitted background contrast behind logo").optional() }).catchall(z.any()).describe("Logo placement and clear space rules for automated creative production").optional(), "colorways": z.array(z.object({ "name": z.string().describe("Colorway name (e.g., 'primary', 'inverted', 'subtle')"), "foreground": z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")).describe("A single hex color value"), "background": z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")).describe("A single hex color value"), "accent": z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")).describe("A single hex color value").optional(), "border": z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")).describe("A single hex color value").optional(), "cta_foreground": z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")).describe("CTA text/icon color, if different from foreground").optional(), "cta_background": z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")).describe("CTA button/container color, if different from accent").optional(), "channels": z.array(z.string()).describe("Channels or contexts where this colorway applies (e.g., 'online', 'print', 'pos', 'social', 'outdoor'). Omit for universal colorways.").optional() }).catchall(z.any()).describe("A named color pairing that defines how colors work together. Colorways ensure foreground/background combinations are always on-brand and accessible.")).describe("Named color pairings for consistent foreground/background combinations").optional(), "color_constraints": z.array(z.object({ "color": z.object({ "kind": z.enum(["name","value","surface"]).describe("Discriminator for the color reference variant.").optional(), "name": z.string().describe("Key from the colors object, such as market_yellow, river_green, or any custom palette key. Unambiguous lookup into colors{}.").optional(), "value": z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")).describe("Literal hex color when no palette key exists.").optional(), "surface": z.enum(["background","foreground","text","logo_background","cta","accent","border","icon","graphic_element"]).describe("Usage surface rather than a specific color value.").optional() }).strict().and(z.union([z.object({ "kind": z.literal("name") }), z.object({ "kind": z.literal("value") }), z.object({ "kind": z.literal("surface") })])).describe("Color role or value this constraint governs."), "applies_to": z.array(z.enum(["background","foreground","text","logo_background","cta","accent","border","icon","graphic_element"])).describe("Surfaces where this color may be used.").optional(), "allowed_on": z.array(z.object({ "kind": z.enum(["name","value","surface"]).describe("Discriminator for the color reference variant.").optional(), "name": z.string().describe("Key from the colors object, such as market_yellow, river_green, or any custom palette key. Unambiguous lookup into colors{}.").optional(), "value": z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")).describe("Literal hex color when no palette key exists.").optional(), "surface": z.enum(["background","foreground","text","logo_background","cta","accent","border","icon","graphic_element"]).describe("Usage surface rather than a specific color value.").optional() }).strict().and(z.union([z.object({ "kind": z.literal("name") }), z.object({ "kind": z.literal("value") }), z.object({ "kind": z.literal("surface") })])).describe("Reference to a brand color or usage surface for machine-readable guideline constraints. Use kind=name for palette lookup keys in colors, kind=value for a literal hex color, and kind=surface for layout surfaces such as background or text.")).describe("Backgrounds, surfaces, or color roles where this color is allowed.").optional(), "forbidden_on": z.array(z.object({ "kind": z.enum(["name","value","surface"]).describe("Discriminator for the color reference variant.").optional(), "name": z.string().describe("Key from the colors object, such as market_yellow, river_green, or any custom palette key. Unambiguous lookup into colors{}.").optional(), "value": z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")).describe("Literal hex color when no palette key exists.").optional(), "surface": z.enum(["background","foreground","text","logo_background","cta","accent","border","icon","graphic_element"]).describe("Usage surface rather than a specific color value.").optional() }).strict().and(z.union([z.object({ "kind": z.literal("name") }), z.object({ "kind": z.literal("value") }), z.object({ "kind": z.literal("surface") })])).describe("Reference to a brand color or usage surface for machine-readable guideline constraints. Use kind=name for palette lookup keys in colors, kind=value for a literal hex color, and kind=surface for layout surfaces such as background or text.")).describe("Backgrounds, surfaces, or color roles where this color is forbidden.").optional(), "never_pair_with": z.array(z.object({ "kind": z.enum(["name","value","surface"]).describe("Discriminator for the color reference variant.").optional(), "name": z.string().describe("Key from the colors object, such as market_yellow, river_green, or any custom palette key. Unambiguous lookup into colors{}.").optional(), "value": z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")).describe("Literal hex color when no palette key exists.").optional(), "surface": z.enum(["background","foreground","text","logo_background","cta","accent","border","icon","graphic_element"]).describe("Usage surface rather than a specific color value.").optional() }).strict().and(z.union([z.object({ "kind": z.literal("name") }), z.object({ "kind": z.literal("value") }), z.object({ "kind": z.literal("surface") })])).describe("Reference to a brand color or usage surface for machine-readable guideline constraints. Use kind=name for palette lookup keys in colors, kind=value for a literal hex color, and kind=surface for layout surfaces such as background or text.")).describe("Color roles or values that must not be paired with this color.").optional(), "contexts": z.array(z.string()).describe("Channels or creative contexts where this constraint applies, such as digital, print, social, or ctv_end_card.").optional(), "severity": z.enum(["must","should"]).describe("Strength of a machine-readable brand guideline constraint.").optional(), "description": z.string().describe("Human-readable rationale or source-language summary for the rule.").optional() }).catchall(z.any()).describe("Machine-readable rule constraining how a brand color may be used or paired. Use for accent-only colors, forbidden foreground/background combinations, and palette pairs that should never appear together.")).describe("Machine-readable constraints for color usage and pairings, such as accent-only rules or forbidden foreground/background combinations.").optional(), "logo_usage_rules": z.array(z.object({ "logo_url": z.string().url().describe("Specific logo asset URL this rule applies to. Omit when the rule applies by variant or tags.").optional(), "logo_id": z.string().regex(new RegExp("^[a-z0-9][a-z0-9_-]*$")).describe("Stable `logos[].id` this rule applies to. Prefer this over logo_url when the rule targets a specific logo entry.").optional(), "logo_variant": z.enum(["primary","secondary","icon","wordmark","full-lockup"]).describe("Logo variant this rule applies to.").optional(), "logo_tags": z.array(z.string()).describe("Logo tags this rule applies to.").optional(), "contexts": z.array(z.string()).describe("Creative contexts where this rule applies.").optional(), "slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Canonical renderer slots where this rule applies. Use this for deterministic logo-card, profile-mark, end-card, and lockup selection.").optional(), "minimum_size": z.object({ "width": z.string().describe("Minimum width, such as 48px or 12mm.").optional(), "height": z.string().describe("Minimum height, such as 18px or 6mm.").optional() }).strict().describe("Minimum rendered size needed for legibility.").optional(), "clear_space": z.string().describe("Minimum clear space around the logo, expressed in brand terms or units.").optional(), "allowed_backgrounds": z.array(z.object({ "kind": z.enum(["name","value","surface"]).describe("Discriminator for the color reference variant.").optional(), "name": z.string().describe("Key from the colors object, such as market_yellow, river_green, or any custom palette key. Unambiguous lookup into colors{}.").optional(), "value": z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")).describe("Literal hex color when no palette key exists.").optional(), "surface": z.enum(["background","foreground","text","logo_background","cta","accent","border","icon","graphic_element"]).describe("Usage surface rather than a specific color value.").optional() }).strict().and(z.union([z.object({ "kind": z.literal("name") }), z.object({ "kind": z.literal("value") }), z.object({ "kind": z.literal("surface") })])).describe("Reference to a brand color or usage surface for machine-readable guideline constraints. Use kind=name for palette lookup keys in colors, kind=value for a literal hex color, and kind=surface for layout surfaces such as background or text.")).describe("Background color roles, values, or surfaces where this logo may be placed.").optional(), "forbidden_backgrounds": z.array(z.object({ "kind": z.enum(["name","value","surface"]).describe("Discriminator for the color reference variant.").optional(), "name": z.string().describe("Key from the colors object, such as market_yellow, river_green, or any custom palette key. Unambiguous lookup into colors{}.").optional(), "value": z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")).describe("Literal hex color when no palette key exists.").optional(), "surface": z.enum(["background","foreground","text","logo_background","cta","accent","border","icon","graphic_element"]).describe("Usage surface rather than a specific color value.").optional() }).strict().and(z.union([z.object({ "kind": z.literal("name") }), z.object({ "kind": z.literal("value") }), z.object({ "kind": z.literal("surface") })])).describe("Reference to a brand color or usage surface for machine-readable guideline constraints. Use kind=name for palette lookup keys in colors, kind=value for a literal hex color, and kind=surface for layout surfaces such as background or text.")).describe("Background color roles, values, or surfaces where this logo must not be placed.").optional(), "forbidden_contexts": z.array(z.string()).describe("Contexts where this logo must not be used, such as photography_without_knockout.").optional(), "severity": z.enum(["must","should"]).describe("Strength of a machine-readable brand guideline constraint.").optional(), "description": z.string().describe("Human-readable rationale or source-language summary for the rule.").optional() }).catchall(z.any()).and(z.union([z.any(), z.any(), z.any(), z.any(), z.any()])).describe("Machine-readable logo selection and placement rule. Complements logos[].usage by making enforceable minimum size, clear-space, background, and context constraints queryable.")).describe("Machine-readable logo selection and placement constraints for minimum size, clear space, backgrounds, and contexts.").optional(), "mark_lockups": z.array(z.object({ "lockup_type": z.enum(["co_brand","secondary_mark","partner","sponsor","program","talent","custom"]).describe("Type of mark relationship governed by this lockup rule."), "ordering": z.enum(["brand_first","partner_first","equal","contextual"]).describe("Required visual ordering of the brand mark relative to partner or secondary marks.").optional(), "contexts": z.array(z.string()).describe("Creative contexts where this lockup rule applies.").optional(), "brand_logo_id": z.string().regex(new RegExp("^[a-z0-9][a-z0-9_-]*$")).describe("Stable `logos[].id` for the brand logo this lockup rule is anchored on.").optional(), "secondary_logo_ids": z.array(z.string().regex(new RegExp("^[a-z0-9][a-z0-9_-]*$")).describe("Stable identifier for a logo entry within this brand.json document. Use lowercase words separated by underscores or hyphens; do not key integrations on mutable asset URLs.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Stable `logos[].id` values for secondary, program, sponsor, or partner marks governed by this lockup rule when those marks are represented in this brand.json.").optional(), "separator": z.object({ "type": z.enum(["none","keyline","space","divider"]).describe("Separator style."), "color": z.object({ "kind": z.enum(["name","value","surface"]).describe("Discriminator for the color reference variant.").optional(), "name": z.string().describe("Key from the colors object, such as market_yellow, river_green, or any custom palette key. Unambiguous lookup into colors{}.").optional(), "value": z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")).describe("Literal hex color when no palette key exists.").optional(), "surface": z.enum(["background","foreground","text","logo_background","cta","accent","border","icon","graphic_element"]).describe("Usage surface rather than a specific color value.").optional() }).strict().and(z.union([z.object({ "kind": z.literal("name") }), z.object({ "kind": z.literal("value") }), z.object({ "kind": z.literal("surface") })])).describe("Reference to a brand color or usage surface for machine-readable guideline constraints. Use kind=name for palette lookup keys in colors, kind=value for a literal hex color, and kind=surface for layout surfaces such as background or text.").optional(), "width": z.string().describe("Separator width, such as 1px.").optional() }).catchall(z.any()).describe("Separator between marks, when required.").optional(), "min_gap": z.string().describe("Minimum gap between marks, expressed in brand terms or units.").optional(), "brand_min_optical_weight_ratio": z.number().gt(0).describe("Minimum optical weight of the brand mark relative to partner marks. 1 means at least equal.").optional(), "partner_max_optical_weight_ratio": z.number().gt(0).describe("Maximum optical weight of partner marks relative to the brand mark. 1 means no larger than the brand mark. Enforcement is at layout time, not parse time — this value signals to renderers and creative agents how much space to provision for each mark.").optional(), "severity": z.enum(["must","should"]).describe("Strength of a machine-readable brand guideline constraint.").optional(), "description": z.string().describe("Human-readable rationale or source-language summary for the lockup rule.").optional() }).catchall(z.any()).describe("Machine-readable layout constraints for co-brand, partner, sponsor, program, or secondary-mark lockups.")).describe("Machine-readable co-brand, partner, sponsor, program, or secondary-mark lockup rules.").optional(), "type_scale": z.object({ "base_width": z.string().describe("Reference canvas width these sizes were designed for (e.g., '1080px'). Generative systems should scale proportionally for other canvas sizes.").optional(), "heading": z.object({ "font": z.string().describe("Font reference. Use a key from the fonts object (e.g., 'primary', 'secondary') to reference a defined font role, or a literal CSS font-family string as a fallback.").optional(), "size": z.string().describe("Font size (e.g., '48px', '2rem')").optional(), "weight": z.string().describe("Font weight (e.g., '700', 'bold')").optional(), "line_height": z.string().describe("Line height (e.g., '1.2', '56px')").optional(), "letter_spacing": z.string().describe("Letter spacing (e.g., '-0.02em', '0.5px')").optional(), "text_transform": z.enum(["none","uppercase","lowercase","capitalize"]).describe("Text transformation").optional() }).catchall(z.any()).describe("A single entry in the type scale").optional(), "subheading": z.object({ "font": z.string().describe("Font reference. Use a key from the fonts object (e.g., 'primary', 'secondary') to reference a defined font role, or a literal CSS font-family string as a fallback.").optional(), "size": z.string().describe("Font size (e.g., '48px', '2rem')").optional(), "weight": z.string().describe("Font weight (e.g., '700', 'bold')").optional(), "line_height": z.string().describe("Line height (e.g., '1.2', '56px')").optional(), "letter_spacing": z.string().describe("Letter spacing (e.g., '-0.02em', '0.5px')").optional(), "text_transform": z.enum(["none","uppercase","lowercase","capitalize"]).describe("Text transformation").optional() }).catchall(z.any()).describe("A single entry in the type scale").optional(), "body": z.object({ "font": z.string().describe("Font reference. Use a key from the fonts object (e.g., 'primary', 'secondary') to reference a defined font role, or a literal CSS font-family string as a fallback.").optional(), "size": z.string().describe("Font size (e.g., '48px', '2rem')").optional(), "weight": z.string().describe("Font weight (e.g., '700', 'bold')").optional(), "line_height": z.string().describe("Line height (e.g., '1.2', '56px')").optional(), "letter_spacing": z.string().describe("Letter spacing (e.g., '-0.02em', '0.5px')").optional(), "text_transform": z.enum(["none","uppercase","lowercase","capitalize"]).describe("Text transformation").optional() }).catchall(z.any()).describe("A single entry in the type scale").optional(), "caption": z.object({ "font": z.string().describe("Font reference. Use a key from the fonts object (e.g., 'primary', 'secondary') to reference a defined font role, or a literal CSS font-family string as a fallback.").optional(), "size": z.string().describe("Font size (e.g., '48px', '2rem')").optional(), "weight": z.string().describe("Font weight (e.g., '700', 'bold')").optional(), "line_height": z.string().describe("Line height (e.g., '1.2', '56px')").optional(), "letter_spacing": z.string().describe("Letter spacing (e.g., '-0.02em', '0.5px')").optional(), "text_transform": z.enum(["none","uppercase","lowercase","capitalize"]).describe("Text transformation").optional() }).catchall(z.any()).describe("A single entry in the type scale").optional(), "cta": z.object({ "font": z.string().describe("Font reference. Use a key from the fonts object (e.g., 'primary', 'secondary') to reference a defined font role, or a literal CSS font-family string as a fallback.").optional(), "size": z.string().describe("Font size (e.g., '48px', '2rem')").optional(), "weight": z.string().describe("Font weight (e.g., '700', 'bold')").optional(), "line_height": z.string().describe("Line height (e.g., '1.2', '56px')").optional(), "letter_spacing": z.string().describe("Letter spacing (e.g., '-0.02em', '0.5px')").optional(), "text_transform": z.enum(["none","uppercase","lowercase","capitalize"]).describe("Text transformation").optional() }).catchall(z.any()).describe("A single entry in the type scale").optional() }).catchall(z.object({ "font": z.string().describe("Font reference. Use a key from the fonts object (e.g., 'primary', 'secondary') to reference a defined font role, or a literal CSS font-family string as a fallback.").optional(), "size": z.string().describe("Font size (e.g., '48px', '2rem')").optional(), "weight": z.string().describe("Font weight (e.g., '700', 'bold')").optional(), "line_height": z.string().describe("Line height (e.g., '1.2', '56px')").optional(), "letter_spacing": z.string().describe("Letter spacing (e.g., '-0.02em', '0.5px')").optional(), "text_transform": z.enum(["none","uppercase","lowercase","capitalize"]).describe("Text transformation").optional() }).catchall(z.any()).describe("A single entry in the type scale")).describe("Typography scale defining sizes and weights for different text roles. When sizes are in px, use base_width to indicate the reference canvas.").optional(), "asset_libraries": z.array(z.object({ "name": z.string().describe("Display name of the asset library"), "type": z.enum(["icon_set","illustration_system","image_library","video_library","template_library"]).describe("Type of asset library").optional(), "url": z.string().url().describe("URL to the asset library (for human access)"), "description": z.string().describe("Description of the library contents and usage").optional(), "color_guide": z.object({ "roles": z.array(z.string()).describe("Named color roles used in the library (e.g., base, shadow_1, highlight_1, stroke)").optional(), "palettes": z.array(z.object({ "name": z.string().describe("Palette name"), "colors": z.record(z.string(), z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")).describe("A single hex color value")).describe("Map of role names to hex color values") }).catchall(z.any())).describe("Named color palettes mapping roles to specific colors").optional() }).catchall(z.any()).describe("Color guide for the asset library defining roles and palettes").optional() }).catchall(z.any()).describe("A managed asset library (icon set, illustration system, image collection). The URL is for human access; agent-facing DAM integration is under investigation.")).describe("References to managed asset libraries (icon sets, illustration systems, image collections). URLs are intended for human access; agent-facing DAM integration is under investigation.").optional(), "restrictions": z.array(z.string()).describe("Visual prohibitions and guardrails (e.g., 'Never use black backgrounds', 'Do not crop the logo', 'No stock photography of people on phones')").optional() }).catchall(z.any()).describe("Structured visual rules for generative creative systems").optional(), "agents": z.array(z.object({ "type": z.enum(["brand","rights","measurement","governance","creative","sales","buying","signals"]).describe("Functional role of this agent"), "url": z.string().url().regex(new RegExp("^https://")).describe("Agent endpoint URL (MCP or A2A). Callers comparing a brand's declared agent URL against another value (e.g., resolving 'is this the agent that signed this artifact?' or matching against a discovery cache) MUST canonicalize both sides per the AdCP URL canonicalization rules, not byte-equality. See docs/reference/url-canonicalization."), "id": z.string().regex(new RegExp("^[a-z0-9_]+$")).max(100).describe("Agent identifier (useful for logging, multi-tenant platforms)"), "description": z.string().max(500).describe("Human-readable description of this agent's capabilities or scope").optional(), "jwks_uri": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL of the agent's JWKS (RFC 7517) containing public keys used to verify artifacts this agent signs or requests it sends. Verified artifacts include signed governance_context tokens (for governance agents) and RFC 9421 HTTP Signatures on outgoing requests (for any agent). When absent, verifiers MUST default to /.well-known/jwks.json on the origin of `url`. Keys are identified by `kid` in the JWS header or RFC 9421 `keyid` parameter; JWKS MAY contain multiple keys to support rotation and per-purpose separation via `key_ops` and `use`.").optional(), "available_uses": z.array(z.enum(["likeness","voice","name","endorsement","motion_capture","signature","catchphrase","sync","background_music","editorial","commercial","ai_generated_image"]).describe("Types of rights usage that can be licensed through the brand protocol. Aligned with DDEX UseType direction for interoperability with music and media rights systems.")).describe("For rights agents: rights uses available for licensing").optional(), "right_types": z.array(z.enum(["talent","character","brand_ip","music","stock_media"]).describe("Categories of intellectual property rights that can be licensed through the brand protocol.")).describe("For rights agents: types of rights available").optional(), "countries": z.array(z.string().regex(new RegExp("^[A-Z]{2}$"))).describe("ISO 3166-1 alpha-2 country codes where this agent operates. Omit for global scope.").optional() }).catchall(z.any()).describe("An agent declared by a brand or house. Each entry identifies one agent endpoint and its functional role in the advertising ecosystem.")).describe("Agents authorized to act on behalf of this brand. Consumers resolving an agent by URL use the matching brand-level entry; do not infer a type-wide override of unrelated house-level entries when multiple same-type entries exist.").optional(), "brand_agent": z.object({ "url": z.string().url().regex(new RegExp("^https://")).describe("Brand agent MCP endpoint URL. Callers comparing this URL against another value (e.g., resolving 'is this the brand's declared agent?' against a discovery cache) MUST canonicalize both sides per the AdCP URL canonicalization rules, not byte-equality. See docs/reference/url-canonicalization."), "id": z.string().regex(new RegExp("^[a-z0-9_]+$")).describe("Agent identifier (useful for logging, multi-tenant DAMs)") }).catchall(z.any()).describe("Deprecated: use agents array with type 'brand' instead. Brand agent that provides dynamic brand data via MCP.").optional(), "rights_agent": z.object({ "url": z.string().url().regex(new RegExp("^https://")).describe("Rights agent MCP endpoint URL. Callers comparing this URL against another value (e.g., matching against a brand's declared rights endpoint) MUST canonicalize both sides per the AdCP URL canonicalization rules, not byte-equality. See docs/reference/url-canonicalization."), "id": z.string().regex(new RegExp("^[a-z0-9_]+$")).describe("Agent identifier"), "available_uses": z.array(z.enum(["likeness","voice","name","endorsement","motion_capture","signature","catchphrase","sync","background_music","editorial","commercial","ai_generated_image"]).describe("Types of rights usage that can be licensed through the brand protocol. Aligned with DDEX UseType direction for interoperability with music and media rights systems.")).describe("Rights uses available for licensing through this agent"), "right_types": z.array(z.enum(["talent","character","brand_ip","music","stock_media"]).describe("Categories of intellectual property rights that can be licensed through the brand protocol.")).describe("Types of rights available").optional(), "countries": z.array(z.string().regex(new RegExp("^[A-Z]{2}$"))).describe("Countries where rights are available (ISO 3166-1 alpha-2)").optional() }).catchall(z.any()).describe("Deprecated: use agents array with type 'rights' instead. Rights licensing agent for this brand.").optional(), "contact": z.object({ "email": z.string().email().describe("Contact email").optional(), "phone": z.string().describe("Contact phone number").optional() }).describe("Brand-level contact information").optional(), "collections": z.array(z.object({ "collection_id": z.string().describe("Collection identifier as used in the seller's get_products responses").optional(), "name": z.string().describe("Human-readable collection name"), "role": z.enum(["host","guest","creator","cast","narrator","producer","correspondent","commentator","analyst"]).describe("This person's role on the collection").optional(), "seller_agent_url": z.string().url().describe("URL of the sales agent that sells inventory for this collection. Buyer agents can query this agent for collection products.").optional() }).catchall(z.any())).describe("Collections this person or brand is associated with. Enables bidirectional linking: a collection's talent references brand.json via brand_url, and brand.json links back to collections.").optional() }).catchall(z.any()).describe("A brand within a house portfolio. Combines identity (who) with creative assets (how to represent). Referenced as domain + brand_id."))).describe("Self-published brand document where the brand owns its own identity attributes. Optionally declares its house via house_domain; for trust, the named house's brand_refs[] must reciprocate (mutual assertion). Standalone brands (no parent house) omit house_domain. Hosted at the brand's own /.well-known/brand.json (or via authoritative_location indirection). See docs/brand-protocol/brand-json.mdx")]).describe("Brand identity and discovery file. Hosted at /.well-known/brand.json on house domains. Contains the full brand portfolio with identity, creative assets, and digital properties. Brands are identified by house + brand_id (like properties are identified by publisher + property_id). Supports variants: house portfolio (full brand data), brand agent (agent provides brand info via MCP), house redirect (pointer to house domain), or authoritative location redirect.") +export const BrandJsonSchema = z.union([z.object({ "$schema": z.string().optional(), "authoritative_location": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL of the authoritative brand.json file"), "redirect_reason": z.enum(["acquisition","divestiture","rebrand","regional","legacy","consolidation","other"]).describe("Optional structured signal indicating why this redirect was put in place. Consumers SHOULD use this to inform cache TTL decisions: 'acquisition' / 'divestiture' / 'rebrand' / 'consolidation' suggest the resolved target is in transition and consumers SHOULD shorten cache TTL until stable. 'regional' / 'legacy' suggest a stable redirect with no special cache handling needed. Free-text rationale belongs in 'note'.").optional(), "redirect_effective_at": z.string().datetime().describe("Optional timestamp when this redirect became effective. Caches MUST treat any entry cached before this timestamp as stale and re-fetch through the redirect.").optional(), "note": z.string().describe("Optional human-readable rationale for the redirect.").optional(), "last_updated": z.string().datetime().optional() }).strict().describe("Redirects to a hosted brand.json file at another URL"), z.object({ "$schema": z.string().optional(), "house": z.string().regex(new RegExp("^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$")).describe("House domain to fetch brand portfolio from"), "region": z.string().regex(new RegExp("^[A-Z]{2}$")).describe("ISO 3166-1 alpha-2 country code if this is a regional domain").optional(), "redirect_reason": z.enum(["acquisition","divestiture","rebrand","regional","legacy","consolidation","other"]).describe("Optional structured signal indicating why this redirect was put in place. Consumers SHOULD use this to inform cache TTL decisions: 'acquisition' / 'divestiture' / 'rebrand' / 'consolidation' suggest the resolved target is in transition and consumers SHOULD shorten cache TTL until stable. 'regional' / 'legacy' suggest a stable redirect with no special cache handling needed. Free-text rationale belongs in 'note'.").optional(), "redirect_effective_at": z.string().datetime().describe("Optional timestamp when this redirect became effective. Caches MUST treat any entry cached before this timestamp as stale and re-fetch through the redirect.").optional(), "note": z.string().describe("Optional human-readable rationale for the redirect.").optional(), "last_updated": z.string().datetime().optional() }).strict().describe("Redirects to the house domain that contains the full brand portfolio"), z.object({ "$schema": z.string().optional(), "version": z.string().optional(), "agents": z.array(z.object({ "type": z.enum(["brand","rights","measurement","governance","creative","sales","buying","signals"]).describe("Functional role of this agent"), "url": z.string().url().regex(new RegExp("^https://")).describe("Agent endpoint URL (MCP or A2A). Callers comparing a brand's declared agent URL against another value (e.g., resolving 'is this the agent that signed this artifact?' or matching against a discovery cache) MUST canonicalize both sides per the AdCP URL canonicalization rules, not byte-equality. See docs/reference/url-canonicalization."), "id": z.string().regex(new RegExp("^[a-z0-9_]+$")).max(100).describe("Agent identifier (useful for logging, multi-tenant platforms)"), "description": z.string().max(500).describe("Human-readable description of this agent's capabilities or scope").optional(), "jwks_uri": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL of the agent's JWKS (RFC 7517) containing public keys used to verify artifacts this agent signs or requests it sends. Verified artifacts include signed governance_context tokens (for governance agents) and RFC 9421 HTTP Signatures on outgoing requests (for any agent). When absent, verifiers MUST default to /.well-known/jwks.json on the origin of `url`. Keys are identified by `kid` in the JWS header or RFC 9421 `keyid` parameter; JWKS MAY contain multiple keys to support rotation and per-purpose separation via `key_ops` and `use`.").optional(), "available_uses": z.array(z.enum(["likeness","voice","name","endorsement","motion_capture","signature","catchphrase","sync","background_music","editorial","commercial","ai_generated_image"]).describe("Types of rights usage that can be licensed through the brand protocol. Aligned with DDEX UseType direction for interoperability with music and media rights systems.")).describe("For rights agents: rights uses available for licensing").optional(), "right_types": z.array(z.enum(["talent","character","brand_ip","music","stock_media"]).describe("Categories of intellectual property rights that can be licensed through the brand protocol.")).describe("For rights agents: types of rights available").optional(), "countries": z.array(z.string().regex(new RegExp("^[A-Z]{2}$"))).describe("ISO 3166-1 alpha-2 country codes where this agent operates. Omit for global scope.").optional() }).catchall(z.any()).describe("An agent declared by a brand or house. Each entry identifies one agent endpoint and its functional role in the advertising ecosystem.")).describe("Agents declared by this brand or house. Multiple entries with the same type are permitted when they have distinct url values, such as one endpoint URL per tenant or property scope. Agent url values MUST be unique within this array; duplicate urls are invalid because signature verifiers resolve a signing key by matching one agent url to one agents[] entry and reject ambiguous matches.").optional(), "brand_agent": z.object({ "url": z.string().url().regex(new RegExp("^https://")).describe("Brand agent MCP endpoint URL. Callers comparing this URL against another value (e.g., resolving 'is this the brand's declared agent?' against a discovery cache) MUST canonicalize both sides per the AdCP URL canonicalization rules, not byte-equality. See docs/reference/url-canonicalization."), "id": z.string().regex(new RegExp("^[a-z0-9_]+$")).describe("Agent identifier (useful for logging, multi-tenant DAMs)") }).catchall(z.any()).describe("Reference to a brand agent that provides brand data via MCP").optional(), "contact": z.object({ "name": z.string().min(1).max(255), "email": z.string().email().max(255).optional(), "domain": z.string().regex(new RegExp("^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$")).describe("A valid domain name").optional() }).catchall(z.any()).describe("Contact information").optional(), "data_subject_contestation": z.object({ "url": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL to a human-accessible contestation form or information page.").optional(), "email": z.string().email().describe("Email address for contestation requests. Deployer MUST monitor and respond within the timelines set by applicable law (e.g., 1 month under GDPR Art. 12(3)).").optional(), "languages": z.array(z.string()).describe("BCP 47 language tags the contestation channel supports (e.g., ['en', 'de', 'fr']). At minimum SHOULD include a language spoken in every jurisdiction where the brand runs regulated-vertical campaigns.").optional() }).strict().and(z.union([z.any(), z.any()])).describe("Contact point where a data subject can request human intervention, express their view, or contest an automated decision — satisfying GDPR Article 22(3) and EU AI Act Article 26(11) transparency obligations. This is a contact reference (URL, email, or both), not a machine-callable API. AdCP surfaces the pointer; the deployer runs the contestation workflow.").optional(), "last_updated": z.string().datetime().optional() }).strict().and(z.union([z.any(), z.any()])).describe("Brand represented by agents that provide brand info via MCP"), z.object({ "$schema": z.string().optional(), "version": z.string().optional(), "default_language": z.string().regex(new RegExp("^(?:[a-z]{2,8}(?:-[A-Z][a-z]{3})?(?:-(?:[A-Z]{2}|[0-9]{3}))?(?:-(?:[a-z0-9]{5,8}|[0-9][a-z0-9]{3}))*(?:-[0-9a-wy-z](?:-[a-z0-9]{2,8})+)*(?:-x(?:-[a-z0-9]{1,8})+)?|x(?:-[a-z0-9]{1,8})+)$")).min(2).max(63).describe("A well-formed BCP 47 language tag used by AdCP only as language identity. Script and region may refine that identity; other valid BCP 47 subtags remain part of tag matching but do not make this a general locale-settings object. It does not determine currency, time zone, number/date formatting, market, or legal jurisdiction. The AdCP canonical wire profile requires lower-case language and variants, title-case script, and upper-case region (for example `en-US`, `zh-Hant-TW`, or `x-private`). RFC 5646 comparisons are case-insensitive and its case regularization is optional; AdCP intentionally requires this stricter single wire spelling and receivers MUST reject differently cased tags rather than silently normalizing them. The schema pattern enforces the AdCP casing profile and extension structure for commonly used tags; conforming receivers additionally validate the complete RFC 5646 grammar and registry rules. Every new AdCP field carrying BCP 47 language identity or a concrete language range MUST reference this schema instead of declaring independent string constraints.").describe("BCP 47 language tag for legacy plain-string and plain-array content in this document. Readers assume en when omitted.").default("en"), "house": z.object({ "domain": z.string().regex(new RegExp("^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$")).describe("The house's domain where brand.json is hosted"), "name": z.string().min(1).describe("Primary display name of the house"), "names": z.array(z.record(z.string(), z.string().min(1)).describe("A localized name using the legacy underscore-separated language/region key convention (for example 'en_US', 'fr_CA', or 'zh_CN') and a name value. Bare language codes such as 'en' are also accepted for backwards compatibility. These keys are not AdCP locale-tag values: a bridge to /schemas/core/locale-tag.json replaces '_' with '-', applies the AdCP canonical wire profile, and validates the result rather than passing the key through unchanged.")).describe("Localized house names including legal name, stock symbol, etc.").optional(), "architecture": z.enum(["branded_house","house_of_brands","hybrid"]).describe("Brand architecture model: branded_house (Google), house_of_brands (P&G), hybrid (Nike)").optional(), "agents": z.array(z.object({ "type": z.enum(["brand","rights","measurement","governance","creative","sales","buying","signals"]).describe("Functional role of this agent"), "url": z.string().url().regex(new RegExp("^https://")).describe("Agent endpoint URL (MCP or A2A). Callers comparing a brand's declared agent URL against another value (e.g., resolving 'is this the agent that signed this artifact?' or matching against a discovery cache) MUST canonicalize both sides per the AdCP URL canonicalization rules, not byte-equality. See docs/reference/url-canonicalization."), "id": z.string().regex(new RegExp("^[a-z0-9_]+$")).max(100).describe("Agent identifier (useful for logging, multi-tenant platforms)"), "description": z.string().max(500).describe("Human-readable description of this agent's capabilities or scope").optional(), "jwks_uri": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL of the agent's JWKS (RFC 7517) containing public keys used to verify artifacts this agent signs or requests it sends. Verified artifacts include signed governance_context tokens (for governance agents) and RFC 9421 HTTP Signatures on outgoing requests (for any agent). When absent, verifiers MUST default to /.well-known/jwks.json on the origin of `url`. Keys are identified by `kid` in the JWS header or RFC 9421 `keyid` parameter; JWKS MAY contain multiple keys to support rotation and per-purpose separation via `key_ops` and `use`.").optional(), "available_uses": z.array(z.enum(["likeness","voice","name","endorsement","motion_capture","signature","catchphrase","sync","background_music","editorial","commercial","ai_generated_image"]).describe("Types of rights usage that can be licensed through the brand protocol. Aligned with DDEX UseType direction for interoperability with music and media rights systems.")).describe("For rights agents: rights uses available for licensing").optional(), "right_types": z.array(z.enum(["talent","character","brand_ip","music","stock_media"]).describe("Categories of intellectual property rights that can be licensed through the brand protocol.")).describe("For rights agents: types of rights available").optional(), "countries": z.array(z.string().regex(new RegExp("^[A-Z]{2}$"))).describe("ISO 3166-1 alpha-2 country codes where this agent operates. Omit for global scope.").optional() }).catchall(z.any()).describe("An agent declared by a brand or house. Each entry identifies one agent endpoint and its functional role in the advertising ecosystem.")).describe("House-level agents that apply to all brands unless overridden at the brand level").optional(), "data_subject_contestation": z.object({ "url": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL to a human-accessible contestation form or information page.").optional(), "email": z.string().email().describe("Email address for contestation requests. Deployer MUST monitor and respond within the timelines set by applicable law (e.g., 1 month under GDPR Art. 12(3)).").optional(), "languages": z.array(z.string()).describe("BCP 47 language tags the contestation channel supports (e.g., ['en', 'de', 'fr']). At minimum SHOULD include a language spoken in every jurisdiction where the brand runs regulated-vertical campaigns.").optional() }).strict().and(z.union([z.any(), z.any()])).describe("House-level fallback contestation contact. Governance agents resolve in order: brand.data_subject_contestation → house.data_subject_contestation → missing (critical finding when human review required).").optional(), "identity_relying_parties": z.array(z.object({ "issuer": z.object({ "domain": z.string().regex(new RegExp("^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$")).describe("Domain where /.well-known/brand.json is hosted, or the brand's operating domain"), "brand_id": z.string().regex(new RegExp("^[a-z0-9_]+$")).describe("Brand identifier within the house portfolio. Optional for single-brand domains.").optional(), "countries": z.array(z.string().regex(new RegExp("^[A-Z]{2}$"))).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Canonical set of ISO 3166-1 alpha-2 countries for this advertiser identity. Omit for a global/default identity. Array order is not meaningful; producers MUST sort codes lexicographically before computing keys or signatures. This qualifies account identity and is not delivery targeting.").optional(), "industries": z.array(z.string()).describe("Inline override for the brand's industries. Useful when the caller cannot modify the brand's canonical brand.json but needs to declare industries for governance (e.g., Annex III vertical detection). brand.json remains the canonical source; when omitted here, governance agents SHOULD resolve from brand.json.").optional(), "data_subject_contestation": z.object({ "url": z.string().url().regex(new RegExp("^https://")).optional(), "email": z.string().email().optional(), "languages": z.array(z.string()).optional() }).strict().and(z.union([z.any(), z.any()])).describe("Inline override for the brand's contestation contact point. Useful when the operator does not control brand.json but needs to discharge Art 22(3) for this plan. brand.json is canonical; when omitted, governance agents resolve brand → house → missing.").optional(), "brand_kit_override": z.object({ "logo": z.object({ "asset_type": z.literal("image").describe("Discriminator identifying this as an image asset. See /schemas/creative/asset-types for the registry."), "url": z.string().url().describe("URL to the image asset"), "width": z.number().int().gte(1).describe("Width in pixels"), "height": z.number().int().gte(1).describe("Height in pixels"), "file_size_bytes": z.number().int().gte(1).describe("Image file size in bytes. Required by agents that advertise a max_file_size_kb constraint.").optional(), "pixel_ratio": z.number().gt(0).describe("Intrinsic pixels per logical render pixel (for example `2` for a 600×500 image intended to render at 300×250). Optional because a validator can infer the ratio when the target format declares logical dimensions. When supplied, it MUST agree with both `width / logical_width` and `height / logical_height`; it is never a substitute for the intrinsic `width` and `height` fields.").optional(), "state_id": z.string().describe("Binding used only when this image populates a `seller_rendered_stateful_display` `state_canvases` slot. It MUST match one declared `states[].state_id` (semantic validators resolve it). Omit for ordinary image slots.").optional(), "breakpoint_id": z.string().describe("Binding used only when this image populates a `seller_rendered_stateful_display` `state_canvases` slot. It MUST match one breakpoint declared on the selected state (semantic validators resolve it). Omit for ordinary image slots.").optional(), "focal_point": z.array(z.number().gte(0).lte(1)).max(2).describe("Normalized `[x, y]` coordinates (0–1 from top-left) of the image's visual anchor. Seller-side renderers crop toward the focal point when deriving renditions across breakpoints and aspect ratios; absent, cropping falls back to center-weighted defaults.").optional(), "format": z.string().describe("Image file format (jpg, png, gif, webp, etc.)").optional(), "alt_text": z.string().describe("Alternative text for accessibility").optional(), "provenance": z.object({ "digital_source_type": z.enum(["digital_capture","digital_creation","trained_algorithmic_media","composite_with_trained_algorithmic_media","algorithmic_media","composite_capture","composite_synthetic","human_edits","data_driven_media"]).describe("IPTC-aligned classification of AI involvement in producing this content").optional(), "synthetic_depiction": z.boolean().describe("Assessed declaration of whether the content synthetically depicts a real or fictional person performing or appearing in a way that was generated or materially manipulated rather than captured as depicted. `true` covers both a fully synthetic performer and material manipulation of a real performer; `false` is an assessed declaration that the content does not contain such a depiction. Absence means the content has not been assessed for synthetic depiction. This field does not claim consent, legality, or independent verification, and receivers MUST NOT derive it solely from `digital_source_type`.").optional(), "ai_tool": z.object({ "name": z.string().describe("Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')"), "version": z.string().describe("Version identifier for the AI tool or model (e.g., '25.1', '0125', '2.1'). For generative models, use the model version rather than the API version.").optional(), "provider": z.string().describe("Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')").optional() }).describe("AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.").optional(), "human_oversight": z.enum(["none","prompt_only","selected","edited","directed"]).describe("Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` — the protocol does not derive disclosure obligations from oversight level. Some regulations include carve-outs for human-edited or human-directed AI output, but those carve-outs have factual prerequisites the schema cannot evaluate. Asserting `edited` or `directed` does not by itself justify `disclosure.required: false`.").optional(), "declared_by": z.object({ "agent_url": z.string().url().describe("URL of the agent or service that declared this provenance").optional(), "role": z.enum(["creator","advertiser","agency","platform","tool"]).describe("Role of the declaring party in the supply chain") }).describe("Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.").optional(), "declared_at": z.string().datetime().describe("When this provenance claim was made (ISO 8601). Distinct from created_time, which records when the content itself was produced. A provenance claim may be attached well after content creation, for example when retroactively declaring AI involvement for regulatory compliance.").optional(), "created_time": z.string().datetime().describe("When this content was created or generated (ISO 8601)").optional(), "c2pa": z.object({ "manifest_url": z.string().url().describe("URL to the C2PA manifest store for this content") }).describe("C2PA sidecar manifest reference. Links to a detached cryptographic provenance manifest for this content. Note: file-level C2PA bindings break when ad servers transcode, resize, or re-encode assets. For pipelines with intermediaries, consider embedded_provenance as the primary provenance mechanism.").optional(), "embedded_provenance": z.array(z.object({ "method": z.enum(["manifest_wrapper","provenance_markers"]).describe("How provenance data is carried within the content"), "standard": z.string().describe("Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)").optional(), "provider": z.string().describe("Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context — not a wire identifier."), "verify_agent": z.object({ "agent_url": z.string().url().regex(new RegExp("^https://")).describe("URL of the governance agent the buyer represents was used to embed/verify this layer. MUST use the `https://` scheme and MUST appear in the seller's `creative_policy.accepted_verifiers[].agent_url` list (canonicalized per /docs/reference/url-canonicalization: lowercase scheme and host, strip default port, normalize path dot-segments). Sellers MUST NOT call this URL until the canonicalized match is confirmed."), "feature_id": z.string().describe("Optional `feature_id` the buyer represents the seller should request via `get_creative_features` (e.g., `encypher.markers_present_v2`). SHOULD match the `feature_id` declared on the matching `accepted_verifiers[]` entry, or be omitted to defer the selector to the seller. When the seller's entry pins a `feature_id`, that value wins; when neither side pins, the seller selects from the agent's `governance.creative_features` catalog.").optional() }).strict().describe("Buyer's representation that this embedding can be verified by a governance agent on the seller's `creative_policy.accepted_verifiers` list. The `agent_url` MUST match (canonicalized) one of the seller's published `accepted_verifiers[].agent_url` entries; sellers reject `sync_creatives` submissions whose `verify_agent.agent_url` is off-list with `PROVENANCE_VERIFIER_NOT_ACCEPTED`. This is buyer-supplied evidence, not buyer-driven routing — the seller is the verifier-of-record and the seller controls which agent it actually calls (the seller MAY use a different on-list agent if it determines this is more appropriate; the seller does not call buyer-asserted endpoints outside its allowlist). MAY be omitted for self-verifiable embeddings (e.g., a C2PA text manifest with a public key the seller already trusts).").optional(), "embedded_at": z.string().datetime().describe("When the provenance data was embedded (ISO 8601)").optional() })).describe("Provenance metadata embedded within the content stream. Each entry declares one embedding layer: structured provenance data carried inside the content itself, as distinct from sidecar references (c2pa.manifest_url). Embedded provenance survives operations that break sidecar and file-level bindings: ad-server transcoding, CMS ingestion, copy-paste, reformatting, and CDN re-encoding. For ad-tech pipelines where content passes through multiple intermediaries, embedded provenance is the reliable path for provenance that persists from declaration through delivery. This is a declaration by the embedding party. The receiving party (the seller) is the verifier-of-record: it confirms the claim by calling a governance agent it trusts (typically one published in `creative_policy.accepted_verifiers`).").optional(), "watermarks": z.array(z.object({ "media_type": z.enum(["audio","image","video","text"]).describe("Media category of the watermarked content"), "provider": z.string().describe("Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context — not a wire identifier."), "verify_agent": z.object({ "agent_url": z.string().url().regex(new RegExp("^https://")).describe("URL of the governance agent the buyer represents was used to apply/detect this watermark. MUST use the `https://` scheme and MUST appear in the seller's `creative_policy.accepted_verifiers[].agent_url` list (canonicalized per /docs/reference/url-canonicalization: lowercase scheme and host, strip default port, normalize path dot-segments). Sellers MUST NOT call this URL until the canonicalized match is confirmed."), "feature_id": z.string().describe("Optional `feature_id` the buyer represents the seller should request via `get_creative_features` (e.g., `imatag.watermark_detected`). SHOULD match the `feature_id` declared on the matching `accepted_verifiers[]` entry, or be omitted to defer the selector to the seller. When the seller's entry pins a `feature_id`, that value wins; when neither side pins, the seller selects from the agent's `governance.creative_features` catalog.").optional() }).strict().describe("Buyer's representation that this watermark can be detected by a governance agent on the seller's `creative_policy.accepted_verifiers` list. The `agent_url` MUST match (canonicalized) one of the seller's published `accepted_verifiers[].agent_url` entries; sellers reject `sync_creatives` submissions whose `verify_agent.agent_url` is off-list with `PROVENANCE_VERIFIER_NOT_ACCEPTED`. This is buyer-supplied evidence, not buyer-driven routing — the seller is the verifier-of-record and the seller controls which agent it actually calls (the seller MAY use a different on-list agent if it determines this is more appropriate; the seller does not call buyer-asserted endpoints outside its allowlist).").optional(), "c2pa_action": z.enum(["c2pa.watermarked.bound","c2pa.watermarked.unbound"]).describe("C2PA action classification for this watermark").optional(), "embedded_at": z.string().datetime().describe("When the watermark was applied (ISO 8601)").optional() })).describe("Content watermarks applied to this asset. Each entry declares one watermarking layer: a content modification that encodes an identifier or fingerprint within the asset. Watermarks differ from embedded provenance: a watermark encodes an identifier (who generated it, who owns it), while embedded provenance carries or references a structured provenance record (the full chain of custody). A single asset may carry both. Aligns with C2PA action taxonomy: c2pa.watermarked.bound (watermark linked to a C2PA manifest) and c2pa.watermarked.unbound (watermark independent of any manifest). This is a declaration by the watermarking party. The receiving party (the seller) is the verifier-of-record: it confirms the claim by calling a governance agent it trusts (typically one published in `creative_policy.accepted_verifiers`).").optional(), "disclosure": z.object({ "required": z.boolean().describe("The declaring party's claim that AI disclosure is required for this content under applicable regulations. This is a declared signal carried through the supply chain — useful as a routing and audit input — not a regulatory determination made by the protocol. Receiving parties remain responsible for their own jurisdictional analysis and should not treat `required: false` as compliance cover."), "jurisdictions": z.array(z.object({ "country": z.string().describe("ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')"), "region": z.string().describe("Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)").optional(), "regulation": z.string().describe("Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')"), "label_text": z.string().describe("Required disclosure label text for this jurisdiction, in the local language").optional(), "render_guidance": z.object({ "persistence": z.enum(["continuous","initial","flexible"]).describe("How long the disclosure must persist during content playback or display").optional(), "min_duration_ms": z.number().int().gte(1).describe("Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial — without it, the duration is at the publisher's discretion. At serve time the publisher reads this from provenance since the brief is not available.").optional(), "positions": z.array(z.enum(["prominent","footer","audio","subtitle","overlay","end_card","pre_roll","companion"]).describe("Where a required disclosure should appear within a creative. Used by creative briefs to specify disclosure placement and by formats to declare which positions they can render.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Preferred disclosure positions in priority order. The first position a format supports should be used.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension object for platform-specific, vendor-namespaced parameters. Extensions are always optional and must be namespaced under a vendor/platform key (e.g., ext.gam, ext.roku). Used for custom capabilities, partner-specific configuration, and features being proposed for standardization.").optional() }).describe("How the disclosure should be rendered for this jurisdiction. Expresses the declaring party's intent for persistence and position based on regulatory requirements. Publishers control actual rendering but governance agents can audit whether guidance was followed.").optional() })).describe("Jurisdictions where disclosure obligations apply").optional() }).describe("Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.").optional(), "verification": z.array(z.object({ "verified_by": z.string().describe("Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')"), "verified_time": z.string().datetime().describe("When the verification was performed (ISO 8601)").optional(), "result": z.enum(["authentic","ai_generated","ai_modified","inconclusive"]).describe("Verification outcome"), "confidence": z.number().gte(0).lte(1).describe("Confidence score of the verification result (0.0 to 1.0)").optional(), "details_url": z.string().url().describe("URL to the full verification report").optional() })).describe("Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim — verification results attached by the declaring party are supplementary. The enforcing party (e.g., seller/publisher) should run its own verification via get_creative_features or calibrate_content.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension object for platform-specific, vendor-namespaced parameters. Extensions are always optional and must be namespaced under a vendor/platform key (e.g., ext.gam, ext.roku). Used for custom capabilities, partner-specific configuration, and features being proposed for standardization.").optional() }).describe("Provenance metadata for this asset, overrides manifest-level provenance").optional() }).catchall(z.any()).describe("Override logo asset.").optional(), "colors": z.object({ "primary": z.string().regex(new RegExp("^#[0-9a-fA-F]{6}$")).optional(), "secondary": z.string().regex(new RegExp("^#[0-9a-fA-F]{6}$")).optional(), "accent": z.string().regex(new RegExp("^#[0-9a-fA-F]{6}$")).optional() }).catchall(z.any()).describe("Override brand colors (hex strings).").optional(), "voice": z.string().describe("Override brand-voice description for surface-composed text/audio output.").optional(), "tagline": z.string().describe("Override tagline.").optional() }).catchall(z.any()).describe("Inline override for brand-kit fields normally resolved from `/.well-known/brand.json` on `domain` (logo, colors, voice, tagline). Use when brand.json is missing, stale, or inappropriate for this specific call — e.g., a campaign-scoped tagline, a co-branded creative, a freshly-rebranded color palette the brand.json hasn't shipped yet. Same inline-override pattern as `industries` and `data_subject_contestation` above: brand.json is canonical, the override is per-call. Adopters needing to override fields outside this subset (`voice_attributes`, `prohibited_terms`, etc.) MUST publish a different brand.json and reference it via a different `domain` — the inline override is intentionally narrow to a small high-traffic subset.\n\n**Merge semantics (normative).** The merge is **field-level**, not whole-object replacement. Each field within `brand_kit_override` (`logo`, `colors`, `voice`, `tagline`) is evaluated independently — when a field is present on the override the override value applies; when a field is absent the brand.json value applies (or is absent if brand.json doesn't carry one either). For composite fields (`colors.primary`, `colors.secondary`, `colors.accent`), the merge is one level deeper: each color slot is evaluated independently — a producer can override `colors.primary` while still inheriting `colors.secondary` from brand.json. SDKs MUST NOT treat a present `brand_kit_override.colors` as wiping the brand.json `colors` block entirely; only the per-slot fields present in the override take precedence. Without this rule, a partial-override semantics would diverge across SDKs and produce inconsistent rendering for the same payload.").optional() }).strict().describe("Identity issuer / attestation authority, referenced as a vendor BrandRef (e.g. {\"domain\": \"world.org\"}) — the same vendor-reference shape AdCP uses for measurement and signals vendors. The issuer's canonical domain is the anchor; it need not host a brand.json, but if it does, that is where its verifier metadata (scheme versions, verify endpoint, JWKs) lives. Issuer-agnostic: World ID, ISO 18013-5 mDL, and W3C-VC issuers all reference by domain. The relying party is namespaced by the issuer — identity is the tuple (issuer.domain, issuer.brand_id, relying_party_id), mirroring (vendor.domain, vendor.brand_id, metric_id)."), "scheme": z.string().describe("Proof scheme and version, e.g. \"world_id_v4\".").optional(), "relying_party_id": z.string().describe("The relying-party id registered with the issuer (and, for on-chain issuers like World ID, with the issuer's registry). One entity may operate many relying parties (scope=entity vs scope=property); this is the attestation's audience / linkability boundary, not an entity identifier."), "scope": z.enum(["entity","property"]).describe("Whether this relying_party_id is shared across the entity's properties (entity → a within-entity unique-human graph) or scoped to a single property (property → per-property pseudonyms, unlinkable across the entity).").optional() }).strict().describe("A verified-identity relying party an entity or brand operates. Used for attestation provenance in TMP Identity Match (the buyer checks a forwarded attestation's relying_party_id against the owner's published list). Issuer-agnostic; World ID is the first issuer.")).describe("Verified-identity relying parties this house/entity operates, so a buyer can verify that a forwarded TMP identity attestation's relying_party_id genuinely belongs to this entity (provenance) rather than being replayed under another owner. For network-as-RP, the network publishes its own relying_party_id here. See specs/tmp-verified-identity-attestation.md.").optional() }).catchall(z.any()).describe("Corporate or organizational entity that owns brands"), "brands": z.array(z.object({ "id": z.string().regex(new RegExp("^[a-z0-9_]+$")).describe("Brand identifier within the house. House chooses this ID."), "url": z.string().url().describe("Primary brand URL for context and asset discovery").optional(), "identity_relying_parties": z.array(z.object({ "issuer": z.object({ "domain": z.string().regex(new RegExp("^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$")).describe("Domain where /.well-known/brand.json is hosted, or the brand's operating domain"), "brand_id": z.string().regex(new RegExp("^[a-z0-9_]+$")).describe("Brand identifier within the house portfolio. Optional for single-brand domains.").optional(), "countries": z.array(z.string().regex(new RegExp("^[A-Z]{2}$"))).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Canonical set of ISO 3166-1 alpha-2 countries for this advertiser identity. Omit for a global/default identity. Array order is not meaningful; producers MUST sort codes lexicographically before computing keys or signatures. This qualifies account identity and is not delivery targeting.").optional(), "industries": z.array(z.string()).describe("Inline override for the brand's industries. Useful when the caller cannot modify the brand's canonical brand.json but needs to declare industries for governance (e.g., Annex III vertical detection). brand.json remains the canonical source; when omitted here, governance agents SHOULD resolve from brand.json.").optional(), "data_subject_contestation": z.object({ "url": z.string().url().regex(new RegExp("^https://")).optional(), "email": z.string().email().optional(), "languages": z.array(z.string()).optional() }).strict().and(z.union([z.any(), z.any()])).describe("Inline override for the brand's contestation contact point. Useful when the operator does not control brand.json but needs to discharge Art 22(3) for this plan. brand.json is canonical; when omitted, governance agents resolve brand → house → missing.").optional(), "brand_kit_override": z.object({ "logo": z.object({ "asset_type": z.literal("image").describe("Discriminator identifying this as an image asset. See /schemas/creative/asset-types for the registry."), "url": z.string().url().describe("URL to the image asset"), "width": z.number().int().gte(1).describe("Width in pixels"), "height": z.number().int().gte(1).describe("Height in pixels"), "file_size_bytes": z.number().int().gte(1).describe("Image file size in bytes. Required by agents that advertise a max_file_size_kb constraint.").optional(), "pixel_ratio": z.number().gt(0).describe("Intrinsic pixels per logical render pixel (for example `2` for a 600×500 image intended to render at 300×250). Optional because a validator can infer the ratio when the target format declares logical dimensions. When supplied, it MUST agree with both `width / logical_width` and `height / logical_height`; it is never a substitute for the intrinsic `width` and `height` fields.").optional(), "state_id": z.string().describe("Binding used only when this image populates a `seller_rendered_stateful_display` `state_canvases` slot. It MUST match one declared `states[].state_id` (semantic validators resolve it). Omit for ordinary image slots.").optional(), "breakpoint_id": z.string().describe("Binding used only when this image populates a `seller_rendered_stateful_display` `state_canvases` slot. It MUST match one breakpoint declared on the selected state (semantic validators resolve it). Omit for ordinary image slots.").optional(), "focal_point": z.array(z.number().gte(0).lte(1)).max(2).describe("Normalized `[x, y]` coordinates (0–1 from top-left) of the image's visual anchor. Seller-side renderers crop toward the focal point when deriving renditions across breakpoints and aspect ratios; absent, cropping falls back to center-weighted defaults.").optional(), "format": z.string().describe("Image file format (jpg, png, gif, webp, etc.)").optional(), "alt_text": z.string().describe("Alternative text for accessibility").optional(), "provenance": z.object({ "digital_source_type": z.enum(["digital_capture","digital_creation","trained_algorithmic_media","composite_with_trained_algorithmic_media","algorithmic_media","composite_capture","composite_synthetic","human_edits","data_driven_media"]).describe("IPTC-aligned classification of AI involvement in producing this content").optional(), "synthetic_depiction": z.boolean().describe("Assessed declaration of whether the content synthetically depicts a real or fictional person performing or appearing in a way that was generated or materially manipulated rather than captured as depicted. `true` covers both a fully synthetic performer and material manipulation of a real performer; `false` is an assessed declaration that the content does not contain such a depiction. Absence means the content has not been assessed for synthetic depiction. This field does not claim consent, legality, or independent verification, and receivers MUST NOT derive it solely from `digital_source_type`.").optional(), "ai_tool": z.object({ "name": z.string().describe("Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')"), "version": z.string().describe("Version identifier for the AI tool or model (e.g., '25.1', '0125', '2.1'). For generative models, use the model version rather than the API version.").optional(), "provider": z.string().describe("Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')").optional() }).describe("AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.").optional(), "human_oversight": z.enum(["none","prompt_only","selected","edited","directed"]).describe("Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` — the protocol does not derive disclosure obligations from oversight level. Some regulations include carve-outs for human-edited or human-directed AI output, but those carve-outs have factual prerequisites the schema cannot evaluate. Asserting `edited` or `directed` does not by itself justify `disclosure.required: false`.").optional(), "declared_by": z.object({ "agent_url": z.string().url().describe("URL of the agent or service that declared this provenance").optional(), "role": z.enum(["creator","advertiser","agency","platform","tool"]).describe("Role of the declaring party in the supply chain") }).describe("Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.").optional(), "declared_at": z.string().datetime().describe("When this provenance claim was made (ISO 8601). Distinct from created_time, which records when the content itself was produced. A provenance claim may be attached well after content creation, for example when retroactively declaring AI involvement for regulatory compliance.").optional(), "created_time": z.string().datetime().describe("When this content was created or generated (ISO 8601)").optional(), "c2pa": z.object({ "manifest_url": z.string().url().describe("URL to the C2PA manifest store for this content") }).describe("C2PA sidecar manifest reference. Links to a detached cryptographic provenance manifest for this content. Note: file-level C2PA bindings break when ad servers transcode, resize, or re-encode assets. For pipelines with intermediaries, consider embedded_provenance as the primary provenance mechanism.").optional(), "embedded_provenance": z.array(z.object({ "method": z.enum(["manifest_wrapper","provenance_markers"]).describe("How provenance data is carried within the content"), "standard": z.string().describe("Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)").optional(), "provider": z.string().describe("Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context — not a wire identifier."), "verify_agent": z.object({ "agent_url": z.string().url().regex(new RegExp("^https://")).describe("URL of the governance agent the buyer represents was used to embed/verify this layer. MUST use the `https://` scheme and MUST appear in the seller's `creative_policy.accepted_verifiers[].agent_url` list (canonicalized per /docs/reference/url-canonicalization: lowercase scheme and host, strip default port, normalize path dot-segments). Sellers MUST NOT call this URL until the canonicalized match is confirmed."), "feature_id": z.string().describe("Optional `feature_id` the buyer represents the seller should request via `get_creative_features` (e.g., `encypher.markers_present_v2`). SHOULD match the `feature_id` declared on the matching `accepted_verifiers[]` entry, or be omitted to defer the selector to the seller. When the seller's entry pins a `feature_id`, that value wins; when neither side pins, the seller selects from the agent's `governance.creative_features` catalog.").optional() }).strict().describe("Buyer's representation that this embedding can be verified by a governance agent on the seller's `creative_policy.accepted_verifiers` list. The `agent_url` MUST match (canonicalized) one of the seller's published `accepted_verifiers[].agent_url` entries; sellers reject `sync_creatives` submissions whose `verify_agent.agent_url` is off-list with `PROVENANCE_VERIFIER_NOT_ACCEPTED`. This is buyer-supplied evidence, not buyer-driven routing — the seller is the verifier-of-record and the seller controls which agent it actually calls (the seller MAY use a different on-list agent if it determines this is more appropriate; the seller does not call buyer-asserted endpoints outside its allowlist). MAY be omitted for self-verifiable embeddings (e.g., a C2PA text manifest with a public key the seller already trusts).").optional(), "embedded_at": z.string().datetime().describe("When the provenance data was embedded (ISO 8601)").optional() })).describe("Provenance metadata embedded within the content stream. Each entry declares one embedding layer: structured provenance data carried inside the content itself, as distinct from sidecar references (c2pa.manifest_url). Embedded provenance survives operations that break sidecar and file-level bindings: ad-server transcoding, CMS ingestion, copy-paste, reformatting, and CDN re-encoding. For ad-tech pipelines where content passes through multiple intermediaries, embedded provenance is the reliable path for provenance that persists from declaration through delivery. This is a declaration by the embedding party. The receiving party (the seller) is the verifier-of-record: it confirms the claim by calling a governance agent it trusts (typically one published in `creative_policy.accepted_verifiers`).").optional(), "watermarks": z.array(z.object({ "media_type": z.enum(["audio","image","video","text"]).describe("Media category of the watermarked content"), "provider": z.string().describe("Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context — not a wire identifier."), "verify_agent": z.object({ "agent_url": z.string().url().regex(new RegExp("^https://")).describe("URL of the governance agent the buyer represents was used to apply/detect this watermark. MUST use the `https://` scheme and MUST appear in the seller's `creative_policy.accepted_verifiers[].agent_url` list (canonicalized per /docs/reference/url-canonicalization: lowercase scheme and host, strip default port, normalize path dot-segments). Sellers MUST NOT call this URL until the canonicalized match is confirmed."), "feature_id": z.string().describe("Optional `feature_id` the buyer represents the seller should request via `get_creative_features` (e.g., `imatag.watermark_detected`). SHOULD match the `feature_id` declared on the matching `accepted_verifiers[]` entry, or be omitted to defer the selector to the seller. When the seller's entry pins a `feature_id`, that value wins; when neither side pins, the seller selects from the agent's `governance.creative_features` catalog.").optional() }).strict().describe("Buyer's representation that this watermark can be detected by a governance agent on the seller's `creative_policy.accepted_verifiers` list. The `agent_url` MUST match (canonicalized) one of the seller's published `accepted_verifiers[].agent_url` entries; sellers reject `sync_creatives` submissions whose `verify_agent.agent_url` is off-list with `PROVENANCE_VERIFIER_NOT_ACCEPTED`. This is buyer-supplied evidence, not buyer-driven routing — the seller is the verifier-of-record and the seller controls which agent it actually calls (the seller MAY use a different on-list agent if it determines this is more appropriate; the seller does not call buyer-asserted endpoints outside its allowlist).").optional(), "c2pa_action": z.enum(["c2pa.watermarked.bound","c2pa.watermarked.unbound"]).describe("C2PA action classification for this watermark").optional(), "embedded_at": z.string().datetime().describe("When the watermark was applied (ISO 8601)").optional() })).describe("Content watermarks applied to this asset. Each entry declares one watermarking layer: a content modification that encodes an identifier or fingerprint within the asset. Watermarks differ from embedded provenance: a watermark encodes an identifier (who generated it, who owns it), while embedded provenance carries or references a structured provenance record (the full chain of custody). A single asset may carry both. Aligns with C2PA action taxonomy: c2pa.watermarked.bound (watermark linked to a C2PA manifest) and c2pa.watermarked.unbound (watermark independent of any manifest). This is a declaration by the watermarking party. The receiving party (the seller) is the verifier-of-record: it confirms the claim by calling a governance agent it trusts (typically one published in `creative_policy.accepted_verifiers`).").optional(), "disclosure": z.object({ "required": z.boolean().describe("The declaring party's claim that AI disclosure is required for this content under applicable regulations. This is a declared signal carried through the supply chain — useful as a routing and audit input — not a regulatory determination made by the protocol. Receiving parties remain responsible for their own jurisdictional analysis and should not treat `required: false` as compliance cover."), "jurisdictions": z.array(z.object({ "country": z.string().describe("ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')"), "region": z.string().describe("Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)").optional(), "regulation": z.string().describe("Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')"), "label_text": z.string().describe("Required disclosure label text for this jurisdiction, in the local language").optional(), "render_guidance": z.object({ "persistence": z.enum(["continuous","initial","flexible"]).describe("How long the disclosure must persist during content playback or display").optional(), "min_duration_ms": z.number().int().gte(1).describe("Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial — without it, the duration is at the publisher's discretion. At serve time the publisher reads this from provenance since the brief is not available.").optional(), "positions": z.array(z.enum(["prominent","footer","audio","subtitle","overlay","end_card","pre_roll","companion"]).describe("Where a required disclosure should appear within a creative. Used by creative briefs to specify disclosure placement and by formats to declare which positions they can render.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Preferred disclosure positions in priority order. The first position a format supports should be used.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension object for platform-specific, vendor-namespaced parameters. Extensions are always optional and must be namespaced under a vendor/platform key (e.g., ext.gam, ext.roku). Used for custom capabilities, partner-specific configuration, and features being proposed for standardization.").optional() }).describe("How the disclosure should be rendered for this jurisdiction. Expresses the declaring party's intent for persistence and position based on regulatory requirements. Publishers control actual rendering but governance agents can audit whether guidance was followed.").optional() })).describe("Jurisdictions where disclosure obligations apply").optional() }).describe("Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.").optional(), "verification": z.array(z.object({ "verified_by": z.string().describe("Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')"), "verified_time": z.string().datetime().describe("When the verification was performed (ISO 8601)").optional(), "result": z.enum(["authentic","ai_generated","ai_modified","inconclusive"]).describe("Verification outcome"), "confidence": z.number().gte(0).lte(1).describe("Confidence score of the verification result (0.0 to 1.0)").optional(), "details_url": z.string().url().describe("URL to the full verification report").optional() })).describe("Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim — verification results attached by the declaring party are supplementary. The enforcing party (e.g., seller/publisher) should run its own verification via get_creative_features or calibrate_content.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension object for platform-specific, vendor-namespaced parameters. Extensions are always optional and must be namespaced under a vendor/platform key (e.g., ext.gam, ext.roku). Used for custom capabilities, partner-specific configuration, and features being proposed for standardization.").optional() }).describe("Provenance metadata for this asset, overrides manifest-level provenance").optional() }).catchall(z.any()).describe("Override logo asset.").optional(), "colors": z.object({ "primary": z.string().regex(new RegExp("^#[0-9a-fA-F]{6}$")).optional(), "secondary": z.string().regex(new RegExp("^#[0-9a-fA-F]{6}$")).optional(), "accent": z.string().regex(new RegExp("^#[0-9a-fA-F]{6}$")).optional() }).catchall(z.any()).describe("Override brand colors (hex strings).").optional(), "voice": z.string().describe("Override brand-voice description for surface-composed text/audio output.").optional(), "tagline": z.string().describe("Override tagline.").optional() }).catchall(z.any()).describe("Inline override for brand-kit fields normally resolved from `/.well-known/brand.json` on `domain` (logo, colors, voice, tagline). Use when brand.json is missing, stale, or inappropriate for this specific call — e.g., a campaign-scoped tagline, a co-branded creative, a freshly-rebranded color palette the brand.json hasn't shipped yet. Same inline-override pattern as `industries` and `data_subject_contestation` above: brand.json is canonical, the override is per-call. Adopters needing to override fields outside this subset (`voice_attributes`, `prohibited_terms`, etc.) MUST publish a different brand.json and reference it via a different `domain` — the inline override is intentionally narrow to a small high-traffic subset.\n\n**Merge semantics (normative).** The merge is **field-level**, not whole-object replacement. Each field within `brand_kit_override` (`logo`, `colors`, `voice`, `tagline`) is evaluated independently — when a field is present on the override the override value applies; when a field is absent the brand.json value applies (or is absent if brand.json doesn't carry one either). For composite fields (`colors.primary`, `colors.secondary`, `colors.accent`), the merge is one level deeper: each color slot is evaluated independently — a producer can override `colors.primary` while still inheriting `colors.secondary` from brand.json. SDKs MUST NOT treat a present `brand_kit_override.colors` as wiping the brand.json `colors` block entirely; only the per-slot fields present in the override take precedence. Without this rule, a partial-override semantics would diverge across SDKs and produce inconsistent rendering for the same payload.").optional() }).strict().describe("Identity issuer / attestation authority, referenced as a vendor BrandRef (e.g. {\"domain\": \"world.org\"}) — the same vendor-reference shape AdCP uses for measurement and signals vendors. The issuer's canonical domain is the anchor; it need not host a brand.json, but if it does, that is where its verifier metadata (scheme versions, verify endpoint, JWKs) lives. Issuer-agnostic: World ID, ISO 18013-5 mDL, and W3C-VC issuers all reference by domain. The relying party is namespaced by the issuer — identity is the tuple (issuer.domain, issuer.brand_id, relying_party_id), mirroring (vendor.domain, vendor.brand_id, metric_id)."), "scheme": z.string().describe("Proof scheme and version, e.g. \"world_id_v4\".").optional(), "relying_party_id": z.string().describe("The relying-party id registered with the issuer (and, for on-chain issuers like World ID, with the issuer's registry). One entity may operate many relying parties (scope=entity vs scope=property); this is the attestation's audience / linkability boundary, not an entity identifier."), "scope": z.enum(["entity","property"]).describe("Whether this relying_party_id is shared across the entity's properties (entity → a within-entity unique-human graph) or scoped to a single property (property → per-property pseudonyms, unlinkable across the entity).").optional() }).strict().describe("A verified-identity relying party an entity or brand operates. Used for attestation provenance in TMP Identity Match (the buyer checks a forwarded attestation's relying_party_id against the owner's published list). Issuer-agnostic; World ID is the first issuer.")).describe("Verified-identity relying parties scoped to this brand/property, for attestation provenance in TMP Identity Match. Use when a brand or property runs its own relying_party_id (per-property pseudonyms); entity-wide relying parties live on the house object. See specs/tmp-verified-identity-attestation.md.").optional(), "names": z.array(z.record(z.string(), z.string().min(1)).describe("A localized name using the legacy underscore-separated language/region key convention (for example 'en_US', 'fr_CA', or 'zh_CN') and a name value. Bare language codes such as 'en' are also accepted for backwards compatibility. These keys are not AdCP locale-tag values: a bridge to /schemas/core/locale-tag.json replaces '_' with '-', applies the AdCP canonical wire profile, and validates the result rather than passing the key through unchanged.")).describe("Localized brand names. Multiple entries per language allowed for aliases."), "keller_type": z.enum(["master","sub_brand","endorsed","independent"]).describe("Brand architecture type from Keller's theory. master: primary brand of house. sub_brand: carries parent name (Nike SB). endorsed: independent identity backed by parent (Air Jordan 'by Nike'). independent: operates separately (Converse under Nike, Inc.)").optional(), "parent_brand": z.string().regex(new RegExp("^[a-z0-9_]+$")).describe("Parent brand ID for sub-brands and endorsed brands").optional(), "description": z.string().describe("Brand description").optional(), "industries": z.array(z.string()).describe("Brand industries (e.g., ['automotive'] or ['pharmaceutical', 'cpg'] for a consumer health company). Describes what the company does — not what regulatory regimes apply (use policy_categories for that).").optional(), "target_audience": z.string().describe("Primary target audience").optional(), "logos": z.array(z.object({ "id": z.string().regex(new RegExp("^[a-z0-9][a-z0-9_-]*$")).describe("Stable identifier for this logo entry. Recommended when logo usage rules or mark lockups need to bind to a specific logo asset.").optional(), "url": z.string().url().describe("URL to the logo asset"), "orientation": z.enum(["square","horizontal","vertical","stacked"]).describe("Logo aspect ratio orientation. square: ~1:1, horizontal: wide, vertical: tall, stacked: vertically arranged elements").optional(), "background": z.enum(["dark-bg","light-bg","transparent-bg"]).describe("Background compatibility. dark-bg: use on dark backgrounds, light-bg: use on light backgrounds, transparent-bg: has transparent background").optional(), "variant": z.enum(["primary","secondary","icon","wordmark","full-lockup"]).describe("Logo variant type. primary: main logo, secondary: alternative, icon: symbol only, wordmark: text only, full-lockup: complete logo").optional(), "tags": z.array(z.string()).describe("Additional semantic tags for custom categorization beyond the standard orientation, background, and variant fields").optional(), "slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Canonical renderer slots where this logo is appropriate. Consumers SHOULD prefer this over inferring from tags or usage prose when selecting a logo for a specific UI surface.").optional(), "usage": z.string().describe("Human-readable description of when to use this logo variant (e.g., 'Primary logo for use on light backgrounds')").optional(), "width": z.number().int().describe("Width in pixels").optional(), "height": z.number().int().describe("Height in pixels").optional() }).catchall(z.any()).describe("Brand logo asset with structured fields for orientation, background compatibility, and variant type")).describe("Brand logo assets").optional(), "colors": z.object({ "primary": z.union([z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")), z.array(z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")))]).optional(), "secondary": z.union([z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")), z.array(z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")))]).optional(), "accent": z.union([z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")), z.array(z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")))]).optional(), "background": z.union([z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")), z.array(z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")))]).optional(), "text": z.union([z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")), z.array(z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")))]).optional(), "heading": z.union([z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")), z.array(z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")))]).optional(), "body": z.union([z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")), z.array(z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")))]).optional(), "label": z.union([z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")), z.array(z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")))]).optional(), "border": z.union([z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")), z.array(z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")))]).optional(), "divider": z.union([z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")), z.array(z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")))]).optional(), "surface_1": z.union([z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")), z.array(z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")))]).optional(), "surface_2": z.union([z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")), z.array(z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")))]).optional() }).catchall(z.union([z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")), z.array(z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")))])).describe("Brand color palette. Each role accepts a single hex color or an array of hex colors for brands with multiple values per role. Beyond the core five roles, brands can provide additional color roles for finer granularity — heading, body, label, border, divider, surface_1, surface_2, etc.").optional(), "fonts": z.object({ "primary": z.union([z.string().describe("CSS font-family name (e.g., 'Montserrat', 'Arial, sans-serif')"), z.object({ "family": z.string().describe("CSS font-family name (e.g., 'Brand Sans')"), "files": z.array(z.object({ "url": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL to the font file (WOFF2, TTF, or OTF)"), "weight": z.number().int().gte(100).lte(900).describe("CSS numeric font-weight for static fonts (100-900)").optional(), "weight_range": z.array(z.number().int().gte(100).lte(900)).max(2).describe("Variable font weight axis range as [min, max] (e.g., [100, 900]). Use instead of weight for variable fonts.").optional(), "style": z.enum(["normal","italic","oblique"]).describe("CSS font-style").optional() }).catchall(z.any()).describe("A font file with weight, style, and variable font metadata")).max(36).describe("Font files for different weights and styles").optional(), "opentype_features": z.array(z.string().regex(new RegExp("^[a-z0-9]{4}$"))).max(20).describe("OpenType feature tags to enable (e.g., ['ss01', 'tnum', 'cv01']). These are four-character tags per the OpenType spec.").optional(), "fallbacks": z.array(z.string().max(100)).max(10).describe("Ordered fallback font-family names for when the primary font is unavailable or does not support the required script (e.g., ['Noto Sans Arabic', 'Noto Sans SC', 'sans-serif'])").optional() }).catchall(z.any()).describe("Structured font with family name, downloadable font files, and typographic metadata")]).describe("Primary font family").optional(), "secondary": z.union([z.string().describe("CSS font-family name (e.g., 'Montserrat', 'Arial, sans-serif')"), z.object({ "family": z.string().describe("CSS font-family name (e.g., 'Brand Sans')"), "files": z.array(z.object({ "url": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL to the font file (WOFF2, TTF, or OTF)"), "weight": z.number().int().gte(100).lte(900).describe("CSS numeric font-weight for static fonts (100-900)").optional(), "weight_range": z.array(z.number().int().gte(100).lte(900)).max(2).describe("Variable font weight axis range as [min, max] (e.g., [100, 900]). Use instead of weight for variable fonts.").optional(), "style": z.enum(["normal","italic","oblique"]).describe("CSS font-style").optional() }).catchall(z.any()).describe("A font file with weight, style, and variable font metadata")).max(36).describe("Font files for different weights and styles").optional(), "opentype_features": z.array(z.string().regex(new RegExp("^[a-z0-9]{4}$"))).max(20).describe("OpenType feature tags to enable (e.g., ['ss01', 'tnum', 'cv01']). These are four-character tags per the OpenType spec.").optional(), "fallbacks": z.array(z.string().max(100)).max(10).describe("Ordered fallback font-family names for when the primary font is unavailable or does not support the required script (e.g., ['Noto Sans Arabic', 'Noto Sans SC', 'sans-serif'])").optional() }).catchall(z.any()).describe("Structured font with family name, downloadable font files, and typographic metadata")]).describe("Secondary font family").optional() }).catchall(z.union([z.string().describe("CSS font-family name (e.g., 'Montserrat', 'Arial, sans-serif')"), z.object({ "family": z.string().describe("CSS font-family name (e.g., 'Brand Sans')"), "files": z.array(z.object({ "url": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL to the font file (WOFF2, TTF, or OTF)"), "weight": z.number().int().gte(100).lte(900).describe("CSS numeric font-weight for static fonts (100-900)").optional(), "weight_range": z.array(z.number().int().gte(100).lte(900)).max(2).describe("Variable font weight axis range as [min, max] (e.g., [100, 900]). Use instead of weight for variable fonts.").optional(), "style": z.enum(["normal","italic","oblique"]).describe("CSS font-style").optional() }).catchall(z.any()).describe("A font file with weight, style, and variable font metadata")).max(36).describe("Font files for different weights and styles").optional(), "opentype_features": z.array(z.string().regex(new RegExp("^[a-z0-9]{4}$"))).max(20).describe("OpenType feature tags to enable (e.g., ['ss01', 'tnum', 'cv01']). These are four-character tags per the OpenType spec.").optional(), "fallbacks": z.array(z.string().max(100)).max(10).describe("Ordered fallback font-family names for when the primary font is unavailable or does not support the required script (e.g., ['Noto Sans Arabic', 'Noto Sans SC', 'sans-serif'])").optional() }).catchall(z.any()).describe("Structured font with family name, downloadable font files, and typographic metadata")]).describe("A font role entry. Either a CSS font-family string (simple) or a structured object with family name and font files (rich).")).describe("Brand typography. Each key is a role name (e.g., 'primary', 'secondary') referenced by type_scale entries. Values are either a CSS font-family string or a structured object with font files for reliable resolution.").optional(), "tone": z.union([z.string().describe("Simple tone descriptors for backwards compatibility"), z.object({ "voice": z.union([z.string(), z.array(z.record(z.string(), z.string().min(1)).describe("A localized name using the legacy underscore-separated language/region key convention (for example 'en_US', 'fr_CA', or 'zh_CN') and a name value. Bare language codes such as 'en' are also accepted for backwards compatibility. These keys are not AdCP locale-tag values: a bridge to /schemas/core/locale-tag.json replaces '_' with '-', applies the AdCP canonical wire profile, and validates the result rather than passing the key through unchanged."))]).describe("High-level voice descriptor, either a legacy plain string or localized values.").optional(), "attributes": z.union([z.array(z.string()), z.array(z.record(z.string(), z.array(z.string())).describe("One locale mapped to a complete ordered string list. The list is selected as a unit so readers never assemble mixed-language output from independently localized elements."))]).describe("Personality traits that characterize the brand voice. Localized lists are selected as a whole.").optional(), "dos": z.union([z.array(z.string()), z.array(z.record(z.string(), z.array(z.string())).describe("One locale mapped to a complete ordered string list. The list is selected as a unit so readers never assemble mixed-language output from independently localized elements."))]).describe("Guidance for copy generation - what TO do. Localized lists are selected as a whole.").optional(), "donts": z.union([z.array(z.string()), z.array(z.record(z.string(), z.array(z.string())).describe("One locale mapped to a complete ordered string list. The list is selected as a unit so readers never assemble mixed-language output from independently localized elements."))]).describe("Guardrails to avoid brand violations - what NOT to do. Localized lists are selected as a whole.").optional() }).describe("Structured brand voice guidelines")]).describe("Brand voice and messaging tone guidelines").optional(), "tagline": z.union([z.string().describe("Plain tagline string for backwards compatibility"), z.array(z.record(z.string(), z.string().min(1)).describe("A localized name using the legacy underscore-separated language/region key convention (for example 'en_US', 'fr_CA', or 'zh_CN') and a name value. Bare language codes such as 'en' are also accepted for backwards compatibility. These keys are not AdCP locale-tag values: a bridge to /schemas/core/locale-tag.json replaces '_' with '-', applies the AdCP canonical wire profile, and validates the result rather than passing the key through unchanged.")).describe("Localized taglines with BCP 47 locale codes")]).describe("Brand tagline or slogan. Accepts a plain string or a localized array matching the names pattern.").optional(), "assets": z.array(z.object({ "asset_id": z.string().describe("Unique identifier"), "asset_type": z.enum(["image","video","audio","text","markdown","html","css","javascript","zip","vast","daast","url","webhook","brief","catalog","published_post"]).describe("Type of asset content"), "url": z.string().url().describe("URL to CDN-hosted asset file"), "tags": z.array(z.string()).describe("Tags for discovery (e.g., 'hero', 'lifestyle', 'product', 'holiday')").optional(), "name": z.union([z.string(), z.array(z.record(z.string(), z.string().min(1)).describe("A localized name using the legacy underscore-separated language/region key convention (for example 'en_US', 'fr_CA', or 'zh_CN') and a name value. Bare language codes such as 'en' are also accepted for backwards compatibility. These keys are not AdCP locale-tag values: a bridge to /schemas/core/locale-tag.json replaces '_' with '-', applies the AdCP canonical wire profile, and validates the result rather than passing the key through unchanged."))]).describe("Human-readable name, either a legacy plain string or localized values.").optional(), "description": z.union([z.string(), z.array(z.record(z.string(), z.string().min(1)).describe("A localized name using the legacy underscore-separated language/region key convention (for example 'en_US', 'fr_CA', or 'zh_CN') and a name value. Bare language codes such as 'en' are also accepted for backwards compatibility. These keys are not AdCP locale-tag values: a bridge to /schemas/core/locale-tag.json replaces '_' with '-', applies the AdCP canonical wire profile, and validates the result rather than passing the key through unchanged."))]).describe("Asset description or usage notes, either a legacy plain string or localized values.").optional(), "width": z.number().int().describe("Image/video width in pixels").optional(), "height": z.number().int().describe("Image/video height in pixels").optional(), "duration_seconds": z.number().describe("Video/audio duration in seconds").optional(), "file_size_bytes": z.number().int().describe("File size in bytes").optional(), "format": z.string().describe("File format (e.g., 'jpg', 'mp4', 'mp3')").optional(), "metadata": z.record(z.string(), z.any()).describe("Additional asset-specific metadata").optional() }).catchall(z.any()).describe("Brand asset (image, video, audio, text)")).describe("Brand asset library").optional(), "properties": z.array(z.object({ "type": z.enum(["website","mobile_app","ctv_app","desktop_app","dooh","podcast","radio","linear_tv","streaming_audio","ai_assistant"]).describe("Property type"), "identifier": z.string().min(1).describe("Property identifier - domain for websites, bundle ID for apps"), "store": z.enum(["apple","google","amazon","roku","samsung","lg","other"]).describe("App store for mobile/CTV apps").optional(), "region": z.string().regex(new RegExp("^([A-Z]{2}|global)$")).describe("ISO 3166-1 alpha-2 country code or 'global'").optional(), "primary": z.boolean().describe("Whether this is the primary property for the brand").default(false), "relationship": z.enum(["owned","direct","delegated","ad_network"]).describe("How this brand relates to the property. 'owned': the brand owns and operates this property (default) and has no adagents.json delegation_type counterpart. 'direct': the brand is the direct sales path for this property, even if a third party operates the software (e.g., a publisher's in-house ad team using a vendor's tech). 'delegated': the brand manages monetization for this property — they are in charge of ad sales (e.g., Mediavine managing a food blog). 'ad_network': the brand sells this property's inventory as part of a network or exchange — they are a path to the inventory, not the path (e.g., PubMatic as an SSP). For non-owned properties, the publisher confirms the relationship by setting the matching delegation_type on the agent's authorization in their adagents.json.").default("owned") }).catchall(z.any()).describe("A digital property associated with a brand. Defaults to owned; use 'relationship' to declare direct, delegated, or ad_network properties. For delegated and network paths, these values match the delegation_type field in adagents.json, creating a bilateral verification chain: the operator declares the relationship here, the publisher confirms by setting the same delegation_type on the agent's authorization in their adagents.json. 'owned' is an inline ownership declaration with no adagents.json counterpart.")).describe("Digital properties associated with this brand — owned, managed, or represented").optional(), "product_catalog": z.object({ "feed_url": z.string().url().describe("URL to product catalog feed"), "feed_format": z.enum(["google_merchant_center","facebook_catalog","shopify","linkedin_jobs","tiktok_shop","pinterest_catalog","openai_product_feed","custom"]).describe("Format of the product feed").optional(), "categories": z.array(z.string()).describe("Product categories available in the catalog").optional(), "last_updated": z.string().datetime().describe("When the product catalog was last updated").optional(), "update_frequency": z.enum(["realtime","hourly","daily","weekly"]).describe("How frequently the product catalog is updated").optional(), "agentic_checkout": z.object({ "endpoint": z.string().url().describe("Base URL for checkout session API"), "spec": z.string().describe("Checkout API specification identifier. Use a namespaced string to identify the checkout protocol (e.g., vendor-prefixed or custom). Vendor-specific values belong under ext.{vendor}."), "supported_payment_providers": z.array(z.string()).describe("Payment providers supported by this checkout endpoint").optional() }).describe("Agentic checkout endpoint configuration").optional() }).catchall(z.any()).describe("Product catalog for e-commerce brands").optional(), "privacy_policy_url": z.string().url().describe("URL to the brand's privacy policy").optional(), "data_subject_contestation": z.object({ "url": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL to a human-accessible contestation form or information page.").optional(), "email": z.string().email().describe("Email address for contestation requests. Deployer MUST monitor and respond within the timelines set by applicable law (e.g., 1 month under GDPR Art. 12(3)).").optional(), "languages": z.array(z.string()).describe("BCP 47 language tags the contestation channel supports (e.g., ['en', 'de', 'fr']). At minimum SHOULD include a language spoken in every jurisdiction where the brand runs regulated-vertical campaigns.").optional() }).strict().and(z.union([z.any(), z.any()])).describe("Contact point where a data subject can request human intervention, express their view, or contest an automated decision — satisfying GDPR Article 22(3) and EU AI Act Article 26(11) transparency obligations. This is a contact reference (URL, email, or both), not a machine-callable API. AdCP surfaces the pointer; the deployer runs the contestation workflow.").optional(), "disclaimers": z.array(z.object({ "text": z.string(), "context": z.string().optional(), "required": z.boolean().default(true) })).describe("Legal disclaimers for creatives").optional(), "trademarks": z.array(z.object({ "registry": z.string().describe("Trademark registry (e.g., 'USPTO', 'EUIPO', 'JPO', 'CNIPA')"), "number": z.string().describe("Registration number as issued by the registry"), "mark": z.string().describe("The registered mark as published"), "status": z.enum(["active","pending","abandoned","cancelled","expired"]).describe("Registration status. Omit for active marks if status tracking is not maintained.").optional(), "license_type": z.enum(["owned","licensed_in","licensed_out"]).describe("Whether the publisher owns the mark, licenses it from another entity, or licenses it to others. 'owned' is the default if omitted.").optional(), "licensor_domain": z.string().regex(new RegExp("^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$")).describe("Domain of the entity that licenses this mark to the publisher. Meaningful when license_type=licensed_in; omit otherwise.").optional(), "countries": z.array(z.string().min(2).max(2)).describe("ISO 3166-1 alpha-2 country codes where this registration applies. Omit for global or where the registry's jurisdiction is implicit.").optional(), "nice_classes": z.array(z.number().int().gte(1).lte(45)).describe("Nice Classification class numbers (1-45) covered by this registration. Disambiguates marks across industries (e.g., Delta-airline vs Delta-faucet). Omit if scope is implicit from registry.").optional() }).catchall(z.any()).describe("A registered trademark. May appear at house level (corporate marks, e.g., 'NIKE' owned by Nike, Inc.) or at brand level (brand-specific marks, e.g., 'CONVERSE' owned by Converse). Resolution between house- and brand-level trademarks is union — both lists are valid claims about marks the publisher controls.")).describe("Brand-level registered trademarks. Use for marks the brand owns or controls (e.g., a sub-brand's own marks distinct from the corporate parent). House-level trademarks live on the house object; resolution between the two is union — both lists are valid claims.").optional(), "voice_synthesis": z.object({ "provider": z.string().optional(), "voice_id": z.string().optional(), "settings": z.record(z.string(), z.any()).optional() }).catchall(z.any()).describe("TTS voice synthesis configuration for AI-generated audio").optional(), "avatar": z.object({ "provider": z.string().optional(), "avatar_id": z.string().optional(), "settings": z.record(z.string(), z.any()).optional() }).catchall(z.any()).describe("Visual avatar configuration").optional(), "visual_guidelines": z.object({ "photography": z.object({ "realism": z.enum(["natural","stylized","hyperreal","abstract"]).describe("Level of photographic realism").optional(), "lighting": z.string().describe("Lighting style (e.g., 'soft daylight', 'studio', 'golden hour', 'high-key', 'low-key')").optional(), "color_temperature": z.enum(["warm","neutral","cool"]).describe("Overall color temperature of photography").optional(), "contrast": z.enum(["low","medium","high"]).describe("Contrast level in photography").optional(), "depth_of_field": z.enum(["shallow","medium","deep"]).describe("Depth of field preference. shallow: blurred background with subject isolation, deep: everything in focus").optional(), "subject": z.object({ "people": z.object({ "age_range": z.string().describe("Target age range (e.g., '20-35')").optional(), "diversity": z.string().describe("Diversity representation (e.g., 'mixed', 'varied')").optional(), "mood": z.array(z.string()).describe("Mood descriptors (e.g., ['confident', 'relaxed'])").optional() }).catchall(z.any()).describe("People photography guidelines").optional(), "product_focus": z.enum(["in-use","isolated","lifestyle","detail"]).describe("How products are shown").optional(), "setting": z.string().describe("Environmental context for photography (e.g., 'indoor', 'outdoor', 'studio', 'urban', 'nature', 'workplace')").optional() }).catchall(z.any()).describe("Subject matter guidelines").optional(), "framing": z.object({ "subject_position": z.string().describe("Where the subject sits in frame (e.g., 'center', 'center-left', 'rule-of-thirds')").optional(), "crop_style": z.string().describe("Cropping convention (e.g., 'waist-up', 'full-body', 'close-up', 'wide')").optional(), "perspective": z.string().describe("Camera perspective (e.g., 'eye-level', 'overhead', 'low-angle')").optional() }).catchall(z.any()).describe("Camera framing rules").optional(), "preferred_aspect_ratios": z.array(z.string().regex(new RegExp("^\\d+:\\d+$"))).describe("Preferred aspect ratios for brand photography (e.g., '16:9', '4:5', '1:1')").optional(), "tags": z.array(z.string()).describe("Additional style descriptors").optional() }).catchall(z.any()).describe("Photography style rules for generative creative systems. Defines how brand photography should look when selected or generated.").optional(), "graphic_style": z.object({ "style_type": z.enum(["flat_illustration","geometric","gradient_mesh","editorial_collage","hand_drawn","minimal_line_art","3d_render","isometric","photographic_composite"]).describe("Primary graphic style").optional(), "stroke_style": z.enum(["rounded","square","mixed","none"]).describe("Stroke end/join style").optional(), "stroke_weight": z.string().describe("Stroke weight (e.g., '2px', 'thin', 'bold')").optional(), "corner_radius": z.string().describe("Default corner radius for graphic and illustration elements (e.g., '12px', '8px', 'sharp'). For UI component radii (buttons, cards, inputs), see visual_guidelines.border_radius.").optional(), "tags": z.array(z.string()).describe("Additional style descriptors").optional() }).catchall(z.any()).describe("Visual language for brand graphics and illustrations").optional(), "shapes": z.object({ "primary_shape": z.string().describe("Primary brand shape (e.g., 'rounded_rectangle', 'circle', 'hexagon')").optional(), "secondary_shapes": z.array(z.string()).describe("Secondary shapes in the brand vocabulary").optional(), "usage": z.object({ "max_per_layout": z.number().int().describe("Maximum distinct shapes per layout").optional(), "overlap_allowed": z.boolean().describe("Whether shapes may overlap").optional() }).catchall(z.any()).describe("Shape usage rules").optional() }).catchall(z.any()).describe("Distinctive shapes used as part of brand visual identity").optional(), "iconography": z.object({ "style": z.enum(["outline","filled","duotone","flat","glyph","hand_drawn"]).describe("Icon rendering style").optional(), "stroke_weight": z.string().describe("Icon stroke weight (e.g., '2px', '1.5px')").optional(), "corner_style": z.enum(["rounded","square","mixed"]).describe("Corner style for icon paths").optional(), "usage": z.object({ "max_per_frame": z.number().int().describe("Maximum icons per creative frame").optional(), "size_ratio": z.string().describe("Icon-to-layout size ratio (e.g., '1:8')").optional() }).catchall(z.any()).describe("Icon usage rules").optional() }).catchall(z.any()).describe("Icon style system and usage rules").optional(), "composition": z.object({ "overlays": z.object({ "gradient_style": z.enum(["linear","radial","conic","none"]).describe("Gradient type for overlays").optional(), "gradient_direction": z.string().describe("Gradient direction (e.g., '45deg', 'to-bottom-right')").optional(), "opacity": z.string().describe("Overlay opacity (e.g., '70%')").optional() }).catchall(z.any()).describe("Graphic overlay rules").optional(), "texture": z.object({ "style": z.enum(["none","subtle_grain","noise","paper","fabric","concrete"]).describe("Texture style applied to creative assets").optional(), "intensity": z.enum(["low","medium","high"]).describe("Texture intensity").optional() }).catchall(z.any()).describe("Texture treatment rules").optional(), "backgrounds": z.object({ "types_allowed": z.array(z.enum(["solid_color","gradient","blurred_photo","image","video","pattern","transparent"])).describe("Permitted background types").optional() }).catchall(z.any()).describe("Background treatment rules").optional() }).catchall(z.any()).describe("Layout composition rules including overlays, textures, and backgrounds").optional(), "border_radius": z.object({ "none": z.string().describe("Explicitly sharp corners (e.g., '0')").optional(), "default": z.string().describe("Default border radius for UI components (e.g., '8px', '12px', '0'). For graphic/illustration elements, see graphic_style.corner_radius.").optional(), "small": z.string().describe("Small border radius for compact elements (e.g., '4px')").optional(), "large": z.string().describe("Large border radius for cards and containers (e.g., '16px', '24px')").optional(), "pill": z.string().describe("Fully rounded / pill shape (e.g., '999px')").optional() }).catchall(z.string()).describe("Named border radius presets for UI components and layout elements. One of the most visible brand differentiators — Airbnb uses generous 20px, Stripe uses precise 4–8px, Spotify uses pill/999px.").optional(), "elevation": z.object({ "none": z.string().describe("No shadow (e.g., 'none')").optional(), "subtle": z.string().describe("Subtle shadow for slight lift (e.g., '0 1px 2px rgba(0,0,0,0.05)')").optional(), "card": z.string().describe("Card-level shadow (e.g., '0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -2px rgba(0,0,0,0.1)')").optional(), "modal": z.string().describe("Modal/overlay shadow (e.g., '0 20px 25px -5px rgba(0,0,0,0.1), 0 8px 10px -6px rgba(0,0,0,0.1)')").optional() }).catchall(z.string()).describe("Named shadow/elevation levels. Brands use elevation as identity — from Stripe's blue-tinted multi-layer shadows to Apple's single diffuse shadow. Values are CSS box-shadow syntax.").optional(), "spacing": z.object({ "unit": z.string().describe("Base grid unit this scale was designed from (e.g., '8px', '4px'). Informational — agents should use the named scale values, not compute from this.").optional(), "scale": z.object({ "xs": z.string().describe("Extra small spacing (e.g., '4px')").optional(), "sm": z.string().describe("Small spacing (e.g., '8px')").optional(), "md": z.string().describe("Medium spacing (e.g., '16px')").optional(), "lg": z.string().describe("Large spacing (e.g., '24px')").optional(), "xl": z.string().describe("Extra large spacing (e.g., '32px')").optional(), "2xl": z.string().describe("Section-level spacing (e.g., '48px', '64px')").optional() }).catchall(z.string()).describe("Named spacing scale built from the base unit").optional() }).strict().describe("Spacing system for consistent layout rhythm. Most design systems use an 8px base grid.").optional(), "graphic_elements": z.array(z.object({ "name": z.string().describe("Element name (e.g., 'Paper Tear', 'Brand Watermark', 'Section Divider')"), "type": z.enum(["border","divider","frame","watermark","pattern","texture_overlay","decorative"]).describe("Element type").optional(), "description": z.string().describe("How the element is used in layouts").optional(), "orientation": z.enum(["horizontal","vertical","any"]).describe("Preferred orientation when used in layouts").optional(), "colors": z.array(z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")).describe("A single hex color value")).describe("Colors this element may appear in").optional(), "max_per_layout": z.number().int().describe("Maximum instances per layout").optional() }).catchall(z.any()).describe("A reusable decorative or structural visual element that is part of the brand identity (e.g., torn paper edges, watermarks, dividers, background patterns)")).describe("Reusable decorative elements that are part of the brand visual identity (e.g., torn paper edges, watermarks, dividers)").optional(), "motion": z.object({ "transition_style": z.enum(["cut","dissolve","slide","wipe","zoom","fade"]).describe("Primary transition style between scenes").optional(), "animation_speed": z.enum(["slow","moderate","fast"]).describe("Overall animation pacing").optional(), "easing": z.string().describe("Default easing function (e.g., 'ease-in-out', 'spring', 'linear')").optional(), "text_entrance": z.enum(["fade","typewriter","slide_up","slide_left","scale","none"]).describe("How text enters the frame").optional(), "pacing": z.enum(["lingering","moderate","fast_cuts"]).describe("Overall editing rhythm").optional(), "kinetic_typography": z.boolean().describe("Whether animated/kinetic typography is allowed").optional(), "tags": z.array(z.string()).describe("Additional motion style descriptors").optional() }).catchall(z.any()).describe("Motion and animation rules for video, animated display, and interactive formats").optional(), "logo_placement": z.object({ "preferred_position": z.enum(["top-left","top-center","top-right","bottom-left","bottom-center","bottom-right","center"]).describe("Preferred logo position in layouts").optional(), "min_clear_space": z.string().describe("Minimum clear space around the logo, expressed as a multiple of logo height (e.g., '0.5x', '1x') or fixed value (e.g., '16px')").optional(), "min_height": z.string().describe("Minimum logo height to maintain legibility (e.g., '40px', '24px')").optional(), "background_contrast": z.enum(["light_only","dark_only","any"]).describe("Permitted background contrast behind logo").optional() }).catchall(z.any()).describe("Logo placement and clear space rules for automated creative production").optional(), "colorways": z.array(z.object({ "name": z.string().describe("Colorway name (e.g., 'primary', 'inverted', 'subtle')"), "foreground": z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")).describe("A single hex color value"), "background": z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")).describe("A single hex color value"), "accent": z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")).describe("A single hex color value").optional(), "border": z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")).describe("A single hex color value").optional(), "cta_foreground": z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")).describe("CTA text/icon color, if different from foreground").optional(), "cta_background": z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")).describe("CTA button/container color, if different from accent").optional(), "channels": z.array(z.string()).describe("Channels or contexts where this colorway applies (e.g., 'online', 'print', 'pos', 'social', 'outdoor'). Omit for universal colorways.").optional() }).catchall(z.any()).describe("A named color pairing that defines how colors work together. Colorways ensure foreground/background combinations are always on-brand and accessible.")).describe("Named color pairings for consistent foreground/background combinations").optional(), "color_constraints": z.array(z.object({ "color": z.object({ "kind": z.enum(["name","value","surface"]).describe("Discriminator for the color reference variant.").optional(), "name": z.string().describe("Key from the colors object, such as market_yellow, river_green, or any custom palette key. Unambiguous lookup into colors{}.").optional(), "value": z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")).describe("Literal hex color when no palette key exists.").optional(), "surface": z.enum(["background","foreground","text","logo_background","cta","accent","border","icon","graphic_element"]).describe("Usage surface rather than a specific color value.").optional() }).strict().and(z.union([z.object({ "kind": z.literal("name") }), z.object({ "kind": z.literal("value") }), z.object({ "kind": z.literal("surface") })])).describe("Color role or value this constraint governs."), "applies_to": z.array(z.enum(["background","foreground","text","logo_background","cta","accent","border","icon","graphic_element"])).describe("Surfaces where this color may be used.").optional(), "allowed_on": z.array(z.object({ "kind": z.enum(["name","value","surface"]).describe("Discriminator for the color reference variant.").optional(), "name": z.string().describe("Key from the colors object, such as market_yellow, river_green, or any custom palette key. Unambiguous lookup into colors{}.").optional(), "value": z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")).describe("Literal hex color when no palette key exists.").optional(), "surface": z.enum(["background","foreground","text","logo_background","cta","accent","border","icon","graphic_element"]).describe("Usage surface rather than a specific color value.").optional() }).strict().and(z.union([z.object({ "kind": z.literal("name") }), z.object({ "kind": z.literal("value") }), z.object({ "kind": z.literal("surface") })])).describe("Reference to a brand color or usage surface for machine-readable guideline constraints. Use kind=name for palette lookup keys in colors, kind=value for a literal hex color, and kind=surface for layout surfaces such as background or text.")).describe("Backgrounds, surfaces, or color roles where this color is allowed.").optional(), "forbidden_on": z.array(z.object({ "kind": z.enum(["name","value","surface"]).describe("Discriminator for the color reference variant.").optional(), "name": z.string().describe("Key from the colors object, such as market_yellow, river_green, or any custom palette key. Unambiguous lookup into colors{}.").optional(), "value": z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")).describe("Literal hex color when no palette key exists.").optional(), "surface": z.enum(["background","foreground","text","logo_background","cta","accent","border","icon","graphic_element"]).describe("Usage surface rather than a specific color value.").optional() }).strict().and(z.union([z.object({ "kind": z.literal("name") }), z.object({ "kind": z.literal("value") }), z.object({ "kind": z.literal("surface") })])).describe("Reference to a brand color or usage surface for machine-readable guideline constraints. Use kind=name for palette lookup keys in colors, kind=value for a literal hex color, and kind=surface for layout surfaces such as background or text.")).describe("Backgrounds, surfaces, or color roles where this color is forbidden.").optional(), "never_pair_with": z.array(z.object({ "kind": z.enum(["name","value","surface"]).describe("Discriminator for the color reference variant.").optional(), "name": z.string().describe("Key from the colors object, such as market_yellow, river_green, or any custom palette key. Unambiguous lookup into colors{}.").optional(), "value": z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")).describe("Literal hex color when no palette key exists.").optional(), "surface": z.enum(["background","foreground","text","logo_background","cta","accent","border","icon","graphic_element"]).describe("Usage surface rather than a specific color value.").optional() }).strict().and(z.union([z.object({ "kind": z.literal("name") }), z.object({ "kind": z.literal("value") }), z.object({ "kind": z.literal("surface") })])).describe("Reference to a brand color or usage surface for machine-readable guideline constraints. Use kind=name for palette lookup keys in colors, kind=value for a literal hex color, and kind=surface for layout surfaces such as background or text.")).describe("Color roles or values that must not be paired with this color.").optional(), "contexts": z.array(z.string()).describe("Channels or creative contexts where this constraint applies, such as digital, print, social, or ctv_end_card.").optional(), "severity": z.enum(["must","should"]).describe("Strength of a machine-readable brand guideline constraint.").optional(), "description": z.string().describe("Human-readable rationale or source-language summary for the rule.").optional() }).catchall(z.any()).describe("Machine-readable rule constraining how a brand color may be used or paired. Use for accent-only colors, forbidden foreground/background combinations, and palette pairs that should never appear together.")).describe("Machine-readable constraints for color usage and pairings, such as accent-only rules or forbidden foreground/background combinations.").optional(), "logo_usage_rules": z.array(z.object({ "logo_url": z.string().url().describe("Specific logo asset URL this rule applies to. Omit when the rule applies by variant or tags.").optional(), "logo_id": z.string().regex(new RegExp("^[a-z0-9][a-z0-9_-]*$")).describe("Stable `logos[].id` this rule applies to. Prefer this over logo_url when the rule targets a specific logo entry.").optional(), "logo_variant": z.enum(["primary","secondary","icon","wordmark","full-lockup"]).describe("Logo variant this rule applies to.").optional(), "logo_tags": z.array(z.string()).describe("Logo tags this rule applies to.").optional(), "contexts": z.array(z.string()).describe("Creative contexts where this rule applies.").optional(), "slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Canonical renderer slots where this rule applies. Use this for deterministic logo-card, profile-mark, end-card, and lockup selection.").optional(), "minimum_size": z.object({ "width": z.string().describe("Minimum width, such as 48px or 12mm.").optional(), "height": z.string().describe("Minimum height, such as 18px or 6mm.").optional() }).strict().describe("Minimum rendered size needed for legibility.").optional(), "clear_space": z.string().describe("Minimum clear space around the logo, expressed in brand terms or units.").optional(), "allowed_backgrounds": z.array(z.object({ "kind": z.enum(["name","value","surface"]).describe("Discriminator for the color reference variant.").optional(), "name": z.string().describe("Key from the colors object, such as market_yellow, river_green, or any custom palette key. Unambiguous lookup into colors{}.").optional(), "value": z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")).describe("Literal hex color when no palette key exists.").optional(), "surface": z.enum(["background","foreground","text","logo_background","cta","accent","border","icon","graphic_element"]).describe("Usage surface rather than a specific color value.").optional() }).strict().and(z.union([z.object({ "kind": z.literal("name") }), z.object({ "kind": z.literal("value") }), z.object({ "kind": z.literal("surface") })])).describe("Reference to a brand color or usage surface for machine-readable guideline constraints. Use kind=name for palette lookup keys in colors, kind=value for a literal hex color, and kind=surface for layout surfaces such as background or text.")).describe("Background color roles, values, or surfaces where this logo may be placed.").optional(), "forbidden_backgrounds": z.array(z.object({ "kind": z.enum(["name","value","surface"]).describe("Discriminator for the color reference variant.").optional(), "name": z.string().describe("Key from the colors object, such as market_yellow, river_green, or any custom palette key. Unambiguous lookup into colors{}.").optional(), "value": z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")).describe("Literal hex color when no palette key exists.").optional(), "surface": z.enum(["background","foreground","text","logo_background","cta","accent","border","icon","graphic_element"]).describe("Usage surface rather than a specific color value.").optional() }).strict().and(z.union([z.object({ "kind": z.literal("name") }), z.object({ "kind": z.literal("value") }), z.object({ "kind": z.literal("surface") })])).describe("Reference to a brand color or usage surface for machine-readable guideline constraints. Use kind=name for palette lookup keys in colors, kind=value for a literal hex color, and kind=surface for layout surfaces such as background or text.")).describe("Background color roles, values, or surfaces where this logo must not be placed.").optional(), "forbidden_contexts": z.array(z.string()).describe("Contexts where this logo must not be used, such as photography_without_knockout.").optional(), "severity": z.enum(["must","should"]).describe("Strength of a machine-readable brand guideline constraint.").optional(), "description": z.string().describe("Human-readable rationale or source-language summary for the rule.").optional() }).catchall(z.any()).and(z.union([z.any(), z.any(), z.any(), z.any(), z.any()])).describe("Machine-readable logo selection and placement rule. Complements logos[].usage by making enforceable minimum size, clear-space, background, and context constraints queryable.")).describe("Machine-readable logo selection and placement constraints for minimum size, clear space, backgrounds, and contexts.").optional(), "mark_lockups": z.array(z.object({ "lockup_type": z.enum(["co_brand","secondary_mark","partner","sponsor","program","talent","custom"]).describe("Type of mark relationship governed by this lockup rule."), "ordering": z.enum(["brand_first","partner_first","equal","contextual"]).describe("Required visual ordering of the brand mark relative to partner or secondary marks.").optional(), "contexts": z.array(z.string()).describe("Creative contexts where this lockup rule applies.").optional(), "brand_logo_id": z.string().regex(new RegExp("^[a-z0-9][a-z0-9_-]*$")).describe("Stable `logos[].id` for the brand logo this lockup rule is anchored on.").optional(), "secondary_logo_ids": z.array(z.string().regex(new RegExp("^[a-z0-9][a-z0-9_-]*$")).describe("Stable identifier for a logo entry within this brand.json document. Use lowercase words separated by underscores or hyphens; do not key integrations on mutable asset URLs.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Stable `logos[].id` values for secondary, program, sponsor, or partner marks governed by this lockup rule when those marks are represented in this brand.json.").optional(), "separator": z.object({ "type": z.enum(["none","keyline","space","divider"]).describe("Separator style."), "color": z.object({ "kind": z.enum(["name","value","surface"]).describe("Discriminator for the color reference variant.").optional(), "name": z.string().describe("Key from the colors object, such as market_yellow, river_green, or any custom palette key. Unambiguous lookup into colors{}.").optional(), "value": z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")).describe("Literal hex color when no palette key exists.").optional(), "surface": z.enum(["background","foreground","text","logo_background","cta","accent","border","icon","graphic_element"]).describe("Usage surface rather than a specific color value.").optional() }).strict().and(z.union([z.object({ "kind": z.literal("name") }), z.object({ "kind": z.literal("value") }), z.object({ "kind": z.literal("surface") })])).describe("Reference to a brand color or usage surface for machine-readable guideline constraints. Use kind=name for palette lookup keys in colors, kind=value for a literal hex color, and kind=surface for layout surfaces such as background or text.").optional(), "width": z.string().describe("Separator width, such as 1px.").optional() }).catchall(z.any()).describe("Separator between marks, when required.").optional(), "min_gap": z.string().describe("Minimum gap between marks, expressed in brand terms or units.").optional(), "brand_min_optical_weight_ratio": z.number().gt(0).describe("Minimum optical weight of the brand mark relative to partner marks. 1 means at least equal.").optional(), "partner_max_optical_weight_ratio": z.number().gt(0).describe("Maximum optical weight of partner marks relative to the brand mark. 1 means no larger than the brand mark. Enforcement is at layout time, not parse time — this value signals to renderers and creative agents how much space to provision for each mark.").optional(), "severity": z.enum(["must","should"]).describe("Strength of a machine-readable brand guideline constraint.").optional(), "description": z.string().describe("Human-readable rationale or source-language summary for the lockup rule.").optional() }).catchall(z.any()).describe("Machine-readable layout constraints for co-brand, partner, sponsor, program, or secondary-mark lockups.")).describe("Machine-readable co-brand, partner, sponsor, program, or secondary-mark lockup rules.").optional(), "type_scale": z.object({ "base_width": z.string().describe("Reference canvas width these sizes were designed for (e.g., '1080px'). Generative systems should scale proportionally for other canvas sizes.").optional(), "heading": z.object({ "font": z.string().describe("Font reference. Use a key from the fonts object (e.g., 'primary', 'secondary') to reference a defined font role, or a literal CSS font-family string as a fallback.").optional(), "size": z.string().describe("Font size (e.g., '48px', '2rem')").optional(), "weight": z.string().describe("Font weight (e.g., '700', 'bold')").optional(), "line_height": z.string().describe("Line height (e.g., '1.2', '56px')").optional(), "letter_spacing": z.string().describe("Letter spacing (e.g., '-0.02em', '0.5px')").optional(), "text_transform": z.enum(["none","uppercase","lowercase","capitalize"]).describe("Text transformation").optional() }).catchall(z.any()).describe("A single entry in the type scale").optional(), "subheading": z.object({ "font": z.string().describe("Font reference. Use a key from the fonts object (e.g., 'primary', 'secondary') to reference a defined font role, or a literal CSS font-family string as a fallback.").optional(), "size": z.string().describe("Font size (e.g., '48px', '2rem')").optional(), "weight": z.string().describe("Font weight (e.g., '700', 'bold')").optional(), "line_height": z.string().describe("Line height (e.g., '1.2', '56px')").optional(), "letter_spacing": z.string().describe("Letter spacing (e.g., '-0.02em', '0.5px')").optional(), "text_transform": z.enum(["none","uppercase","lowercase","capitalize"]).describe("Text transformation").optional() }).catchall(z.any()).describe("A single entry in the type scale").optional(), "body": z.object({ "font": z.string().describe("Font reference. Use a key from the fonts object (e.g., 'primary', 'secondary') to reference a defined font role, or a literal CSS font-family string as a fallback.").optional(), "size": z.string().describe("Font size (e.g., '48px', '2rem')").optional(), "weight": z.string().describe("Font weight (e.g., '700', 'bold')").optional(), "line_height": z.string().describe("Line height (e.g., '1.2', '56px')").optional(), "letter_spacing": z.string().describe("Letter spacing (e.g., '-0.02em', '0.5px')").optional(), "text_transform": z.enum(["none","uppercase","lowercase","capitalize"]).describe("Text transformation").optional() }).catchall(z.any()).describe("A single entry in the type scale").optional(), "caption": z.object({ "font": z.string().describe("Font reference. Use a key from the fonts object (e.g., 'primary', 'secondary') to reference a defined font role, or a literal CSS font-family string as a fallback.").optional(), "size": z.string().describe("Font size (e.g., '48px', '2rem')").optional(), "weight": z.string().describe("Font weight (e.g., '700', 'bold')").optional(), "line_height": z.string().describe("Line height (e.g., '1.2', '56px')").optional(), "letter_spacing": z.string().describe("Letter spacing (e.g., '-0.02em', '0.5px')").optional(), "text_transform": z.enum(["none","uppercase","lowercase","capitalize"]).describe("Text transformation").optional() }).catchall(z.any()).describe("A single entry in the type scale").optional(), "cta": z.object({ "font": z.string().describe("Font reference. Use a key from the fonts object (e.g., 'primary', 'secondary') to reference a defined font role, or a literal CSS font-family string as a fallback.").optional(), "size": z.string().describe("Font size (e.g., '48px', '2rem')").optional(), "weight": z.string().describe("Font weight (e.g., '700', 'bold')").optional(), "line_height": z.string().describe("Line height (e.g., '1.2', '56px')").optional(), "letter_spacing": z.string().describe("Letter spacing (e.g., '-0.02em', '0.5px')").optional(), "text_transform": z.enum(["none","uppercase","lowercase","capitalize"]).describe("Text transformation").optional() }).catchall(z.any()).describe("A single entry in the type scale").optional() }).catchall(z.object({ "font": z.string().describe("Font reference. Use a key from the fonts object (e.g., 'primary', 'secondary') to reference a defined font role, or a literal CSS font-family string as a fallback.").optional(), "size": z.string().describe("Font size (e.g., '48px', '2rem')").optional(), "weight": z.string().describe("Font weight (e.g., '700', 'bold')").optional(), "line_height": z.string().describe("Line height (e.g., '1.2', '56px')").optional(), "letter_spacing": z.string().describe("Letter spacing (e.g., '-0.02em', '0.5px')").optional(), "text_transform": z.enum(["none","uppercase","lowercase","capitalize"]).describe("Text transformation").optional() }).catchall(z.any()).describe("A single entry in the type scale")).describe("Typography scale defining sizes and weights for different text roles. When sizes are in px, use base_width to indicate the reference canvas.").optional(), "asset_libraries": z.array(z.object({ "name": z.string().describe("Display name of the asset library"), "type": z.enum(["icon_set","illustration_system","image_library","video_library","template_library"]).describe("Type of asset library").optional(), "url": z.string().url().describe("URL to the asset library (for human access)"), "description": z.string().describe("Description of the library contents and usage").optional(), "color_guide": z.object({ "roles": z.array(z.string()).describe("Named color roles used in the library (e.g., base, shadow_1, highlight_1, stroke)").optional(), "palettes": z.array(z.object({ "name": z.string().describe("Palette name"), "colors": z.record(z.string(), z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")).describe("A single hex color value")).describe("Map of role names to hex color values") }).catchall(z.any())).describe("Named color palettes mapping roles to specific colors").optional() }).catchall(z.any()).describe("Color guide for the asset library defining roles and palettes").optional() }).catchall(z.any()).describe("A managed asset library (icon set, illustration system, image collection). The URL is for human access; agent-facing DAM integration is under investigation.")).describe("References to managed asset libraries (icon sets, illustration systems, image collections). URLs are intended for human access; agent-facing DAM integration is under investigation.").optional(), "restrictions": z.array(z.string()).describe("Visual prohibitions and guardrails (e.g., 'Never use black backgrounds', 'Do not crop the logo', 'No stock photography of people on phones')").optional() }).catchall(z.any()).describe("Structured visual rules for generative creative systems").optional(), "agents": z.array(z.object({ "type": z.enum(["brand","rights","measurement","governance","creative","sales","buying","signals"]).describe("Functional role of this agent"), "url": z.string().url().regex(new RegExp("^https://")).describe("Agent endpoint URL (MCP or A2A). Callers comparing a brand's declared agent URL against another value (e.g., resolving 'is this the agent that signed this artifact?' or matching against a discovery cache) MUST canonicalize both sides per the AdCP URL canonicalization rules, not byte-equality. See docs/reference/url-canonicalization."), "id": z.string().regex(new RegExp("^[a-z0-9_]+$")).max(100).describe("Agent identifier (useful for logging, multi-tenant platforms)"), "description": z.string().max(500).describe("Human-readable description of this agent's capabilities or scope").optional(), "jwks_uri": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL of the agent's JWKS (RFC 7517) containing public keys used to verify artifacts this agent signs or requests it sends. Verified artifacts include signed governance_context tokens (for governance agents) and RFC 9421 HTTP Signatures on outgoing requests (for any agent). When absent, verifiers MUST default to /.well-known/jwks.json on the origin of `url`. Keys are identified by `kid` in the JWS header or RFC 9421 `keyid` parameter; JWKS MAY contain multiple keys to support rotation and per-purpose separation via `key_ops` and `use`.").optional(), "available_uses": z.array(z.enum(["likeness","voice","name","endorsement","motion_capture","signature","catchphrase","sync","background_music","editorial","commercial","ai_generated_image"]).describe("Types of rights usage that can be licensed through the brand protocol. Aligned with DDEX UseType direction for interoperability with music and media rights systems.")).describe("For rights agents: rights uses available for licensing").optional(), "right_types": z.array(z.enum(["talent","character","brand_ip","music","stock_media"]).describe("Categories of intellectual property rights that can be licensed through the brand protocol.")).describe("For rights agents: types of rights available").optional(), "countries": z.array(z.string().regex(new RegExp("^[A-Z]{2}$"))).describe("ISO 3166-1 alpha-2 country codes where this agent operates. Omit for global scope.").optional() }).catchall(z.any()).describe("An agent declared by a brand or house. Each entry identifies one agent endpoint and its functional role in the advertising ecosystem.")).describe("Agents authorized to act on behalf of this brand. Consumers resolving an agent by URL use the matching brand-level entry; do not infer a type-wide override of unrelated house-level entries when multiple same-type entries exist.").optional(), "brand_agent": z.object({ "url": z.string().url().regex(new RegExp("^https://")).describe("Brand agent MCP endpoint URL. Callers comparing this URL against another value (e.g., resolving 'is this the brand's declared agent?' against a discovery cache) MUST canonicalize both sides per the AdCP URL canonicalization rules, not byte-equality. See docs/reference/url-canonicalization."), "id": z.string().regex(new RegExp("^[a-z0-9_]+$")).describe("Agent identifier (useful for logging, multi-tenant DAMs)") }).catchall(z.any()).describe("Deprecated: use agents array with type 'brand' instead. Brand agent that provides dynamic brand data via MCP.").optional(), "rights_agent": z.object({ "url": z.string().url().regex(new RegExp("^https://")).describe("Rights agent MCP endpoint URL. Callers comparing this URL against another value (e.g., matching against a brand's declared rights endpoint) MUST canonicalize both sides per the AdCP URL canonicalization rules, not byte-equality. See docs/reference/url-canonicalization."), "id": z.string().regex(new RegExp("^[a-z0-9_]+$")).describe("Agent identifier"), "available_uses": z.array(z.enum(["likeness","voice","name","endorsement","motion_capture","signature","catchphrase","sync","background_music","editorial","commercial","ai_generated_image"]).describe("Types of rights usage that can be licensed through the brand protocol. Aligned with DDEX UseType direction for interoperability with music and media rights systems.")).describe("Rights uses available for licensing through this agent"), "right_types": z.array(z.enum(["talent","character","brand_ip","music","stock_media"]).describe("Categories of intellectual property rights that can be licensed through the brand protocol.")).describe("Types of rights available").optional(), "countries": z.array(z.string().regex(new RegExp("^[A-Z]{2}$"))).describe("Countries where rights are available (ISO 3166-1 alpha-2)").optional() }).catchall(z.any()).describe("Deprecated: use agents array with type 'rights' instead. Rights licensing agent for this brand.").optional(), "contact": z.object({ "email": z.string().email().describe("Contact email").optional(), "phone": z.string().describe("Contact phone number").optional() }).describe("Brand-level contact information").optional(), "collections": z.array(z.object({ "collection_id": z.string().describe("Collection identifier as used in the seller's get_products responses").optional(), "name": z.string().describe("Human-readable collection name"), "role": z.enum(["host","guest","creator","cast","narrator","producer","correspondent","commentator","analyst"]).describe("This person's role on the collection").optional(), "seller_agent_url": z.string().url().describe("URL of the sales agent that sells inventory for this collection. Buyer agents can query this agent for collection products.").optional() }).catchall(z.any())).describe("Collections this person or brand is associated with. Enables bidirectional linking: a collection's talent references brand.json via brand_url, and brand.json links back to collections.").optional() }).catchall(z.any()).describe("A brand within a house portfolio. Combines identity (who) with creative assets (how to represent). Referenced as domain + brand_id.")).describe("Inline brands owned by this house (parent-owned data). Use for sub-brands without their own canonical document — typically those without a dedicated domain or that the holdco wants to manage centrally. A brand_id MUST NOT appear in both brands[] and brand_refs[].").optional(), "brand_refs": z.array(z.object({ "domain": z.string().regex(new RegExp("^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$")).describe("Domain where the child's canonical brand.json lives"), "brand_id": z.string().regex(new RegExp("^[a-z0-9_]+$")).describe("Stable brand identifier within the house portfolio. Required so the cross-array uniqueness invariant (brand_id MUST NOT appear in both brands[] and brand_refs[]) is enforceable."), "managed_by": z.string().regex(new RegExp("^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$")).describe("Optional domain of the entity that operationally manages this brand (e.g., an agency network within a holdco). House-declared. Consumers MUST NOT use it for trust or authorization decisions. Aggregation across houses ('show me everything BBH manages') is the intended use; trust is unaffected.").optional(), "effective_at": z.string().datetime().describe("ISO 8601 timestamp when the house established this ownership claim. Consumers age mutual-assertion edges from this date for TTL purposes and MUST NOT extend relationship trust before a future effective_at is reached. Optional; absent means the consumer ages from its own durable first observation.").optional() }).strict().describe("A house's ownership entry for a brand that publishes its own canonical brand.json elsewhere. The publisher (the house) asserts 'I own this brand, hosted at this domain, effective on this date.' Mutual-assertion trust requires the child's house_domain to reciprocate. Distinct from core/brand-ref.json (which identifies brands in media-buy plans). See docs/brand-protocol/brand-json.mdx")).describe("Portfolio entries for brands owned by this house that publish their own canonical brand.json elsewhere (child-owned data). Each entry asserts ownership plus where the child's document lives. Mutual-assertion trust: the pointed-to document's house_domain must equal this house's domain. Invariants: a brand_id MUST NOT appear in both brands[] and brand_refs[]; brand_id and domain MUST each be unique within brand_refs[]. See docs/brand-protocol/brand-json.mdx").optional(), "contact": z.object({ "name": z.string().min(1).max(255), "email": z.string().email().max(255).optional(), "domain": z.string().regex(new RegExp("^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$")).describe("A valid domain name").optional() }).catchall(z.any()).describe("Contact information").optional(), "authorized_operators": z.array(z.object({ "domain": z.string().regex(new RegExp("^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$")).describe("Domain of the authorized operator (e.g., 'groupm.com')"), "brands": z.array(z.string().regex(new RegExp("^([a-z0-9_]+|\\*)$"))).describe("Brand IDs this operator is authorized for. Use ['*'] for all brands in the portfolio."), "countries": z.array(z.string().regex(new RegExp("^[A-Z]{2}$"))).describe("ISO 3166-1 alpha-2 country codes where this authorization applies. Omit for global authorization.").optional(), "scopes": z.array(z.enum(["all","media_buying","creative_generation","rights_clearance","governance","measurement","agent_operations"])).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Activities this operator is authorized to perform for the listed brands and countries. Omit for backwards-compatible broad authorization. Use ['all'] only when every listed scope is delegated.").optional(), "valid_from": z.string().datetime().describe("ISO 8601 timestamp when this operator authorization starts. Omit when authorization is already active or the start date is not tracked.").optional(), "valid_until": z.string().datetime().describe("ISO 8601 timestamp when this operator authorization expires. Consumers MUST treat entries at or after this timestamp as inactive for authorization decisions.").optional() }).catchall(z.any()).describe("An entity authorized to represent brands from this house. Verified by resolving the operator's domain. Optional validity fields let houses time-box agency-of-record and delegated-operator relationships without changing historical entries.")).describe("Entities authorized to represent brands from this house. Third parties (sellers, platforms) can verify an operator's authorization by checking this list. Operators are identified by domain.").optional(), "trademarks": z.array(z.object({ "registry": z.string().describe("Trademark registry (e.g., 'USPTO', 'EUIPO', 'JPO', 'CNIPA')"), "number": z.string().describe("Registration number as issued by the registry"), "mark": z.string().describe("The registered mark as published"), "status": z.enum(["active","pending","abandoned","cancelled","expired"]).describe("Registration status. Omit for active marks if status tracking is not maintained.").optional(), "license_type": z.enum(["owned","licensed_in","licensed_out"]).describe("Whether the publisher owns the mark, licenses it from another entity, or licenses it to others. 'owned' is the default if omitted.").optional(), "licensor_domain": z.string().regex(new RegExp("^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$")).describe("Domain of the entity that licenses this mark to the publisher. Meaningful when license_type=licensed_in; omit otherwise.").optional(), "countries": z.array(z.string().min(2).max(2)).describe("ISO 3166-1 alpha-2 country codes where this registration applies. Omit for global or where the registry's jurisdiction is implicit.").optional(), "nice_classes": z.array(z.number().int().gte(1).lte(45)).describe("Nice Classification class numbers (1-45) covered by this registration. Disambiguates marks across industries (e.g., Delta-airline vs Delta-faucet). Omit if scope is implicit from registry.").optional() }).catchall(z.any()).describe("A registered trademark. May appear at house level (corporate marks, e.g., 'NIKE' owned by Nike, Inc.) or at brand level (brand-specific marks, e.g., 'CONVERSE' owned by Converse). Resolution between house- and brand-level trademarks is union — both lists are valid claims about marks the publisher controls.")).describe("House-level (corporate) registered trademarks. Brand-level marks live on individual brand entries; resolution is union.").optional(), "last_updated": z.string().datetime().optional() }).strict().and(z.union([z.any(), z.any()])).describe("Full house/brand portfolio with hierarchy, creative assets, and properties. May carry inline brands (parent-owned, brands[]) and/or pointer brands (child-owned canonical documents, brand_refs[]). At least one of brands[] or brand_refs[] is required. A brand_id MUST NOT appear in both. See docs/brand-protocol/brand-json.mdx"), z.record(z.string(), z.any()).and(z.intersection(z.object({ "$schema": z.string().optional(), "version": z.string().optional(), "default_language": z.string().regex(new RegExp("^(?:[a-z]{2,8}(?:-[A-Z][a-z]{3})?(?:-(?:[A-Z]{2}|[0-9]{3}))?(?:-(?:[a-z0-9]{5,8}|[0-9][a-z0-9]{3}))*(?:-[0-9a-wy-z](?:-[a-z0-9]{2,8})+)*(?:-x(?:-[a-z0-9]{1,8})+)?|x(?:-[a-z0-9]{1,8})+)$")).min(2).max(63).describe("A well-formed BCP 47 language tag used by AdCP only as language identity. Script and region may refine that identity; other valid BCP 47 subtags remain part of tag matching but do not make this a general locale-settings object. It does not determine currency, time zone, number/date formatting, market, or legal jurisdiction. The AdCP canonical wire profile requires lower-case language and variants, title-case script, and upper-case region (for example `en-US`, `zh-Hant-TW`, or `x-private`). RFC 5646 comparisons are case-insensitive and its case regularization is optional; AdCP intentionally requires this stricter single wire spelling and receivers MUST reject differently cased tags rather than silently normalizing them. The schema pattern enforces the AdCP casing profile and extension structure for commonly used tags; conforming receivers additionally validate the complete RFC 5646 grammar and registry rules. Every new AdCP field carrying BCP 47 language identity or a concrete language range MUST reference this schema instead of declaring independent string constraints.").describe("BCP 47 language tag for legacy plain-string and plain-array content in this document. Readers assume en when omitted.").default("en"), "house_domain": z.string().regex(new RegExp("^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$")).describe("Optional pointer to the corporate house this brand belongs to. The named house's brand_refs[] MUST reciprocate for mutual-assertion trust. Single-hop only — a brand cannot itself declare brand_refs[]. Omit for standalone brands (no house).").optional(), "last_updated": z.string().datetime().optional() }), z.object({ "id": z.string().regex(new RegExp("^[a-z0-9_]+$")).describe("Brand identifier within the house. House chooses this ID."), "url": z.string().url().describe("Primary brand URL for context and asset discovery").optional(), "identity_relying_parties": z.array(z.object({ "issuer": z.object({ "domain": z.string().regex(new RegExp("^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$")).describe("Domain where /.well-known/brand.json is hosted, or the brand's operating domain"), "brand_id": z.string().regex(new RegExp("^[a-z0-9_]+$")).describe("Brand identifier within the house portfolio. Optional for single-brand domains.").optional(), "countries": z.array(z.string().regex(new RegExp("^[A-Z]{2}$"))).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Canonical set of ISO 3166-1 alpha-2 countries for this advertiser identity. Omit for a global/default identity. Array order is not meaningful; producers MUST sort codes lexicographically before computing keys or signatures. This qualifies account identity and is not delivery targeting.").optional(), "industries": z.array(z.string()).describe("Inline override for the brand's industries. Useful when the caller cannot modify the brand's canonical brand.json but needs to declare industries for governance (e.g., Annex III vertical detection). brand.json remains the canonical source; when omitted here, governance agents SHOULD resolve from brand.json.").optional(), "data_subject_contestation": z.object({ "url": z.string().url().regex(new RegExp("^https://")).optional(), "email": z.string().email().optional(), "languages": z.array(z.string()).optional() }).strict().and(z.union([z.any(), z.any()])).describe("Inline override for the brand's contestation contact point. Useful when the operator does not control brand.json but needs to discharge Art 22(3) for this plan. brand.json is canonical; when omitted, governance agents resolve brand → house → missing.").optional(), "brand_kit_override": z.object({ "logo": z.object({ "asset_type": z.literal("image").describe("Discriminator identifying this as an image asset. See /schemas/creative/asset-types for the registry."), "url": z.string().url().describe("URL to the image asset"), "width": z.number().int().gte(1).describe("Width in pixels"), "height": z.number().int().gte(1).describe("Height in pixels"), "file_size_bytes": z.number().int().gte(1).describe("Image file size in bytes. Required by agents that advertise a max_file_size_kb constraint.").optional(), "pixel_ratio": z.number().gt(0).describe("Intrinsic pixels per logical render pixel (for example `2` for a 600×500 image intended to render at 300×250). Optional because a validator can infer the ratio when the target format declares logical dimensions. When supplied, it MUST agree with both `width / logical_width` and `height / logical_height`; it is never a substitute for the intrinsic `width` and `height` fields.").optional(), "state_id": z.string().describe("Binding used only when this image populates a `seller_rendered_stateful_display` `state_canvases` slot. It MUST match one declared `states[].state_id` (semantic validators resolve it). Omit for ordinary image slots.").optional(), "breakpoint_id": z.string().describe("Binding used only when this image populates a `seller_rendered_stateful_display` `state_canvases` slot. It MUST match one breakpoint declared on the selected state (semantic validators resolve it). Omit for ordinary image slots.").optional(), "focal_point": z.array(z.number().gte(0).lte(1)).max(2).describe("Normalized `[x, y]` coordinates (0–1 from top-left) of the image's visual anchor. Seller-side renderers crop toward the focal point when deriving renditions across breakpoints and aspect ratios; absent, cropping falls back to center-weighted defaults.").optional(), "format": z.string().describe("Image file format (jpg, png, gif, webp, etc.)").optional(), "alt_text": z.string().describe("Alternative text for accessibility").optional(), "provenance": z.object({ "digital_source_type": z.enum(["digital_capture","digital_creation","trained_algorithmic_media","composite_with_trained_algorithmic_media","algorithmic_media","composite_capture","composite_synthetic","human_edits","data_driven_media"]).describe("IPTC-aligned classification of AI involvement in producing this content").optional(), "synthetic_depiction": z.boolean().describe("Assessed declaration of whether the content synthetically depicts a real or fictional person performing or appearing in a way that was generated or materially manipulated rather than captured as depicted. `true` covers both a fully synthetic performer and material manipulation of a real performer; `false` is an assessed declaration that the content does not contain such a depiction. Absence means the content has not been assessed for synthetic depiction. This field does not claim consent, legality, or independent verification, and receivers MUST NOT derive it solely from `digital_source_type`.").optional(), "ai_tool": z.object({ "name": z.string().describe("Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')"), "version": z.string().describe("Version identifier for the AI tool or model (e.g., '25.1', '0125', '2.1'). For generative models, use the model version rather than the API version.").optional(), "provider": z.string().describe("Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')").optional() }).describe("AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.").optional(), "human_oversight": z.enum(["none","prompt_only","selected","edited","directed"]).describe("Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` — the protocol does not derive disclosure obligations from oversight level. Some regulations include carve-outs for human-edited or human-directed AI output, but those carve-outs have factual prerequisites the schema cannot evaluate. Asserting `edited` or `directed` does not by itself justify `disclosure.required: false`.").optional(), "declared_by": z.object({ "agent_url": z.string().url().describe("URL of the agent or service that declared this provenance").optional(), "role": z.enum(["creator","advertiser","agency","platform","tool"]).describe("Role of the declaring party in the supply chain") }).describe("Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.").optional(), "declared_at": z.string().datetime().describe("When this provenance claim was made (ISO 8601). Distinct from created_time, which records when the content itself was produced. A provenance claim may be attached well after content creation, for example when retroactively declaring AI involvement for regulatory compliance.").optional(), "created_time": z.string().datetime().describe("When this content was created or generated (ISO 8601)").optional(), "c2pa": z.object({ "manifest_url": z.string().url().describe("URL to the C2PA manifest store for this content") }).describe("C2PA sidecar manifest reference. Links to a detached cryptographic provenance manifest for this content. Note: file-level C2PA bindings break when ad servers transcode, resize, or re-encode assets. For pipelines with intermediaries, consider embedded_provenance as the primary provenance mechanism.").optional(), "embedded_provenance": z.array(z.object({ "method": z.enum(["manifest_wrapper","provenance_markers"]).describe("How provenance data is carried within the content"), "standard": z.string().describe("Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)").optional(), "provider": z.string().describe("Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context — not a wire identifier."), "verify_agent": z.object({ "agent_url": z.string().url().regex(new RegExp("^https://")).describe("URL of the governance agent the buyer represents was used to embed/verify this layer. MUST use the `https://` scheme and MUST appear in the seller's `creative_policy.accepted_verifiers[].agent_url` list (canonicalized per /docs/reference/url-canonicalization: lowercase scheme and host, strip default port, normalize path dot-segments). Sellers MUST NOT call this URL until the canonicalized match is confirmed."), "feature_id": z.string().describe("Optional `feature_id` the buyer represents the seller should request via `get_creative_features` (e.g., `encypher.markers_present_v2`). SHOULD match the `feature_id` declared on the matching `accepted_verifiers[]` entry, or be omitted to defer the selector to the seller. When the seller's entry pins a `feature_id`, that value wins; when neither side pins, the seller selects from the agent's `governance.creative_features` catalog.").optional() }).strict().describe("Buyer's representation that this embedding can be verified by a governance agent on the seller's `creative_policy.accepted_verifiers` list. The `agent_url` MUST match (canonicalized) one of the seller's published `accepted_verifiers[].agent_url` entries; sellers reject `sync_creatives` submissions whose `verify_agent.agent_url` is off-list with `PROVENANCE_VERIFIER_NOT_ACCEPTED`. This is buyer-supplied evidence, not buyer-driven routing — the seller is the verifier-of-record and the seller controls which agent it actually calls (the seller MAY use a different on-list agent if it determines this is more appropriate; the seller does not call buyer-asserted endpoints outside its allowlist). MAY be omitted for self-verifiable embeddings (e.g., a C2PA text manifest with a public key the seller already trusts).").optional(), "embedded_at": z.string().datetime().describe("When the provenance data was embedded (ISO 8601)").optional() })).describe("Provenance metadata embedded within the content stream. Each entry declares one embedding layer: structured provenance data carried inside the content itself, as distinct from sidecar references (c2pa.manifest_url). Embedded provenance survives operations that break sidecar and file-level bindings: ad-server transcoding, CMS ingestion, copy-paste, reformatting, and CDN re-encoding. For ad-tech pipelines where content passes through multiple intermediaries, embedded provenance is the reliable path for provenance that persists from declaration through delivery. This is a declaration by the embedding party. The receiving party (the seller) is the verifier-of-record: it confirms the claim by calling a governance agent it trusts (typically one published in `creative_policy.accepted_verifiers`).").optional(), "watermarks": z.array(z.object({ "media_type": z.enum(["audio","image","video","text"]).describe("Media category of the watermarked content"), "provider": z.string().describe("Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context — not a wire identifier."), "verify_agent": z.object({ "agent_url": z.string().url().regex(new RegExp("^https://")).describe("URL of the governance agent the buyer represents was used to apply/detect this watermark. MUST use the `https://` scheme and MUST appear in the seller's `creative_policy.accepted_verifiers[].agent_url` list (canonicalized per /docs/reference/url-canonicalization: lowercase scheme and host, strip default port, normalize path dot-segments). Sellers MUST NOT call this URL until the canonicalized match is confirmed."), "feature_id": z.string().describe("Optional `feature_id` the buyer represents the seller should request via `get_creative_features` (e.g., `imatag.watermark_detected`). SHOULD match the `feature_id` declared on the matching `accepted_verifiers[]` entry, or be omitted to defer the selector to the seller. When the seller's entry pins a `feature_id`, that value wins; when neither side pins, the seller selects from the agent's `governance.creative_features` catalog.").optional() }).strict().describe("Buyer's representation that this watermark can be detected by a governance agent on the seller's `creative_policy.accepted_verifiers` list. The `agent_url` MUST match (canonicalized) one of the seller's published `accepted_verifiers[].agent_url` entries; sellers reject `sync_creatives` submissions whose `verify_agent.agent_url` is off-list with `PROVENANCE_VERIFIER_NOT_ACCEPTED`. This is buyer-supplied evidence, not buyer-driven routing — the seller is the verifier-of-record and the seller controls which agent it actually calls (the seller MAY use a different on-list agent if it determines this is more appropriate; the seller does not call buyer-asserted endpoints outside its allowlist).").optional(), "c2pa_action": z.enum(["c2pa.watermarked.bound","c2pa.watermarked.unbound"]).describe("C2PA action classification for this watermark").optional(), "embedded_at": z.string().datetime().describe("When the watermark was applied (ISO 8601)").optional() })).describe("Content watermarks applied to this asset. Each entry declares one watermarking layer: a content modification that encodes an identifier or fingerprint within the asset. Watermarks differ from embedded provenance: a watermark encodes an identifier (who generated it, who owns it), while embedded provenance carries or references a structured provenance record (the full chain of custody). A single asset may carry both. Aligns with C2PA action taxonomy: c2pa.watermarked.bound (watermark linked to a C2PA manifest) and c2pa.watermarked.unbound (watermark independent of any manifest). This is a declaration by the watermarking party. The receiving party (the seller) is the verifier-of-record: it confirms the claim by calling a governance agent it trusts (typically one published in `creative_policy.accepted_verifiers`).").optional(), "disclosure": z.object({ "required": z.boolean().describe("The declaring party's claim that AI disclosure is required for this content under applicable regulations. This is a declared signal carried through the supply chain — useful as a routing and audit input — not a regulatory determination made by the protocol. Receiving parties remain responsible for their own jurisdictional analysis and should not treat `required: false` as compliance cover."), "jurisdictions": z.array(z.object({ "country": z.string().describe("ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')"), "region": z.string().describe("Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)").optional(), "regulation": z.string().describe("Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')"), "label_text": z.string().describe("Required disclosure label text for this jurisdiction, in the local language").optional(), "render_guidance": z.object({ "persistence": z.enum(["continuous","initial","flexible"]).describe("How long the disclosure must persist during content playback or display").optional(), "min_duration_ms": z.number().int().gte(1).describe("Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial — without it, the duration is at the publisher's discretion. At serve time the publisher reads this from provenance since the brief is not available.").optional(), "positions": z.array(z.enum(["prominent","footer","audio","subtitle","overlay","end_card","pre_roll","companion"]).describe("Where a required disclosure should appear within a creative. Used by creative briefs to specify disclosure placement and by formats to declare which positions they can render.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Preferred disclosure positions in priority order. The first position a format supports should be used.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension object for platform-specific, vendor-namespaced parameters. Extensions are always optional and must be namespaced under a vendor/platform key (e.g., ext.gam, ext.roku). Used for custom capabilities, partner-specific configuration, and features being proposed for standardization.").optional() }).describe("How the disclosure should be rendered for this jurisdiction. Expresses the declaring party's intent for persistence and position based on regulatory requirements. Publishers control actual rendering but governance agents can audit whether guidance was followed.").optional() })).describe("Jurisdictions where disclosure obligations apply").optional() }).describe("Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.").optional(), "verification": z.array(z.object({ "verified_by": z.string().describe("Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')"), "verified_time": z.string().datetime().describe("When the verification was performed (ISO 8601)").optional(), "result": z.enum(["authentic","ai_generated","ai_modified","inconclusive"]).describe("Verification outcome"), "confidence": z.number().gte(0).lte(1).describe("Confidence score of the verification result (0.0 to 1.0)").optional(), "details_url": z.string().url().describe("URL to the full verification report").optional() })).describe("Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim — verification results attached by the declaring party are supplementary. The enforcing party (e.g., seller/publisher) should run its own verification via get_creative_features or calibrate_content.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension object for platform-specific, vendor-namespaced parameters. Extensions are always optional and must be namespaced under a vendor/platform key (e.g., ext.gam, ext.roku). Used for custom capabilities, partner-specific configuration, and features being proposed for standardization.").optional() }).describe("Provenance metadata for this asset, overrides manifest-level provenance").optional() }).catchall(z.any()).describe("Override logo asset.").optional(), "colors": z.object({ "primary": z.string().regex(new RegExp("^#[0-9a-fA-F]{6}$")).optional(), "secondary": z.string().regex(new RegExp("^#[0-9a-fA-F]{6}$")).optional(), "accent": z.string().regex(new RegExp("^#[0-9a-fA-F]{6}$")).optional() }).catchall(z.any()).describe("Override brand colors (hex strings).").optional(), "voice": z.string().describe("Override brand-voice description for surface-composed text/audio output.").optional(), "tagline": z.string().describe("Override tagline.").optional() }).catchall(z.any()).describe("Inline override for brand-kit fields normally resolved from `/.well-known/brand.json` on `domain` (logo, colors, voice, tagline). Use when brand.json is missing, stale, or inappropriate for this specific call — e.g., a campaign-scoped tagline, a co-branded creative, a freshly-rebranded color palette the brand.json hasn't shipped yet. Same inline-override pattern as `industries` and `data_subject_contestation` above: brand.json is canonical, the override is per-call. Adopters needing to override fields outside this subset (`voice_attributes`, `prohibited_terms`, etc.) MUST publish a different brand.json and reference it via a different `domain` — the inline override is intentionally narrow to a small high-traffic subset.\n\n**Merge semantics (normative).** The merge is **field-level**, not whole-object replacement. Each field within `brand_kit_override` (`logo`, `colors`, `voice`, `tagline`) is evaluated independently — when a field is present on the override the override value applies; when a field is absent the brand.json value applies (or is absent if brand.json doesn't carry one either). For composite fields (`colors.primary`, `colors.secondary`, `colors.accent`), the merge is one level deeper: each color slot is evaluated independently — a producer can override `colors.primary` while still inheriting `colors.secondary` from brand.json. SDKs MUST NOT treat a present `brand_kit_override.colors` as wiping the brand.json `colors` block entirely; only the per-slot fields present in the override take precedence. Without this rule, a partial-override semantics would diverge across SDKs and produce inconsistent rendering for the same payload.").optional() }).strict().describe("Identity issuer / attestation authority, referenced as a vendor BrandRef (e.g. {\"domain\": \"world.org\"}) — the same vendor-reference shape AdCP uses for measurement and signals vendors. The issuer's canonical domain is the anchor; it need not host a brand.json, but if it does, that is where its verifier metadata (scheme versions, verify endpoint, JWKs) lives. Issuer-agnostic: World ID, ISO 18013-5 mDL, and W3C-VC issuers all reference by domain. The relying party is namespaced by the issuer — identity is the tuple (issuer.domain, issuer.brand_id, relying_party_id), mirroring (vendor.domain, vendor.brand_id, metric_id)."), "scheme": z.string().describe("Proof scheme and version, e.g. \"world_id_v4\".").optional(), "relying_party_id": z.string().describe("The relying-party id registered with the issuer (and, for on-chain issuers like World ID, with the issuer's registry). One entity may operate many relying parties (scope=entity vs scope=property); this is the attestation's audience / linkability boundary, not an entity identifier."), "scope": z.enum(["entity","property"]).describe("Whether this relying_party_id is shared across the entity's properties (entity → a within-entity unique-human graph) or scoped to a single property (property → per-property pseudonyms, unlinkable across the entity).").optional() }).strict().describe("A verified-identity relying party an entity or brand operates. Used for attestation provenance in TMP Identity Match (the buyer checks a forwarded attestation's relying_party_id against the owner's published list). Issuer-agnostic; World ID is the first issuer.")).describe("Verified-identity relying parties scoped to this brand/property, for attestation provenance in TMP Identity Match. Use when a brand or property runs its own relying_party_id (per-property pseudonyms); entity-wide relying parties live on the house object. See specs/tmp-verified-identity-attestation.md.").optional(), "names": z.array(z.record(z.string(), z.string().min(1)).describe("A localized name using the legacy underscore-separated language/region key convention (for example 'en_US', 'fr_CA', or 'zh_CN') and a name value. Bare language codes such as 'en' are also accepted for backwards compatibility. These keys are not AdCP locale-tag values: a bridge to /schemas/core/locale-tag.json replaces '_' with '-', applies the AdCP canonical wire profile, and validates the result rather than passing the key through unchanged.")).describe("Localized brand names. Multiple entries per language allowed for aliases."), "keller_type": z.enum(["master","sub_brand","endorsed","independent"]).describe("Brand architecture type from Keller's theory. master: primary brand of house. sub_brand: carries parent name (Nike SB). endorsed: independent identity backed by parent (Air Jordan 'by Nike'). independent: operates separately (Converse under Nike, Inc.)").optional(), "parent_brand": z.string().regex(new RegExp("^[a-z0-9_]+$")).describe("Parent brand ID for sub-brands and endorsed brands").optional(), "description": z.string().describe("Brand description").optional(), "industries": z.array(z.string()).describe("Brand industries (e.g., ['automotive'] or ['pharmaceutical', 'cpg'] for a consumer health company). Describes what the company does — not what regulatory regimes apply (use policy_categories for that).").optional(), "target_audience": z.string().describe("Primary target audience").optional(), "logos": z.array(z.object({ "id": z.string().regex(new RegExp("^[a-z0-9][a-z0-9_-]*$")).describe("Stable identifier for this logo entry. Recommended when logo usage rules or mark lockups need to bind to a specific logo asset.").optional(), "url": z.string().url().describe("URL to the logo asset"), "orientation": z.enum(["square","horizontal","vertical","stacked"]).describe("Logo aspect ratio orientation. square: ~1:1, horizontal: wide, vertical: tall, stacked: vertically arranged elements").optional(), "background": z.enum(["dark-bg","light-bg","transparent-bg"]).describe("Background compatibility. dark-bg: use on dark backgrounds, light-bg: use on light backgrounds, transparent-bg: has transparent background").optional(), "variant": z.enum(["primary","secondary","icon","wordmark","full-lockup"]).describe("Logo variant type. primary: main logo, secondary: alternative, icon: symbol only, wordmark: text only, full-lockup: complete logo").optional(), "tags": z.array(z.string()).describe("Additional semantic tags for custom categorization beyond the standard orientation, background, and variant fields").optional(), "slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Canonical renderer slots where this logo is appropriate. Consumers SHOULD prefer this over inferring from tags or usage prose when selecting a logo for a specific UI surface.").optional(), "usage": z.string().describe("Human-readable description of when to use this logo variant (e.g., 'Primary logo for use on light backgrounds')").optional(), "width": z.number().int().describe("Width in pixels").optional(), "height": z.number().int().describe("Height in pixels").optional() }).catchall(z.any()).describe("Brand logo asset with structured fields for orientation, background compatibility, and variant type")).describe("Brand logo assets").optional(), "colors": z.object({ "primary": z.union([z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")), z.array(z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")))]).optional(), "secondary": z.union([z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")), z.array(z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")))]).optional(), "accent": z.union([z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")), z.array(z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")))]).optional(), "background": z.union([z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")), z.array(z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")))]).optional(), "text": z.union([z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")), z.array(z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")))]).optional(), "heading": z.union([z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")), z.array(z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")))]).optional(), "body": z.union([z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")), z.array(z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")))]).optional(), "label": z.union([z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")), z.array(z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")))]).optional(), "border": z.union([z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")), z.array(z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")))]).optional(), "divider": z.union([z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")), z.array(z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")))]).optional(), "surface_1": z.union([z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")), z.array(z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")))]).optional(), "surface_2": z.union([z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")), z.array(z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")))]).optional() }).catchall(z.union([z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")), z.array(z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")))])).describe("Brand color palette. Each role accepts a single hex color or an array of hex colors for brands with multiple values per role. Beyond the core five roles, brands can provide additional color roles for finer granularity — heading, body, label, border, divider, surface_1, surface_2, etc.").optional(), "fonts": z.object({ "primary": z.union([z.string().describe("CSS font-family name (e.g., 'Montserrat', 'Arial, sans-serif')"), z.object({ "family": z.string().describe("CSS font-family name (e.g., 'Brand Sans')"), "files": z.array(z.object({ "url": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL to the font file (WOFF2, TTF, or OTF)"), "weight": z.number().int().gte(100).lte(900).describe("CSS numeric font-weight for static fonts (100-900)").optional(), "weight_range": z.array(z.number().int().gte(100).lte(900)).max(2).describe("Variable font weight axis range as [min, max] (e.g., [100, 900]). Use instead of weight for variable fonts.").optional(), "style": z.enum(["normal","italic","oblique"]).describe("CSS font-style").optional() }).catchall(z.any()).describe("A font file with weight, style, and variable font metadata")).max(36).describe("Font files for different weights and styles").optional(), "opentype_features": z.array(z.string().regex(new RegExp("^[a-z0-9]{4}$"))).max(20).describe("OpenType feature tags to enable (e.g., ['ss01', 'tnum', 'cv01']). These are four-character tags per the OpenType spec.").optional(), "fallbacks": z.array(z.string().max(100)).max(10).describe("Ordered fallback font-family names for when the primary font is unavailable or does not support the required script (e.g., ['Noto Sans Arabic', 'Noto Sans SC', 'sans-serif'])").optional() }).catchall(z.any()).describe("Structured font with family name, downloadable font files, and typographic metadata")]).describe("Primary font family").optional(), "secondary": z.union([z.string().describe("CSS font-family name (e.g., 'Montserrat', 'Arial, sans-serif')"), z.object({ "family": z.string().describe("CSS font-family name (e.g., 'Brand Sans')"), "files": z.array(z.object({ "url": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL to the font file (WOFF2, TTF, or OTF)"), "weight": z.number().int().gte(100).lte(900).describe("CSS numeric font-weight for static fonts (100-900)").optional(), "weight_range": z.array(z.number().int().gte(100).lte(900)).max(2).describe("Variable font weight axis range as [min, max] (e.g., [100, 900]). Use instead of weight for variable fonts.").optional(), "style": z.enum(["normal","italic","oblique"]).describe("CSS font-style").optional() }).catchall(z.any()).describe("A font file with weight, style, and variable font metadata")).max(36).describe("Font files for different weights and styles").optional(), "opentype_features": z.array(z.string().regex(new RegExp("^[a-z0-9]{4}$"))).max(20).describe("OpenType feature tags to enable (e.g., ['ss01', 'tnum', 'cv01']). These are four-character tags per the OpenType spec.").optional(), "fallbacks": z.array(z.string().max(100)).max(10).describe("Ordered fallback font-family names for when the primary font is unavailable or does not support the required script (e.g., ['Noto Sans Arabic', 'Noto Sans SC', 'sans-serif'])").optional() }).catchall(z.any()).describe("Structured font with family name, downloadable font files, and typographic metadata")]).describe("Secondary font family").optional() }).catchall(z.union([z.string().describe("CSS font-family name (e.g., 'Montserrat', 'Arial, sans-serif')"), z.object({ "family": z.string().describe("CSS font-family name (e.g., 'Brand Sans')"), "files": z.array(z.object({ "url": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL to the font file (WOFF2, TTF, or OTF)"), "weight": z.number().int().gte(100).lte(900).describe("CSS numeric font-weight for static fonts (100-900)").optional(), "weight_range": z.array(z.number().int().gte(100).lte(900)).max(2).describe("Variable font weight axis range as [min, max] (e.g., [100, 900]). Use instead of weight for variable fonts.").optional(), "style": z.enum(["normal","italic","oblique"]).describe("CSS font-style").optional() }).catchall(z.any()).describe("A font file with weight, style, and variable font metadata")).max(36).describe("Font files for different weights and styles").optional(), "opentype_features": z.array(z.string().regex(new RegExp("^[a-z0-9]{4}$"))).max(20).describe("OpenType feature tags to enable (e.g., ['ss01', 'tnum', 'cv01']). These are four-character tags per the OpenType spec.").optional(), "fallbacks": z.array(z.string().max(100)).max(10).describe("Ordered fallback font-family names for when the primary font is unavailable or does not support the required script (e.g., ['Noto Sans Arabic', 'Noto Sans SC', 'sans-serif'])").optional() }).catchall(z.any()).describe("Structured font with family name, downloadable font files, and typographic metadata")]).describe("A font role entry. Either a CSS font-family string (simple) or a structured object with family name and font files (rich).")).describe("Brand typography. Each key is a role name (e.g., 'primary', 'secondary') referenced by type_scale entries. Values are either a CSS font-family string or a structured object with font files for reliable resolution.").optional(), "tone": z.union([z.string().describe("Simple tone descriptors for backwards compatibility"), z.object({ "voice": z.union([z.string(), z.array(z.record(z.string(), z.string().min(1)).describe("A localized name using the legacy underscore-separated language/region key convention (for example 'en_US', 'fr_CA', or 'zh_CN') and a name value. Bare language codes such as 'en' are also accepted for backwards compatibility. These keys are not AdCP locale-tag values: a bridge to /schemas/core/locale-tag.json replaces '_' with '-', applies the AdCP canonical wire profile, and validates the result rather than passing the key through unchanged."))]).describe("High-level voice descriptor, either a legacy plain string or localized values.").optional(), "attributes": z.union([z.array(z.string()), z.array(z.record(z.string(), z.array(z.string())).describe("One locale mapped to a complete ordered string list. The list is selected as a unit so readers never assemble mixed-language output from independently localized elements."))]).describe("Personality traits that characterize the brand voice. Localized lists are selected as a whole.").optional(), "dos": z.union([z.array(z.string()), z.array(z.record(z.string(), z.array(z.string())).describe("One locale mapped to a complete ordered string list. The list is selected as a unit so readers never assemble mixed-language output from independently localized elements."))]).describe("Guidance for copy generation - what TO do. Localized lists are selected as a whole.").optional(), "donts": z.union([z.array(z.string()), z.array(z.record(z.string(), z.array(z.string())).describe("One locale mapped to a complete ordered string list. The list is selected as a unit so readers never assemble mixed-language output from independently localized elements."))]).describe("Guardrails to avoid brand violations - what NOT to do. Localized lists are selected as a whole.").optional() }).describe("Structured brand voice guidelines")]).describe("Brand voice and messaging tone guidelines").optional(), "tagline": z.union([z.string().describe("Plain tagline string for backwards compatibility"), z.array(z.record(z.string(), z.string().min(1)).describe("A localized name using the legacy underscore-separated language/region key convention (for example 'en_US', 'fr_CA', or 'zh_CN') and a name value. Bare language codes such as 'en' are also accepted for backwards compatibility. These keys are not AdCP locale-tag values: a bridge to /schemas/core/locale-tag.json replaces '_' with '-', applies the AdCP canonical wire profile, and validates the result rather than passing the key through unchanged.")).describe("Localized taglines with BCP 47 locale codes")]).describe("Brand tagline or slogan. Accepts a plain string or a localized array matching the names pattern.").optional(), "assets": z.array(z.object({ "asset_id": z.string().describe("Unique identifier"), "asset_type": z.enum(["image","video","audio","text","markdown","html","css","javascript","zip","vast","daast","url","webhook","brief","catalog","published_post"]).describe("Type of asset content"), "url": z.string().url().describe("URL to CDN-hosted asset file"), "tags": z.array(z.string()).describe("Tags for discovery (e.g., 'hero', 'lifestyle', 'product', 'holiday')").optional(), "name": z.union([z.string(), z.array(z.record(z.string(), z.string().min(1)).describe("A localized name using the legacy underscore-separated language/region key convention (for example 'en_US', 'fr_CA', or 'zh_CN') and a name value. Bare language codes such as 'en' are also accepted for backwards compatibility. These keys are not AdCP locale-tag values: a bridge to /schemas/core/locale-tag.json replaces '_' with '-', applies the AdCP canonical wire profile, and validates the result rather than passing the key through unchanged."))]).describe("Human-readable name, either a legacy plain string or localized values.").optional(), "description": z.union([z.string(), z.array(z.record(z.string(), z.string().min(1)).describe("A localized name using the legacy underscore-separated language/region key convention (for example 'en_US', 'fr_CA', or 'zh_CN') and a name value. Bare language codes such as 'en' are also accepted for backwards compatibility. These keys are not AdCP locale-tag values: a bridge to /schemas/core/locale-tag.json replaces '_' with '-', applies the AdCP canonical wire profile, and validates the result rather than passing the key through unchanged."))]).describe("Asset description or usage notes, either a legacy plain string or localized values.").optional(), "width": z.number().int().describe("Image/video width in pixels").optional(), "height": z.number().int().describe("Image/video height in pixels").optional(), "duration_seconds": z.number().describe("Video/audio duration in seconds").optional(), "file_size_bytes": z.number().int().describe("File size in bytes").optional(), "format": z.string().describe("File format (e.g., 'jpg', 'mp4', 'mp3')").optional(), "metadata": z.record(z.string(), z.any()).describe("Additional asset-specific metadata").optional() }).catchall(z.any()).describe("Brand asset (image, video, audio, text)")).describe("Brand asset library").optional(), "properties": z.array(z.object({ "type": z.enum(["website","mobile_app","ctv_app","desktop_app","dooh","podcast","radio","linear_tv","streaming_audio","ai_assistant"]).describe("Property type"), "identifier": z.string().min(1).describe("Property identifier - domain for websites, bundle ID for apps"), "store": z.enum(["apple","google","amazon","roku","samsung","lg","other"]).describe("App store for mobile/CTV apps").optional(), "region": z.string().regex(new RegExp("^([A-Z]{2}|global)$")).describe("ISO 3166-1 alpha-2 country code or 'global'").optional(), "primary": z.boolean().describe("Whether this is the primary property for the brand").default(false), "relationship": z.enum(["owned","direct","delegated","ad_network"]).describe("How this brand relates to the property. 'owned': the brand owns and operates this property (default) and has no adagents.json delegation_type counterpart. 'direct': the brand is the direct sales path for this property, even if a third party operates the software (e.g., a publisher's in-house ad team using a vendor's tech). 'delegated': the brand manages monetization for this property — they are in charge of ad sales (e.g., Mediavine managing a food blog). 'ad_network': the brand sells this property's inventory as part of a network or exchange — they are a path to the inventory, not the path (e.g., PubMatic as an SSP). For non-owned properties, the publisher confirms the relationship by setting the matching delegation_type on the agent's authorization in their adagents.json.").default("owned") }).catchall(z.any()).describe("A digital property associated with a brand. Defaults to owned; use 'relationship' to declare direct, delegated, or ad_network properties. For delegated and network paths, these values match the delegation_type field in adagents.json, creating a bilateral verification chain: the operator declares the relationship here, the publisher confirms by setting the same delegation_type on the agent's authorization in their adagents.json. 'owned' is an inline ownership declaration with no adagents.json counterpart.")).describe("Digital properties associated with this brand — owned, managed, or represented").optional(), "product_catalog": z.object({ "feed_url": z.string().url().describe("URL to product catalog feed"), "feed_format": z.enum(["google_merchant_center","facebook_catalog","shopify","linkedin_jobs","tiktok_shop","pinterest_catalog","openai_product_feed","custom"]).describe("Format of the product feed").optional(), "categories": z.array(z.string()).describe("Product categories available in the catalog").optional(), "last_updated": z.string().datetime().describe("When the product catalog was last updated").optional(), "update_frequency": z.enum(["realtime","hourly","daily","weekly"]).describe("How frequently the product catalog is updated").optional(), "agentic_checkout": z.object({ "endpoint": z.string().url().describe("Base URL for checkout session API"), "spec": z.string().describe("Checkout API specification identifier. Use a namespaced string to identify the checkout protocol (e.g., vendor-prefixed or custom). Vendor-specific values belong under ext.{vendor}."), "supported_payment_providers": z.array(z.string()).describe("Payment providers supported by this checkout endpoint").optional() }).describe("Agentic checkout endpoint configuration").optional() }).catchall(z.any()).describe("Product catalog for e-commerce brands").optional(), "privacy_policy_url": z.string().url().describe("URL to the brand's privacy policy").optional(), "data_subject_contestation": z.object({ "url": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL to a human-accessible contestation form or information page.").optional(), "email": z.string().email().describe("Email address for contestation requests. Deployer MUST monitor and respond within the timelines set by applicable law (e.g., 1 month under GDPR Art. 12(3)).").optional(), "languages": z.array(z.string()).describe("BCP 47 language tags the contestation channel supports (e.g., ['en', 'de', 'fr']). At minimum SHOULD include a language spoken in every jurisdiction where the brand runs regulated-vertical campaigns.").optional() }).strict().and(z.union([z.any(), z.any()])).describe("Contact point where a data subject can request human intervention, express their view, or contest an automated decision — satisfying GDPR Article 22(3) and EU AI Act Article 26(11) transparency obligations. This is a contact reference (URL, email, or both), not a machine-callable API. AdCP surfaces the pointer; the deployer runs the contestation workflow.").optional(), "disclaimers": z.array(z.object({ "text": z.string(), "context": z.string().optional(), "required": z.boolean().default(true) })).describe("Legal disclaimers for creatives").optional(), "trademarks": z.array(z.object({ "registry": z.string().describe("Trademark registry (e.g., 'USPTO', 'EUIPO', 'JPO', 'CNIPA')"), "number": z.string().describe("Registration number as issued by the registry"), "mark": z.string().describe("The registered mark as published"), "status": z.enum(["active","pending","abandoned","cancelled","expired"]).describe("Registration status. Omit for active marks if status tracking is not maintained.").optional(), "license_type": z.enum(["owned","licensed_in","licensed_out"]).describe("Whether the publisher owns the mark, licenses it from another entity, or licenses it to others. 'owned' is the default if omitted.").optional(), "licensor_domain": z.string().regex(new RegExp("^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$")).describe("Domain of the entity that licenses this mark to the publisher. Meaningful when license_type=licensed_in; omit otherwise.").optional(), "countries": z.array(z.string().min(2).max(2)).describe("ISO 3166-1 alpha-2 country codes where this registration applies. Omit for global or where the registry's jurisdiction is implicit.").optional(), "nice_classes": z.array(z.number().int().gte(1).lte(45)).describe("Nice Classification class numbers (1-45) covered by this registration. Disambiguates marks across industries (e.g., Delta-airline vs Delta-faucet). Omit if scope is implicit from registry.").optional() }).catchall(z.any()).describe("A registered trademark. May appear at house level (corporate marks, e.g., 'NIKE' owned by Nike, Inc.) or at brand level (brand-specific marks, e.g., 'CONVERSE' owned by Converse). Resolution between house- and brand-level trademarks is union — both lists are valid claims about marks the publisher controls.")).describe("Brand-level registered trademarks. Use for marks the brand owns or controls (e.g., a sub-brand's own marks distinct from the corporate parent). House-level trademarks live on the house object; resolution between the two is union — both lists are valid claims.").optional(), "voice_synthesis": z.object({ "provider": z.string().optional(), "voice_id": z.string().optional(), "settings": z.record(z.string(), z.any()).optional() }).catchall(z.any()).describe("TTS voice synthesis configuration for AI-generated audio").optional(), "avatar": z.object({ "provider": z.string().optional(), "avatar_id": z.string().optional(), "settings": z.record(z.string(), z.any()).optional() }).catchall(z.any()).describe("Visual avatar configuration").optional(), "visual_guidelines": z.object({ "photography": z.object({ "realism": z.enum(["natural","stylized","hyperreal","abstract"]).describe("Level of photographic realism").optional(), "lighting": z.string().describe("Lighting style (e.g., 'soft daylight', 'studio', 'golden hour', 'high-key', 'low-key')").optional(), "color_temperature": z.enum(["warm","neutral","cool"]).describe("Overall color temperature of photography").optional(), "contrast": z.enum(["low","medium","high"]).describe("Contrast level in photography").optional(), "depth_of_field": z.enum(["shallow","medium","deep"]).describe("Depth of field preference. shallow: blurred background with subject isolation, deep: everything in focus").optional(), "subject": z.object({ "people": z.object({ "age_range": z.string().describe("Target age range (e.g., '20-35')").optional(), "diversity": z.string().describe("Diversity representation (e.g., 'mixed', 'varied')").optional(), "mood": z.array(z.string()).describe("Mood descriptors (e.g., ['confident', 'relaxed'])").optional() }).catchall(z.any()).describe("People photography guidelines").optional(), "product_focus": z.enum(["in-use","isolated","lifestyle","detail"]).describe("How products are shown").optional(), "setting": z.string().describe("Environmental context for photography (e.g., 'indoor', 'outdoor', 'studio', 'urban', 'nature', 'workplace')").optional() }).catchall(z.any()).describe("Subject matter guidelines").optional(), "framing": z.object({ "subject_position": z.string().describe("Where the subject sits in frame (e.g., 'center', 'center-left', 'rule-of-thirds')").optional(), "crop_style": z.string().describe("Cropping convention (e.g., 'waist-up', 'full-body', 'close-up', 'wide')").optional(), "perspective": z.string().describe("Camera perspective (e.g., 'eye-level', 'overhead', 'low-angle')").optional() }).catchall(z.any()).describe("Camera framing rules").optional(), "preferred_aspect_ratios": z.array(z.string().regex(new RegExp("^\\d+:\\d+$"))).describe("Preferred aspect ratios for brand photography (e.g., '16:9', '4:5', '1:1')").optional(), "tags": z.array(z.string()).describe("Additional style descriptors").optional() }).catchall(z.any()).describe("Photography style rules for generative creative systems. Defines how brand photography should look when selected or generated.").optional(), "graphic_style": z.object({ "style_type": z.enum(["flat_illustration","geometric","gradient_mesh","editorial_collage","hand_drawn","minimal_line_art","3d_render","isometric","photographic_composite"]).describe("Primary graphic style").optional(), "stroke_style": z.enum(["rounded","square","mixed","none"]).describe("Stroke end/join style").optional(), "stroke_weight": z.string().describe("Stroke weight (e.g., '2px', 'thin', 'bold')").optional(), "corner_radius": z.string().describe("Default corner radius for graphic and illustration elements (e.g., '12px', '8px', 'sharp'). For UI component radii (buttons, cards, inputs), see visual_guidelines.border_radius.").optional(), "tags": z.array(z.string()).describe("Additional style descriptors").optional() }).catchall(z.any()).describe("Visual language for brand graphics and illustrations").optional(), "shapes": z.object({ "primary_shape": z.string().describe("Primary brand shape (e.g., 'rounded_rectangle', 'circle', 'hexagon')").optional(), "secondary_shapes": z.array(z.string()).describe("Secondary shapes in the brand vocabulary").optional(), "usage": z.object({ "max_per_layout": z.number().int().describe("Maximum distinct shapes per layout").optional(), "overlap_allowed": z.boolean().describe("Whether shapes may overlap").optional() }).catchall(z.any()).describe("Shape usage rules").optional() }).catchall(z.any()).describe("Distinctive shapes used as part of brand visual identity").optional(), "iconography": z.object({ "style": z.enum(["outline","filled","duotone","flat","glyph","hand_drawn"]).describe("Icon rendering style").optional(), "stroke_weight": z.string().describe("Icon stroke weight (e.g., '2px', '1.5px')").optional(), "corner_style": z.enum(["rounded","square","mixed"]).describe("Corner style for icon paths").optional(), "usage": z.object({ "max_per_frame": z.number().int().describe("Maximum icons per creative frame").optional(), "size_ratio": z.string().describe("Icon-to-layout size ratio (e.g., '1:8')").optional() }).catchall(z.any()).describe("Icon usage rules").optional() }).catchall(z.any()).describe("Icon style system and usage rules").optional(), "composition": z.object({ "overlays": z.object({ "gradient_style": z.enum(["linear","radial","conic","none"]).describe("Gradient type for overlays").optional(), "gradient_direction": z.string().describe("Gradient direction (e.g., '45deg', 'to-bottom-right')").optional(), "opacity": z.string().describe("Overlay opacity (e.g., '70%')").optional() }).catchall(z.any()).describe("Graphic overlay rules").optional(), "texture": z.object({ "style": z.enum(["none","subtle_grain","noise","paper","fabric","concrete"]).describe("Texture style applied to creative assets").optional(), "intensity": z.enum(["low","medium","high"]).describe("Texture intensity").optional() }).catchall(z.any()).describe("Texture treatment rules").optional(), "backgrounds": z.object({ "types_allowed": z.array(z.enum(["solid_color","gradient","blurred_photo","image","video","pattern","transparent"])).describe("Permitted background types").optional() }).catchall(z.any()).describe("Background treatment rules").optional() }).catchall(z.any()).describe("Layout composition rules including overlays, textures, and backgrounds").optional(), "border_radius": z.object({ "none": z.string().describe("Explicitly sharp corners (e.g., '0')").optional(), "default": z.string().describe("Default border radius for UI components (e.g., '8px', '12px', '0'). For graphic/illustration elements, see graphic_style.corner_radius.").optional(), "small": z.string().describe("Small border radius for compact elements (e.g., '4px')").optional(), "large": z.string().describe("Large border radius for cards and containers (e.g., '16px', '24px')").optional(), "pill": z.string().describe("Fully rounded / pill shape (e.g., '999px')").optional() }).catchall(z.string()).describe("Named border radius presets for UI components and layout elements. One of the most visible brand differentiators — Airbnb uses generous 20px, Stripe uses precise 4–8px, Spotify uses pill/999px.").optional(), "elevation": z.object({ "none": z.string().describe("No shadow (e.g., 'none')").optional(), "subtle": z.string().describe("Subtle shadow for slight lift (e.g., '0 1px 2px rgba(0,0,0,0.05)')").optional(), "card": z.string().describe("Card-level shadow (e.g., '0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -2px rgba(0,0,0,0.1)')").optional(), "modal": z.string().describe("Modal/overlay shadow (e.g., '0 20px 25px -5px rgba(0,0,0,0.1), 0 8px 10px -6px rgba(0,0,0,0.1)')").optional() }).catchall(z.string()).describe("Named shadow/elevation levels. Brands use elevation as identity — from Stripe's blue-tinted multi-layer shadows to Apple's single diffuse shadow. Values are CSS box-shadow syntax.").optional(), "spacing": z.object({ "unit": z.string().describe("Base grid unit this scale was designed from (e.g., '8px', '4px'). Informational — agents should use the named scale values, not compute from this.").optional(), "scale": z.object({ "xs": z.string().describe("Extra small spacing (e.g., '4px')").optional(), "sm": z.string().describe("Small spacing (e.g., '8px')").optional(), "md": z.string().describe("Medium spacing (e.g., '16px')").optional(), "lg": z.string().describe("Large spacing (e.g., '24px')").optional(), "xl": z.string().describe("Extra large spacing (e.g., '32px')").optional(), "2xl": z.string().describe("Section-level spacing (e.g., '48px', '64px')").optional() }).catchall(z.string()).describe("Named spacing scale built from the base unit").optional() }).strict().describe("Spacing system for consistent layout rhythm. Most design systems use an 8px base grid.").optional(), "graphic_elements": z.array(z.object({ "name": z.string().describe("Element name (e.g., 'Paper Tear', 'Brand Watermark', 'Section Divider')"), "type": z.enum(["border","divider","frame","watermark","pattern","texture_overlay","decorative"]).describe("Element type").optional(), "description": z.string().describe("How the element is used in layouts").optional(), "orientation": z.enum(["horizontal","vertical","any"]).describe("Preferred orientation when used in layouts").optional(), "colors": z.array(z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")).describe("A single hex color value")).describe("Colors this element may appear in").optional(), "max_per_layout": z.number().int().describe("Maximum instances per layout").optional() }).catchall(z.any()).describe("A reusable decorative or structural visual element that is part of the brand identity (e.g., torn paper edges, watermarks, dividers, background patterns)")).describe("Reusable decorative elements that are part of the brand visual identity (e.g., torn paper edges, watermarks, dividers)").optional(), "motion": z.object({ "transition_style": z.enum(["cut","dissolve","slide","wipe","zoom","fade"]).describe("Primary transition style between scenes").optional(), "animation_speed": z.enum(["slow","moderate","fast"]).describe("Overall animation pacing").optional(), "easing": z.string().describe("Default easing function (e.g., 'ease-in-out', 'spring', 'linear')").optional(), "text_entrance": z.enum(["fade","typewriter","slide_up","slide_left","scale","none"]).describe("How text enters the frame").optional(), "pacing": z.enum(["lingering","moderate","fast_cuts"]).describe("Overall editing rhythm").optional(), "kinetic_typography": z.boolean().describe("Whether animated/kinetic typography is allowed").optional(), "tags": z.array(z.string()).describe("Additional motion style descriptors").optional() }).catchall(z.any()).describe("Motion and animation rules for video, animated display, and interactive formats").optional(), "logo_placement": z.object({ "preferred_position": z.enum(["top-left","top-center","top-right","bottom-left","bottom-center","bottom-right","center"]).describe("Preferred logo position in layouts").optional(), "min_clear_space": z.string().describe("Minimum clear space around the logo, expressed as a multiple of logo height (e.g., '0.5x', '1x') or fixed value (e.g., '16px')").optional(), "min_height": z.string().describe("Minimum logo height to maintain legibility (e.g., '40px', '24px')").optional(), "background_contrast": z.enum(["light_only","dark_only","any"]).describe("Permitted background contrast behind logo").optional() }).catchall(z.any()).describe("Logo placement and clear space rules for automated creative production").optional(), "colorways": z.array(z.object({ "name": z.string().describe("Colorway name (e.g., 'primary', 'inverted', 'subtle')"), "foreground": z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")).describe("A single hex color value"), "background": z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")).describe("A single hex color value"), "accent": z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")).describe("A single hex color value").optional(), "border": z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")).describe("A single hex color value").optional(), "cta_foreground": z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")).describe("CTA text/icon color, if different from foreground").optional(), "cta_background": z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")).describe("CTA button/container color, if different from accent").optional(), "channels": z.array(z.string()).describe("Channels or contexts where this colorway applies (e.g., 'online', 'print', 'pos', 'social', 'outdoor'). Omit for universal colorways.").optional() }).catchall(z.any()).describe("A named color pairing that defines how colors work together. Colorways ensure foreground/background combinations are always on-brand and accessible.")).describe("Named color pairings for consistent foreground/background combinations").optional(), "color_constraints": z.array(z.object({ "color": z.object({ "kind": z.enum(["name","value","surface"]).describe("Discriminator for the color reference variant.").optional(), "name": z.string().describe("Key from the colors object, such as market_yellow, river_green, or any custom palette key. Unambiguous lookup into colors{}.").optional(), "value": z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")).describe("Literal hex color when no palette key exists.").optional(), "surface": z.enum(["background","foreground","text","logo_background","cta","accent","border","icon","graphic_element"]).describe("Usage surface rather than a specific color value.").optional() }).strict().and(z.union([z.object({ "kind": z.literal("name") }), z.object({ "kind": z.literal("value") }), z.object({ "kind": z.literal("surface") })])).describe("Color role or value this constraint governs."), "applies_to": z.array(z.enum(["background","foreground","text","logo_background","cta","accent","border","icon","graphic_element"])).describe("Surfaces where this color may be used.").optional(), "allowed_on": z.array(z.object({ "kind": z.enum(["name","value","surface"]).describe("Discriminator for the color reference variant.").optional(), "name": z.string().describe("Key from the colors object, such as market_yellow, river_green, or any custom palette key. Unambiguous lookup into colors{}.").optional(), "value": z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")).describe("Literal hex color when no palette key exists.").optional(), "surface": z.enum(["background","foreground","text","logo_background","cta","accent","border","icon","graphic_element"]).describe("Usage surface rather than a specific color value.").optional() }).strict().and(z.union([z.object({ "kind": z.literal("name") }), z.object({ "kind": z.literal("value") }), z.object({ "kind": z.literal("surface") })])).describe("Reference to a brand color or usage surface for machine-readable guideline constraints. Use kind=name for palette lookup keys in colors, kind=value for a literal hex color, and kind=surface for layout surfaces such as background or text.")).describe("Backgrounds, surfaces, or color roles where this color is allowed.").optional(), "forbidden_on": z.array(z.object({ "kind": z.enum(["name","value","surface"]).describe("Discriminator for the color reference variant.").optional(), "name": z.string().describe("Key from the colors object, such as market_yellow, river_green, or any custom palette key. Unambiguous lookup into colors{}.").optional(), "value": z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")).describe("Literal hex color when no palette key exists.").optional(), "surface": z.enum(["background","foreground","text","logo_background","cta","accent","border","icon","graphic_element"]).describe("Usage surface rather than a specific color value.").optional() }).strict().and(z.union([z.object({ "kind": z.literal("name") }), z.object({ "kind": z.literal("value") }), z.object({ "kind": z.literal("surface") })])).describe("Reference to a brand color or usage surface for machine-readable guideline constraints. Use kind=name for palette lookup keys in colors, kind=value for a literal hex color, and kind=surface for layout surfaces such as background or text.")).describe("Backgrounds, surfaces, or color roles where this color is forbidden.").optional(), "never_pair_with": z.array(z.object({ "kind": z.enum(["name","value","surface"]).describe("Discriminator for the color reference variant.").optional(), "name": z.string().describe("Key from the colors object, such as market_yellow, river_green, or any custom palette key. Unambiguous lookup into colors{}.").optional(), "value": z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")).describe("Literal hex color when no palette key exists.").optional(), "surface": z.enum(["background","foreground","text","logo_background","cta","accent","border","icon","graphic_element"]).describe("Usage surface rather than a specific color value.").optional() }).strict().and(z.union([z.object({ "kind": z.literal("name") }), z.object({ "kind": z.literal("value") }), z.object({ "kind": z.literal("surface") })])).describe("Reference to a brand color or usage surface for machine-readable guideline constraints. Use kind=name for palette lookup keys in colors, kind=value for a literal hex color, and kind=surface for layout surfaces such as background or text.")).describe("Color roles or values that must not be paired with this color.").optional(), "contexts": z.array(z.string()).describe("Channels or creative contexts where this constraint applies, such as digital, print, social, or ctv_end_card.").optional(), "severity": z.enum(["must","should"]).describe("Strength of a machine-readable brand guideline constraint.").optional(), "description": z.string().describe("Human-readable rationale or source-language summary for the rule.").optional() }).catchall(z.any()).describe("Machine-readable rule constraining how a brand color may be used or paired. Use for accent-only colors, forbidden foreground/background combinations, and palette pairs that should never appear together.")).describe("Machine-readable constraints for color usage and pairings, such as accent-only rules or forbidden foreground/background combinations.").optional(), "logo_usage_rules": z.array(z.object({ "logo_url": z.string().url().describe("Specific logo asset URL this rule applies to. Omit when the rule applies by variant or tags.").optional(), "logo_id": z.string().regex(new RegExp("^[a-z0-9][a-z0-9_-]*$")).describe("Stable `logos[].id` this rule applies to. Prefer this over logo_url when the rule targets a specific logo entry.").optional(), "logo_variant": z.enum(["primary","secondary","icon","wordmark","full-lockup"]).describe("Logo variant this rule applies to.").optional(), "logo_tags": z.array(z.string()).describe("Logo tags this rule applies to.").optional(), "contexts": z.array(z.string()).describe("Creative contexts where this rule applies.").optional(), "slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Canonical renderer slots where this rule applies. Use this for deterministic logo-card, profile-mark, end-card, and lockup selection.").optional(), "minimum_size": z.object({ "width": z.string().describe("Minimum width, such as 48px or 12mm.").optional(), "height": z.string().describe("Minimum height, such as 18px or 6mm.").optional() }).strict().describe("Minimum rendered size needed for legibility.").optional(), "clear_space": z.string().describe("Minimum clear space around the logo, expressed in brand terms or units.").optional(), "allowed_backgrounds": z.array(z.object({ "kind": z.enum(["name","value","surface"]).describe("Discriminator for the color reference variant.").optional(), "name": z.string().describe("Key from the colors object, such as market_yellow, river_green, or any custom palette key. Unambiguous lookup into colors{}.").optional(), "value": z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")).describe("Literal hex color when no palette key exists.").optional(), "surface": z.enum(["background","foreground","text","logo_background","cta","accent","border","icon","graphic_element"]).describe("Usage surface rather than a specific color value.").optional() }).strict().and(z.union([z.object({ "kind": z.literal("name") }), z.object({ "kind": z.literal("value") }), z.object({ "kind": z.literal("surface") })])).describe("Reference to a brand color or usage surface for machine-readable guideline constraints. Use kind=name for palette lookup keys in colors, kind=value for a literal hex color, and kind=surface for layout surfaces such as background or text.")).describe("Background color roles, values, or surfaces where this logo may be placed.").optional(), "forbidden_backgrounds": z.array(z.object({ "kind": z.enum(["name","value","surface"]).describe("Discriminator for the color reference variant.").optional(), "name": z.string().describe("Key from the colors object, such as market_yellow, river_green, or any custom palette key. Unambiguous lookup into colors{}.").optional(), "value": z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")).describe("Literal hex color when no palette key exists.").optional(), "surface": z.enum(["background","foreground","text","logo_background","cta","accent","border","icon","graphic_element"]).describe("Usage surface rather than a specific color value.").optional() }).strict().and(z.union([z.object({ "kind": z.literal("name") }), z.object({ "kind": z.literal("value") }), z.object({ "kind": z.literal("surface") })])).describe("Reference to a brand color or usage surface for machine-readable guideline constraints. Use kind=name for palette lookup keys in colors, kind=value for a literal hex color, and kind=surface for layout surfaces such as background or text.")).describe("Background color roles, values, or surfaces where this logo must not be placed.").optional(), "forbidden_contexts": z.array(z.string()).describe("Contexts where this logo must not be used, such as photography_without_knockout.").optional(), "severity": z.enum(["must","should"]).describe("Strength of a machine-readable brand guideline constraint.").optional(), "description": z.string().describe("Human-readable rationale or source-language summary for the rule.").optional() }).catchall(z.any()).and(z.union([z.any(), z.any(), z.any(), z.any(), z.any()])).describe("Machine-readable logo selection and placement rule. Complements logos[].usage by making enforceable minimum size, clear-space, background, and context constraints queryable.")).describe("Machine-readable logo selection and placement constraints for minimum size, clear space, backgrounds, and contexts.").optional(), "mark_lockups": z.array(z.object({ "lockup_type": z.enum(["co_brand","secondary_mark","partner","sponsor","program","talent","custom"]).describe("Type of mark relationship governed by this lockup rule."), "ordering": z.enum(["brand_first","partner_first","equal","contextual"]).describe("Required visual ordering of the brand mark relative to partner or secondary marks.").optional(), "contexts": z.array(z.string()).describe("Creative contexts where this lockup rule applies.").optional(), "brand_logo_id": z.string().regex(new RegExp("^[a-z0-9][a-z0-9_-]*$")).describe("Stable `logos[].id` for the brand logo this lockup rule is anchored on.").optional(), "secondary_logo_ids": z.array(z.string().regex(new RegExp("^[a-z0-9][a-z0-9_-]*$")).describe("Stable identifier for a logo entry within this brand.json document. Use lowercase words separated by underscores or hyphens; do not key integrations on mutable asset URLs.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Stable `logos[].id` values for secondary, program, sponsor, or partner marks governed by this lockup rule when those marks are represented in this brand.json.").optional(), "separator": z.object({ "type": z.enum(["none","keyline","space","divider"]).describe("Separator style."), "color": z.object({ "kind": z.enum(["name","value","surface"]).describe("Discriminator for the color reference variant.").optional(), "name": z.string().describe("Key from the colors object, such as market_yellow, river_green, or any custom palette key. Unambiguous lookup into colors{}.").optional(), "value": z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")).describe("Literal hex color when no palette key exists.").optional(), "surface": z.enum(["background","foreground","text","logo_background","cta","accent","border","icon","graphic_element"]).describe("Usage surface rather than a specific color value.").optional() }).strict().and(z.union([z.object({ "kind": z.literal("name") }), z.object({ "kind": z.literal("value") }), z.object({ "kind": z.literal("surface") })])).describe("Reference to a brand color or usage surface for machine-readable guideline constraints. Use kind=name for palette lookup keys in colors, kind=value for a literal hex color, and kind=surface for layout surfaces such as background or text.").optional(), "width": z.string().describe("Separator width, such as 1px.").optional() }).catchall(z.any()).describe("Separator between marks, when required.").optional(), "min_gap": z.string().describe("Minimum gap between marks, expressed in brand terms or units.").optional(), "brand_min_optical_weight_ratio": z.number().gt(0).describe("Minimum optical weight of the brand mark relative to partner marks. 1 means at least equal.").optional(), "partner_max_optical_weight_ratio": z.number().gt(0).describe("Maximum optical weight of partner marks relative to the brand mark. 1 means no larger than the brand mark. Enforcement is at layout time, not parse time — this value signals to renderers and creative agents how much space to provision for each mark.").optional(), "severity": z.enum(["must","should"]).describe("Strength of a machine-readable brand guideline constraint.").optional(), "description": z.string().describe("Human-readable rationale or source-language summary for the lockup rule.").optional() }).catchall(z.any()).describe("Machine-readable layout constraints for co-brand, partner, sponsor, program, or secondary-mark lockups.")).describe("Machine-readable co-brand, partner, sponsor, program, or secondary-mark lockup rules.").optional(), "type_scale": z.object({ "base_width": z.string().describe("Reference canvas width these sizes were designed for (e.g., '1080px'). Generative systems should scale proportionally for other canvas sizes.").optional(), "heading": z.object({ "font": z.string().describe("Font reference. Use a key from the fonts object (e.g., 'primary', 'secondary') to reference a defined font role, or a literal CSS font-family string as a fallback.").optional(), "size": z.string().describe("Font size (e.g., '48px', '2rem')").optional(), "weight": z.string().describe("Font weight (e.g., '700', 'bold')").optional(), "line_height": z.string().describe("Line height (e.g., '1.2', '56px')").optional(), "letter_spacing": z.string().describe("Letter spacing (e.g., '-0.02em', '0.5px')").optional(), "text_transform": z.enum(["none","uppercase","lowercase","capitalize"]).describe("Text transformation").optional() }).catchall(z.any()).describe("A single entry in the type scale").optional(), "subheading": z.object({ "font": z.string().describe("Font reference. Use a key from the fonts object (e.g., 'primary', 'secondary') to reference a defined font role, or a literal CSS font-family string as a fallback.").optional(), "size": z.string().describe("Font size (e.g., '48px', '2rem')").optional(), "weight": z.string().describe("Font weight (e.g., '700', 'bold')").optional(), "line_height": z.string().describe("Line height (e.g., '1.2', '56px')").optional(), "letter_spacing": z.string().describe("Letter spacing (e.g., '-0.02em', '0.5px')").optional(), "text_transform": z.enum(["none","uppercase","lowercase","capitalize"]).describe("Text transformation").optional() }).catchall(z.any()).describe("A single entry in the type scale").optional(), "body": z.object({ "font": z.string().describe("Font reference. Use a key from the fonts object (e.g., 'primary', 'secondary') to reference a defined font role, or a literal CSS font-family string as a fallback.").optional(), "size": z.string().describe("Font size (e.g., '48px', '2rem')").optional(), "weight": z.string().describe("Font weight (e.g., '700', 'bold')").optional(), "line_height": z.string().describe("Line height (e.g., '1.2', '56px')").optional(), "letter_spacing": z.string().describe("Letter spacing (e.g., '-0.02em', '0.5px')").optional(), "text_transform": z.enum(["none","uppercase","lowercase","capitalize"]).describe("Text transformation").optional() }).catchall(z.any()).describe("A single entry in the type scale").optional(), "caption": z.object({ "font": z.string().describe("Font reference. Use a key from the fonts object (e.g., 'primary', 'secondary') to reference a defined font role, or a literal CSS font-family string as a fallback.").optional(), "size": z.string().describe("Font size (e.g., '48px', '2rem')").optional(), "weight": z.string().describe("Font weight (e.g., '700', 'bold')").optional(), "line_height": z.string().describe("Line height (e.g., '1.2', '56px')").optional(), "letter_spacing": z.string().describe("Letter spacing (e.g., '-0.02em', '0.5px')").optional(), "text_transform": z.enum(["none","uppercase","lowercase","capitalize"]).describe("Text transformation").optional() }).catchall(z.any()).describe("A single entry in the type scale").optional(), "cta": z.object({ "font": z.string().describe("Font reference. Use a key from the fonts object (e.g., 'primary', 'secondary') to reference a defined font role, or a literal CSS font-family string as a fallback.").optional(), "size": z.string().describe("Font size (e.g., '48px', '2rem')").optional(), "weight": z.string().describe("Font weight (e.g., '700', 'bold')").optional(), "line_height": z.string().describe("Line height (e.g., '1.2', '56px')").optional(), "letter_spacing": z.string().describe("Letter spacing (e.g., '-0.02em', '0.5px')").optional(), "text_transform": z.enum(["none","uppercase","lowercase","capitalize"]).describe("Text transformation").optional() }).catchall(z.any()).describe("A single entry in the type scale").optional() }).catchall(z.object({ "font": z.string().describe("Font reference. Use a key from the fonts object (e.g., 'primary', 'secondary') to reference a defined font role, or a literal CSS font-family string as a fallback.").optional(), "size": z.string().describe("Font size (e.g., '48px', '2rem')").optional(), "weight": z.string().describe("Font weight (e.g., '700', 'bold')").optional(), "line_height": z.string().describe("Line height (e.g., '1.2', '56px')").optional(), "letter_spacing": z.string().describe("Letter spacing (e.g., '-0.02em', '0.5px')").optional(), "text_transform": z.enum(["none","uppercase","lowercase","capitalize"]).describe("Text transformation").optional() }).catchall(z.any()).describe("A single entry in the type scale")).describe("Typography scale defining sizes and weights for different text roles. When sizes are in px, use base_width to indicate the reference canvas.").optional(), "asset_libraries": z.array(z.object({ "name": z.string().describe("Display name of the asset library"), "type": z.enum(["icon_set","illustration_system","image_library","video_library","template_library"]).describe("Type of asset library").optional(), "url": z.string().url().describe("URL to the asset library (for human access)"), "description": z.string().describe("Description of the library contents and usage").optional(), "color_guide": z.object({ "roles": z.array(z.string()).describe("Named color roles used in the library (e.g., base, shadow_1, highlight_1, stroke)").optional(), "palettes": z.array(z.object({ "name": z.string().describe("Palette name"), "colors": z.record(z.string(), z.string().regex(new RegExp("^#[0-9A-Fa-f]{6}$")).describe("A single hex color value")).describe("Map of role names to hex color values") }).catchall(z.any())).describe("Named color palettes mapping roles to specific colors").optional() }).catchall(z.any()).describe("Color guide for the asset library defining roles and palettes").optional() }).catchall(z.any()).describe("A managed asset library (icon set, illustration system, image collection). The URL is for human access; agent-facing DAM integration is under investigation.")).describe("References to managed asset libraries (icon sets, illustration systems, image collections). URLs are intended for human access; agent-facing DAM integration is under investigation.").optional(), "restrictions": z.array(z.string()).describe("Visual prohibitions and guardrails (e.g., 'Never use black backgrounds', 'Do not crop the logo', 'No stock photography of people on phones')").optional() }).catchall(z.any()).describe("Structured visual rules for generative creative systems").optional(), "agents": z.array(z.object({ "type": z.enum(["brand","rights","measurement","governance","creative","sales","buying","signals"]).describe("Functional role of this agent"), "url": z.string().url().regex(new RegExp("^https://")).describe("Agent endpoint URL (MCP or A2A). Callers comparing a brand's declared agent URL against another value (e.g., resolving 'is this the agent that signed this artifact?' or matching against a discovery cache) MUST canonicalize both sides per the AdCP URL canonicalization rules, not byte-equality. See docs/reference/url-canonicalization."), "id": z.string().regex(new RegExp("^[a-z0-9_]+$")).max(100).describe("Agent identifier (useful for logging, multi-tenant platforms)"), "description": z.string().max(500).describe("Human-readable description of this agent's capabilities or scope").optional(), "jwks_uri": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL of the agent's JWKS (RFC 7517) containing public keys used to verify artifacts this agent signs or requests it sends. Verified artifacts include signed governance_context tokens (for governance agents) and RFC 9421 HTTP Signatures on outgoing requests (for any agent). When absent, verifiers MUST default to /.well-known/jwks.json on the origin of `url`. Keys are identified by `kid` in the JWS header or RFC 9421 `keyid` parameter; JWKS MAY contain multiple keys to support rotation and per-purpose separation via `key_ops` and `use`.").optional(), "available_uses": z.array(z.enum(["likeness","voice","name","endorsement","motion_capture","signature","catchphrase","sync","background_music","editorial","commercial","ai_generated_image"]).describe("Types of rights usage that can be licensed through the brand protocol. Aligned with DDEX UseType direction for interoperability with music and media rights systems.")).describe("For rights agents: rights uses available for licensing").optional(), "right_types": z.array(z.enum(["talent","character","brand_ip","music","stock_media"]).describe("Categories of intellectual property rights that can be licensed through the brand protocol.")).describe("For rights agents: types of rights available").optional(), "countries": z.array(z.string().regex(new RegExp("^[A-Z]{2}$"))).describe("ISO 3166-1 alpha-2 country codes where this agent operates. Omit for global scope.").optional() }).catchall(z.any()).describe("An agent declared by a brand or house. Each entry identifies one agent endpoint and its functional role in the advertising ecosystem.")).describe("Agents authorized to act on behalf of this brand. Consumers resolving an agent by URL use the matching brand-level entry; do not infer a type-wide override of unrelated house-level entries when multiple same-type entries exist.").optional(), "brand_agent": z.object({ "url": z.string().url().regex(new RegExp("^https://")).describe("Brand agent MCP endpoint URL. Callers comparing this URL against another value (e.g., resolving 'is this the brand's declared agent?' against a discovery cache) MUST canonicalize both sides per the AdCP URL canonicalization rules, not byte-equality. See docs/reference/url-canonicalization."), "id": z.string().regex(new RegExp("^[a-z0-9_]+$")).describe("Agent identifier (useful for logging, multi-tenant DAMs)") }).catchall(z.any()).describe("Deprecated: use agents array with type 'brand' instead. Brand agent that provides dynamic brand data via MCP.").optional(), "rights_agent": z.object({ "url": z.string().url().regex(new RegExp("^https://")).describe("Rights agent MCP endpoint URL. Callers comparing this URL against another value (e.g., matching against a brand's declared rights endpoint) MUST canonicalize both sides per the AdCP URL canonicalization rules, not byte-equality. See docs/reference/url-canonicalization."), "id": z.string().regex(new RegExp("^[a-z0-9_]+$")).describe("Agent identifier"), "available_uses": z.array(z.enum(["likeness","voice","name","endorsement","motion_capture","signature","catchphrase","sync","background_music","editorial","commercial","ai_generated_image"]).describe("Types of rights usage that can be licensed through the brand protocol. Aligned with DDEX UseType direction for interoperability with music and media rights systems.")).describe("Rights uses available for licensing through this agent"), "right_types": z.array(z.enum(["talent","character","brand_ip","music","stock_media"]).describe("Categories of intellectual property rights that can be licensed through the brand protocol.")).describe("Types of rights available").optional(), "countries": z.array(z.string().regex(new RegExp("^[A-Z]{2}$"))).describe("Countries where rights are available (ISO 3166-1 alpha-2)").optional() }).catchall(z.any()).describe("Deprecated: use agents array with type 'rights' instead. Rights licensing agent for this brand.").optional(), "contact": z.object({ "email": z.string().email().describe("Contact email").optional(), "phone": z.string().describe("Contact phone number").optional() }).describe("Brand-level contact information").optional(), "collections": z.array(z.object({ "collection_id": z.string().describe("Collection identifier as used in the seller's get_products responses").optional(), "name": z.string().describe("Human-readable collection name"), "role": z.enum(["host","guest","creator","cast","narrator","producer","correspondent","commentator","analyst"]).describe("This person's role on the collection").optional(), "seller_agent_url": z.string().url().describe("URL of the sales agent that sells inventory for this collection. Buyer agents can query this agent for collection products.").optional() }).catchall(z.any())).describe("Collections this person or brand is associated with. Enables bidirectional linking: a collection's talent references brand.json via brand_url, and brand.json links back to collections.").optional() }).catchall(z.any()).describe("A brand within a house portfolio. Combines identity (who) with creative assets (how to represent). Referenced as domain + brand_id."))).describe("Self-published brand document where the brand owns its own identity attributes. Optionally declares its house via house_domain; for trust, the named house's brand_refs[] must reciprocate (mutual assertion). Standalone brands (no parent house) omit house_domain. Hosted at the brand's own /.well-known/brand.json (or via authoritative_location indirection). See docs/brand-protocol/brand-json.mdx")]).describe("Brand identity and discovery file. Hosted at /.well-known/brand.json on house domains. Contains the full brand portfolio with identity, creative assets, and digital properties. Brands are identified by house + brand_id (like properties are identified by publisher + property_id). Supports variants: house portfolio (full brand data), brand agent (agent provides brand info via MCP), house redirect (pointer to house domain), or authoritative location redirect.") export type BrandJson = z.infer; // ---- adagents.json ---- -export const AdagentsJsonSchema = z.union([z.object({ "$schema": z.string().describe("JSON Schema identifier for this adagents.json file").optional(), "authoritative_location": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL of the authoritative adagents.json file. When present, this file is a reference and the authoritative location contains the actual agent authorization data. Because one deploy can change authorization across every publisher in the network, validators MUST cap response size, refuse redirects on the fetch, enforce short timeouts, and serve the previously cached file on transient 5xx. Two-tier size cap: pointer files served at `/.well-known/adagents.json` use the general 5 MB SSRF cap; dereferenced authoritative files (this URL's response, after the indirection) use a recommended 20 MB cap because the origin has explicitly opted in to fanning out across a publisher network. See docs/governance/property/managed-networks#security-considerations."), "last_updated": z.string().datetime().describe("ISO 8601 timestamp indicating when this reference was last updated").optional() }).catchall(z.any()).describe("URL reference variant - points to the authoritative location of the adagents.json file"), z.object({ "$schema": z.string().describe("JSON Schema identifier for this adagents.json file").optional(), "contact": z.object({ "name": z.string().min(1).max(255).describe("Name of the entity managing this file (e.g., 'Meta Advertising Operations', 'Clear Channel Digital')"), "email": z.string().email().min(1).max(255).describe("Contact email for questions or issues with this authorization file").optional(), "domain": z.string().regex(new RegExp("^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$")).describe("Primary domain of the entity managing this file").optional(), "seller_id": z.string().min(1).max(255).describe("Seller ID from IAB Tech Lab sellers.json (if applicable)").optional(), "tag_id": z.string().min(1).max(100).describe("TAG Certified Against Fraud ID for verification (if applicable)").optional(), "privacy_policy_url": z.string().url().describe("URL to the entity's privacy policy. Used for consumer consent flows when interacting with this sales agent.").optional() }).catchall(z.any()).describe("Contact information for the entity managing this adagents.json file (may be publisher or third-party operator)").optional(), "catalog_etag": z.string().min(1).max(255).describe("Opaque publisher-controlled cache validator for the public catalog portions of this file (`properties[]`, `collections[]`, `placements[]`, `formats[]`, `signals[]`, and tag metadata). Publishers SHOULD change this value whenever any catalog entry or catalog-scoped authorization changes, even when the hosting URL and HTTP validators stay the same. Buyer SDKs SHOULD cache resolved catalog lookups by URL plus `catalog_etag` (falling back to HTTP ETag/Last-Modified, then bounded TTL when absent) and re-resolve placement, format, collection, property, and signal references when it changes. This value is not a cryptographic digest; it is a compact version token such as a deployment hash, revision ID, or ISO timestamp.").optional(), "catalog_role": z.literal("community_format_registry").describe("Marks a catalog payload intended for the fixed AgenticAdvertising.org community-format registry. This self-description is not provenance: consumers accept reference_renderer only when the document was obtained from the configured, reviewed community-registry origin.").optional(), "properties": z.array(z.object({ "property_id": z.string().regex(new RegExp("^[a-z0-9_]+$")).describe("Unique identifier for this property (optional). Enables referencing properties by ID instead of repeating full objects.").optional(), "property_type": z.enum(["website","mobile_app","ctv_app","desktop_app","dooh","podcast","radio","linear_tv","streaming_audio","ai_assistant"]).describe("Type of advertising property"), "name": z.string().describe("Human-readable property name"), "identifiers": z.array(z.object({ "type": z.enum(["domain","subdomain","network_id","ios_bundle","android_package","apple_app_store_id","google_play_id","roku_store_id","fire_tv_asin","samsung_app_id","apple_tv_bundle","bundle_id","venue_id","screen_id","openooh_venue_type","rss_url","apple_podcast_id","spotify_collection_id","podcast_guid","station_id","facility_id"]).describe("Type of identifier for this property"), "value": z.string().describe("The identifier value. For domain type: 'example.com' matches base domain plus www and m subdomains; 'edition.example.com' matches that specific subdomain; '*.example.com' matches ALL subdomains but NOT base domain") }).catchall(z.any())).describe("Array of identifiers for this property"), "tags": z.array(z.string().regex(new RegExp("^[a-z0-9_]+$")).describe("Tag for categorizing publisher properties. Must be lowercase alphanumeric with underscores only.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Tags for categorization and grouping (e.g., network membership, content categories)").optional(), "supported_channels": z.array(z.enum(["display","olv","social","search","ctv","linear_tv","radio","streaming_audio","podcast","dooh","ooh","print","cinema","email","gaming","retail_media","influencer","affiliate","product_placement","sponsored_intelligence"]).describe("Standardized advertising media channels describing how buyers allocate budget. Channels are planning abstractions, not technical substrates. See the Media Channel Taxonomy specification for detailed definitions.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Advertising channels this property supports (e.g., ['display', 'olv', 'social']). Publishers declare which channels their inventory aligns with. Properties may support multiple channels. See the Media Channel Taxonomy for definitions.").optional(), "publisher_domain": z.string().describe("Domain where adagents.json should be checked for authorization validation. Optional in adagents.json (file location implies domain).").optional() }).catchall(z.any()).describe("An advertising property that can be validated via adagents.json")).describe("Array of all properties covered by this adagents.json file. Defines the canonical property list that authorized agents reference.").optional(), "revoked_publisher_domains": z.array(z.object({ "publisher_domain": z.string().regex(new RegExp("^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$")).describe("Publisher domain being revoked. Matches against the same canonicalized form used in `publisher_properties[].publisher_domain`."), "revoked_at": z.string().datetime().describe("ISO 8601 timestamp when this publisher was revoked. Validators MAY use this to order revocations against their own cached state."), "reason": z.enum(["relationship_ended","compliance_violation","publisher_request","other"]).describe("Reason for revocation. **Operator-internal self-classification for review routing — not a public accusation.** `relationship_ended` is the routine commercial case. `compliance_violation` SHOULD be used only when the network has itself determined the publisher is out of policy; for un-adjudicated third-party allegations (regulator inquiries, advertiser complaints, ongoing investigations), use `other` to avoid making a discoverable adverse statement. `publisher_request` is for publisher-initiated exits. Compare to sellers.json, which deliberately carries no reason field for the same exposure concern.").optional() }).catchall(z.any())).describe("Publisher domains explicitly removed from this managed network. Validators MUST treat any publisher domain listed here as no-longer-authorized, taking precedence over any appearance of the same domain in `authorized_agents[].publisher_properties[].publisher_domain` / `.publisher_domains[]`, in `authorized_agents[].properties[].publisher_domain` (`inline_properties` authorization type), or in top-level `properties[].publisher_domain`. Lets a network propagate per-publisher revocations on the next refresh instead of waiting for the file-level 7-day cache cap. Validators MUST hold previously-observed `(publisher_domain, revoked_at)` tuples for 7 days from the validator's first observation, even if the entry vanishes from a subsequent fetch — this closes the rollback gap where an attacker re-serves a stale file with the revocation removed. Networks SHOULD retain entries for at least 7 days after `revoked_at` so validators that didn't observe the original entry still pick it up on refresh.").optional(), "collections": z.array(z.object({ "collection_id": z.string().describe("Publisher-assigned identifier for this collection. Declared in the publisher's adagents.json collections array. Products reference collections via collection selectors with publisher_domain and collection_ids. Use distribution identifiers for cross-seller matching across publishers."), "name": z.string().describe("Human-readable collection name"), "kind": z.enum(["series","publication","event_series","rotation"]).describe("What kind of content program this is. Helps agents interpret installments correctly. Defaults to 'series' when absent.").optional(), "description": z.string().describe("What the collection is about").optional(), "genre": z.array(z.string()).describe("Genre tags. When genre_taxonomy is present, values are taxonomy IDs (e.g., IAB Content Taxonomy 3.0 codes). Otherwise free-form.").optional(), "genre_taxonomy": z.string().describe("Taxonomy system for genre values (e.g., 'iab_content_3.0'). When present, genre values should be valid taxonomy IDs. Recommended for machine-readable brand safety evaluation.").optional(), "language": z.string().describe("Primary language (BCP 47 tag, e.g., 'en', 'es-MX')").optional(), "content_rating": z.object({ "system": z.enum(["tv_parental","mpaa","podcast","esrb","bbfc","fsk","acb","chvrs","csa","pegi","custom"]).describe("Rating system used"), "rating": z.string().describe("Rating value within the system (e.g., 'TV-PG', 'R', 'explicit')") }).catchall(z.any()).describe("Baseline content rating for the collection. Individual installments may override this.").optional(), "cadence": z.enum(["daily","weekly","monthly","seasonal","event","irregular"]).describe("How frequently the collection releases new installments").optional(), "season": z.string().describe("Current or most recent season identifier (e.g., '3', '2026', 'spring_2026'). A lightweight label — not a full season object.").optional(), "status": z.enum(["active","hiatus","ended","upcoming"]).describe("Lifecycle status of the collection").optional(), "production_quality": z.enum(["professional","prosumer","ugc"]).describe("Production quality tier. Seller-declared. Maps to OpenRTB content.prodq (professional=1, prosumer=2, ugc=3).").optional(), "talent": z.array(z.object({ "role": z.enum(["host","guest","creator","cast","narrator","producer","correspondent","commentator","analyst"]).describe("Role of this person on the collection or installment"), "name": z.string().describe("Person's name as credited on the collection"), "brand_url": z.string().url().describe("URL to this person's brand.json entry. Enables buyer agents to evaluate the talent's brand identity and associations.").optional() }).catchall(z.any()).describe("A person associated with a collection or installment, with an optional link to their brand.json identity")).describe("Hosts, recurring cast, creators associated with the collection. Each talent entry may include a brand_url linking to their brand.json identity.").optional(), "special": z.object({ "name": z.string().describe("Name of the event (e.g., 'Olympics 2028', 'Super Bowl LXI')"), "category": z.enum(["awards","championship","concert","conference","election","festival","gala","holiday","premiere","product_launch","reunion","tribute"]).describe("Category of the event").optional(), "starts": z.string().datetime().describe("When the event starts (ISO 8601)").optional(), "ends": z.string().datetime().describe("When the event ends (ISO 8601). Omit for single-day events.").optional() }).catchall(z.any()).describe("When present, this collection is a special — content anchored to a real-world event or occasion. Individual installments may override with their own event context.").optional(), "limited_series": z.object({ "total_installments": z.number().int().gte(1).describe("Planned number of installments in the series"), "starts": z.string().datetime().describe("When the series begins (ISO 8601)").optional(), "ends": z.string().datetime().describe("When the series ends (ISO 8601)").optional() }).catchall(z.any()).describe("When present, this collection is a limited series — a bounded run with a defined arc, installment count, and end date.").optional(), "distribution": z.array(z.object({ "publisher_domain": z.string().describe("Domain of the publisher platform where the collection is distributed (e.g., 'youtube.com', 'spotify.com')"), "identifiers": z.array(z.object({ "type": z.enum(["apple_podcast_id","spotify_collection_id","rss_url","podcast_guid","amazon_music_id","iheart_id","podcast_index_id","youtube_channel_id","youtube_channel_handle","youtube_channel_url","youtube_playlist_id","amazon_title_id","roku_channel_id","pluto_channel_id","tubi_id","peacock_id","tiktok_id","twitch_channel","imdb_id","gracenote_id","eidr_id","domain","substack_id"]).describe("Type of distribution identifier"), "value": z.string().describe("The identifier value") }).strict()).describe("Platform-specific identifiers for the collection on this publisher") }).catchall(z.any()).describe("A collection's presence on a specific publisher platform, identified by platform-specific identifiers. Enables cross-seller matching when the same collection is sold by different agents.")).describe("Where this collection is distributed. Each entry maps the collection to a publisher platform with platform-specific identifiers. Collections SHOULD include at least one platform-independent identifier (imdb_id, gracenote_id, eidr_id) when available.").optional(), "deadline_policy": z.object({ "booking_lead_days": z.number().int().gte(0).describe("Days before scheduled_at by which the placement must be booked").optional(), "cancellation_lead_days": z.number().int().gte(0).describe("Days before scheduled_at by which cancellation is penalty-free").optional(), "material_stages": z.array(z.object({ "stage": z.string().describe("Stage identifier. Standard values: 'draft' (needs seller processing), 'final' (production-ready)."), "lead_days": z.number().int().gte(0).describe("Days before scheduled_at this stage is due"), "label": z.string().describe("What the seller needs at this stage").optional() }).catchall(z.any())).describe("Default material submission stages. Items MUST be in chronological order (earliest due first). Agents compute due_at as: installment.scheduled_at minus lead_days.").optional(), "business_days_only": z.boolean().describe("When true, lead_days counts business days (Mon-Fri) rather than calendar days. Defaults to false.").default(false) }).catchall(z.any()).describe("Default deadline rules for installments of this collection. Agents compute absolute deadlines from each installment's scheduled_at and these lead times. Installments with explicit deadlines override this policy.").optional(), "related_collections": z.array(z.object({ "collection_id": z.string().describe("The related collection's collection_id within this seller's response"), "relationship": z.enum(["spinoff","companion","sequel","prequel","crossover"]).describe("How the collections are related") }).strict()).describe("Relationships to other collections (spin-offs, companion collections, etc.). Each entry references another collection by collection_id within the same publisher's adagents.json.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension object for platform-specific, vendor-namespaced parameters. Extensions are always optional and must be namespaced under a vendor/platform key (e.g., ext.gam, ext.roku). Used for custom capabilities, partner-specific configuration, and features being proposed for standardization.").optional() }).catchall(z.any()).describe("A recurring inventory container — a named program, publication, event series, or rotation that produces bookable installments on a defined cadence. The kind field indicates how to interpret this collection: 'series' for TV/podcast programs, 'publication' for print/newsletter titles, 'event_series' for live events, 'rotation' for DOOH scheduling. Declared in the publisher's adagents.json and referenced by products via collection selectors.")).describe("Collections produced or distributed by this publisher. Declares the content programs whose inventory is sold through authorized agents. Products in get_products responses reference these collections by collection_id.").optional(), "placements": z.array(z.object({ "placement_id": z.string().describe("Stable placement identifier unique within this adagents.json file."), "name": z.string().describe("Human-readable placement name (e.g., 'Homepage Banner', 'Pre-roll', 'Sponsored Listing Slot 1')."), "description": z.string().describe("Description of where and how this placement appears.").optional(), "tags": z.array(z.string()).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Tags for grouping and querying placements across properties and products (e.g., 'homepage', 'native', 'premium', 'pre_roll').").optional(), "property_ids": z.array(z.string().regex(new RegExp("^[a-z0-9_]+$")).describe("Identifier for a publisher property. Must be lowercase alphanumeric with underscores only.")).describe("Property IDs in this adagents.json where this placement can appear.").optional(), "property_tags": z.array(z.string().regex(new RegExp("^[a-z0-9_]+$")).describe("Tag for categorizing publisher properties. Must be lowercase alphanumeric with underscores only.")).describe("Property tags in this adagents.json where this placement can appear. Useful for network-wide positions such as 'pre_roll' or 'homepage_native_feed'.").optional(), "collection_ids": z.array(z.string()).describe("Optional collection IDs in this adagents.json where this placement is valid. Use to narrow a placement to specific content programs carried on the selected properties.").optional(), "channels": z.array(z.enum(["display","olv","social","search","ctv","linear_tv","radio","streaming_audio","podcast","dooh","ooh","print","cinema","email","gaming","retail_media","influencer","affiliate","product_placement","sponsored_intelligence"]).describe("Standardized advertising media channels describing how buyers allocate budget. Channels are planning abstractions, not technical substrates. See the Media Channel Taxonomy specification for detailed definitions.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Advertising channels where this placement can run. Products that reference the placement may narrow this set but should not broaden it.").optional(), "presentation_ref": z.object({ "uri": z.string().url().regex(new RegExp("^https://")).describe("Publisher-controlled HTTPS URL for the presentation metadata. Consumers MUST apply the same SSRF, redirect, response-size, timeout, and DNS-rebinding protections used for format_schema fetches."), "digest": z.string().regex(new RegExp("^sha256:[a-f0-9]{64}$")).describe("SHA-256 content digest. Consumers cache by uri@digest and MUST fail closed on a digest mismatch."), "media_type": z.literal("application/vnd.adcp.placement-presentation+json").describe("Media type of the referenced declarative presentation document."), "schema_version": z.literal("1.0").describe("Version of /schemas/core/placement-presentation.json used to validate and compose the referenced document.") }).strict().describe("Optional publisher-specific declarative frame for representing this placement's real chrome offline. It composes around the selected creative rendering unless the publisher-delegated preview_provider route explicitly covers placement presentation. It MUST NOT be promoted to or copied onto a shared format entry.").optional(), "preview_provider": z.object({ "agent_url": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL of the delegated creative-agent endpoint. Buyers call get_adcp_capabilities and preview_creative on this endpoint. They MUST allow only public IPs, pin DNS resolution through connection, refuse redirects, cap time and response size, and attach provider credentials only after exact normalized-origin binding."), "authority": z.literal("publisher_designated").describe("Explicitly states that authority comes from the publisher-hosted placement declaration. The provider's rendering_origin metadata is informational and remains non-authoritative elsewhere."), "routes": z.array(z.object({ "format_option_id": z.string().min(1).describe("Format option in this adagents.json placement for which the delegation applies. It MUST resolve through the same-file top-level formats[] catalog or an inline placement format declaration."), "capability_id": z.string().regex(new RegExp("^[a-zA-Z0-9_-]+$")).describe("Agent-local preview capability advertised by the delegated provider. The provider's canonical format declaration MUST satisfy the resolved placement format option."), "covers_placement_presentation": z.boolean().describe("True only when the publisher delegates both creative rendering and the complete placement-specific frame to this route. When false or omitted, consumers compose any presentation_ref around the provider's creative render.").default(false) }).strict()) }).strict().describe("Optional publisher delegation to a callable AdCP preview provider for specific format options on this placement. This publisher-origin route is the only mechanism that grants preview authority inside the placement's scope; an agent's own rendering_origin description does not.").optional(), "format_options": z.array(z.union([z.object({ "format_option_id": z.string().describe("Matches a `format_option_id` in the file's top-level `formats[]`."), "locale_policy": z.object({ "accepted_language_ranges": z.array(z.string().regex(new RegExp("^(?:[a-z]{2,8}(?:-[A-Z][a-z]{3})?(?:-(?:[A-Z]{2}|[0-9]{3}))?(?:-(?:[a-z0-9]{5,8}|[0-9][a-z0-9]{3}))*(?:-[0-9a-wy-z](?:-[a-z0-9]{2,8})+)*(?:-x(?:-[a-z0-9]{1,8})+)?|x(?:-[a-z0-9]{1,8})+)$")).min(2).max(63).describe("A well-formed BCP 47 language tag used by AdCP only as language identity. Script and region may refine that identity; other valid BCP 47 subtags remain part of tag matching but do not make this a general locale-settings object. It does not determine currency, time zone, number/date formatting, market, or legal jurisdiction. The AdCP canonical wire profile requires lower-case language and variants, title-case script, and upper-case region (for example `en-US`, `zh-Hant-TW`, or `x-private`). RFC 5646 comparisons are case-insensitive and its case regularization is optional; AdCP intentionally requires this stricter single wire spelling and receivers MUST reject differently cased tags rather than silently normalizing them. The schema pattern enforces the AdCP casing profile and extension structure for commonly used tags; conforming receivers additionally validate the complete RFC 5646 grammar and registry rules. Every new AdCP field carrying BCP 47 language identity or a concrete language range MUST reference this schema instead of declaring independent string constraints.")).max(50).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Concrete canonical BCP 47 language ranges accepted by this format option. RFC 4647 Basic Filtering is directional: seller range fr accepts variant fr-CA, but seller range fr-CA does not accept variant fr or fr-FR. Use zxx explicitly for language-neutral creative; und means unknown and is not a wildcard.") }).catchall(z.any()).describe("Optional placement-local locale policy. When the resolved top-level format declares a policy, every placement range must be contained by one of its ranges; otherwise this introduces a narrowing of the unconstrained format. The resolved effective route remains canonical-only.").optional() }).catchall(z.any()).describe("Reference an entry in the file's top-level `formats[]` by `format_option_id`. Resolved at validation time. additionalProperties: true so placement-local fields (display_name, etc.) carry through without forcing a full inline declaration."), z.object({ "format_option_id": z.string().describe("Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'.").optional(), "publisher_domain": z.string().regex(new RegExp("^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$")).describe("Namespace for `format_option_id` when this declaration references or narrows a publisher-declared format option from that publisher's adagents.json top-level `formats[]`. Product-local options omit this field and are selected by `format_option_id` within the target product.").optional(), "display_name": z.string().describe("Optional seller-controlled human-readable label for this format declaration. Used by buyer dashboards, catalog UIs, and reporting surfaces to show a seller's own naming ('Homepage Takeover', 'Branded Canvas', 'Reels Premium Video') rather than the raw `format_kind` or `format_option_id`. Has no machine semantics — buyer agents route on `format_kind` and `format_option_id`; `display_name` is purely for human presentation. Freeform; no enumeration. Sellers SHOULD keep it stable once published to avoid dashboard churn.").optional(), "sample_render_url": z.string().url().regex(new RegExp("^https://")).describe("Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.").optional(), "applies_to_channels": z.array(z.enum(["display","olv","social","search","ctv","linear_tv","radio","streaming_audio","podcast","dooh","ooh","print","cinema","email","gaming","retail_media","influencer","affiliate","product_placement","sponsored_intelligence"]).describe("Standardized advertising media channels describing how buyers allocate budget. Channels are planning abstractions, not technical substrates. See the Media Channel Taxonomy specification for detailed definitions.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Optional subset of the parent product's `channels` to which this declaration applies. When omitted, the declaration applies to ALL channels declared on the product. Lets a multi-channel product (e.g., `channels: ['display', 'video']`) carry distinct format_options per channel — `format_options: [{format_kind: 'image', applies_to_channels: ['display']}, {format_kind: 'video_hosted', applies_to_channels: ['video']}]`. Buyers ship channel-appropriate manifests per `applies_to_channels`.").optional(), "seller_preference": z.enum(["preferred","accepted","discouraged"]).describe("Optional soft routing hint *within* a product's accepted set of formats — NOT an enforcement axis. `preferred` — seller actively recommends this format (often because of measurement, viewability, or render-quality differences); `accepted` — supported on equal footing with other format_options (default when omitted); `discouraged` — supported but suboptimal (e.g., legacy 3p-tag where the seller would prefer html5 for OM-SDK coverage). Buyer agents picking between format_options SHOULD respect seller preferences when their own constraints don't override.\n\n**Not an enforcement axis (normative).** `seller_preference` does NOT carry the meaning of 'this format won't work / required-only'. That case is structural: `format_options[]` IS the closed set of accepted formats; anything outside the list is rejected at `create_media_buy` regardless of preference. A seller that accepts only one format lists exactly that one entry — the structural fact does the enforcement work, no enum value needed. There is intentionally no `required` value; preference is bounded to *ranking within the already-accepted set*, not gating into it.").optional(), "locale_policy": z.object({ "accepted_language_ranges": z.array(z.string().regex(new RegExp("^(?:[a-z]{2,8}(?:-[A-Z][a-z]{3})?(?:-(?:[A-Z]{2}|[0-9]{3}))?(?:-(?:[a-z0-9]{5,8}|[0-9][a-z0-9]{3}))*(?:-[0-9a-wy-z](?:-[a-z0-9]{2,8})+)*(?:-x(?:-[a-z0-9]{1,8})+)?|x(?:-[a-z0-9]{1,8})+)$")).min(2).max(63).describe("A well-formed BCP 47 language tag used by AdCP only as language identity. Script and region may refine that identity; other valid BCP 47 subtags remain part of tag matching but do not make this a general locale-settings object. It does not determine currency, time zone, number/date formatting, market, or legal jurisdiction. The AdCP canonical wire profile requires lower-case language and variants, title-case script, and upper-case region (for example `en-US`, `zh-Hant-TW`, or `x-private`). RFC 5646 comparisons are case-insensitive and its case regularization is optional; AdCP intentionally requires this stricter single wire spelling and receivers MUST reject differently cased tags rather than silently normalizing them. The schema pattern enforces the AdCP casing profile and extension structure for commonly used tags; conforming receivers additionally validate the complete RFC 5646 grammar and registry rules. Every new AdCP field carrying BCP 47 language identity or a concrete language range MUST reference this schema instead of declaring independent string constraints.")).max(50).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Concrete canonical BCP 47 language ranges accepted by this format option. RFC 4647 Basic Filtering is directional: seller range fr accepts variant fr-CA, but seller range fr-CA does not accept variant fr or fr-FR. Use zxx explicitly for language-neutral creative; und means unknown and is not a wildcard.") }).catchall(z.any()).describe("Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.").optional(), "canonical_formats_only": z.boolean().describe("When true, this format declaration has no clean v1 projection and SDKs MUST NOT synthesize a v1 `format_id` for it. Buyers reading the product on the v1 wire path see this declaration absent from `format_ids`; only v2-aware buyers (reading `format_options`) discover it. Set explicitly for `format_kind: \"custom\"` declarations (no canonical exists in v1 to project onto) and for declarations whose canonical/parameter shape cannot round-trip through a v1 named format without semantic loss. The protocol does NOT mint synthetic v1 format_ids for unmappable declarations — the alternative (an `aao-synth/*` namespace populated automatically) was considered and rejected because adopters would index on synthetic IDs that have no stable identity. Producers SHOULD set `canonical_formats_only: true` rather than omit the declaration from `format_options` — explicit v2-only is more useful than silent absence.").default(false), "experimental": z.boolean().describe("When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum.").default(false), "format_shape": z.string().describe("REQUIRED when `format_kind: \"custom\"`; otherwise MUST be absent. Recognized global pattern this custom shape is an instance of, drawn from the [format-shape vocabulary registry](/schemas/core/format-shape-vocabulary.json) (`multi_placement_takeover`, `roadblock`, `branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values valid (validators MAY soft-warn) — adopters CAN ship a shape that isn't yet in the registry. Adding entries is a vocabulary PR. Once a `format_shape` entry sees 2+ adopters with substantively similar `format_schema` content for 90+ days, the working group promotes it to a first-class canonical.").optional(), "v1_format_ref": z.array(z.object({ "agent_url": z.string().url().describe("URL of the agent that defines this format (e.g., 'https://creative.adcontextprotocol.org' for standard formats, or 'https://publisher.com/.well-known/adcp/sales' for custom formats). Callers comparing two `format-id` values MUST canonicalize `agent_url` per the AdCP URL canonicalization rules before treating two formats as the same. See docs/reference/url-canonicalization."), "id": z.string().regex(new RegExp("^[a-zA-Z0-9_-]+$")).describe("Format identifier within the agent's namespace (e.g., 'display_static', 'video_hosted', 'audio_standard'). When used alone, references a template format. When combined with dimension/duration fields, creates a parameterized format ID for a specific variant."), "width": z.number().int().gte(1).describe("Width in pixels for visual formats. When specified, height must also be specified. Both fields together create a parameterized format ID for dimension-specific variants.").optional(), "height": z.number().int().gte(1).describe("Height in pixels for visual formats. When specified, width must also be specified. Both fields together create a parameterized format ID for dimension-specific variants.").optional(), "duration_ms": z.number().gte(1).describe("Duration in milliseconds for time-based formats (video, audio). When specified, creates a parameterized format ID. Omit to reference a template format without parameters.").optional(), "pixel_ratio": z.number().gt(0).describe("Required intrinsic-pixel density for a parameterized visual format, expressed as intrinsic pixels per logical pixel. Requires `width` and `height`. Example: `{id: \"display_image\", width: 300, height: 250, pixel_ratio: 2}` identifies a 300×250 logical render supplied by a 600×500 image. Omit for the backward-compatible 1x variant.").optional() }).catchall(z.any()).describe("**DEPRECATED in 3.2.** Legacy named-format reference retained for 3.x compatibility. This JSON object — never a plain string — identifies a format by its declaring agent and local slug, and may carry legacy logical dimensions, pixel ratio, or duration parameters. New products and manifests use canonical `format_options`, `format_kind`, and `format_option_ref`; creative-agent build routing uses `creative.supported_formats[].capability_id`.")).describe("Authoritative v2 → v1 link, expressed as an array of one or more v1 `format_id` ({agent_url, id}) values. Each entry asserts that this canonical-formats declaration IS the same underlying format as the referenced v1 named format. Always an array (single-ref is `[{...}]`) so the multi-size case below has a clean wire shape — adopters surveyed in the SDK implementor review pushed for this over the lossy single-ref form.\n\nThe v2 declaration's `params` MUST narrow (be compatible with) each referenced v1 format's `requirements` — see the 'Narrows — formal definition' section in canonical-formats.mdx. SDKs comparing dual-emitted shapes (`Product.format_ids[]` ⊇ entries from `v1_format_ref` AND `Product.format_options[]` carrying this declaration) treat the link as the authoritative pairing and run the narrowing check between this declaration and EACH referenced v1 format file's `requirements`.\n\n**Multi-size fan-out (normative).** When the declaration carries `params.sizes: [{w,h}, ...]` (multi-size flexible slot), sellers SHOULD carry one `v1_format_ref[]` entry per size, each pointing at the per-size v1 named format in the AAO catalog. Example: a multi-size image declaration with `sizes: [300x250, 728x90, 970x250]` SHOULD carry `v1_format_ref: [{aao, display_300x250_image}, {aao, display_728x90_image}, {aao, display_970x250_image}]`. v1-only buyers then see the product on all three sizes via the `format_ids[]` dual-emission. When `v1_format_ref[]` count < `sizes[]` count, SDKs MUST emit `FORMAT_DECLARATION_V1_LOSSY_MULTI_SIZE` on the response `errors[]` (advisory, alongside the partial-coverage v1 emit — NOT in place of it). SDKs MAY (non-normative) fan out automatically by catalog lookup when `v1_format_ref[]` has length 1 and `sizes[]` has length N — opt-in, requires catalog access; sellers asserting refs is the source of truth.\n\nMutually exclusive with `canonical_formats_only: true` — a declaration can EITHER assert no v1 projection (`canonical_formats_only: true`) OR link to v1 named formats (`v1_format_ref[]`), never both. When neither is present, SDKs fall back to the resolution order in `v1-canonical-mapping.json` (seller's explicit `canonical` field on the v1 file → registry glob → structural match → fail-closed).\n\nThis is the v2-side authoritative replacement for the v1-side `canonical_parameters` field on `format.json` (which is deprecated for 3.1, removed at 4.0). Sellers SHOULD prefer authoring v2 declarations with `v1_format_ref[]` over mirroring the v2 shape onto v1 files via `canonical_parameters`; the directional link (v2 declaration → v1 identifiers) is the same fact without the parallel-shape drift surface.\n\n**AAO-hosted convention (normative).** For IAB-standard formats (image dimensions, VAST/DAAST tags, standard third-party tags, HTML5 banner bundles), sellers SHOULD point each `v1_format_ref[].agent_url` at the AAO-hosted canonical agent URL `https://creative.adcontextprotocol.org` and use the registry-published id (e.g., `display_300x250_image`, `video_vast_30s`, `audio_standard_30s`, `display_300x250_html`, `display_js`). This converges the v1-wire namespace: every seller's IAB MREC points at the same `{agent_url, id}` pair, so v1-only buyers' allowlists work uniformly. Without this convention, every publisher's 300x250 ships with a different `v1_format_ref` (theirs vs nytimes.example vs cnn.example vs …) and the v1 wire fragments into per-publisher namespaces — exactly what canonical-formats was designed to eliminate.\n\nFor platform-specific formats (Meta Reels, TikTok Spark, Snap Spotlight, etc.), each `v1_format_ref[].agent_url` SHOULD point at the platform's own agent_url when the platform has adopted AdCP and publishes its own `adagents.json` with `formats[]`. When the platform has NOT adopted AdCP, sellers SHOULD point at the AAO community-registry mirror — `https://creative.adcontextprotocol.org/translated/` + `id: ` (e.g., `https://creative.adcontextprotocol.org/translated/meta` + `id: meta_reels`). This keeps the v1 namespace converged across all sellers selling that platform's inventory until the platform owns its own adagents.json.\n\n**Platform-adoption cutover (normative).** When a platform adopts AdCP and publishes its own adagents.json, sellers MUST update `v1_format_ref[].agent_url` to the platform's adopted agent_url in the same minor release as the AAO mirror entry's `superseded_by` field goes live (see `static/schemas/source/adagents.json#superseded_by`). The AAO mirror entry SHOULD continue serving for ≥1 minor release after `superseded_by` is set, returning an advisory 'superseded' marker so v1 buyer allowlists keyed on the mirror URL get an explicit signal rather than a silent break. **Identity-confusion note**: the mirror URL is *format-shape namespace*, NOT seller identity. Inventory authorization always flows from `authorized_agents[]` + publisher signing keys; a buyer matching `v1_format_ref[].agent_url` against an allowlist is matching format-shape provenance, not seller identity.\n\n**Mirror domain migration (3.1).** Earlier drafts used `https://mirror.adcontextprotocol.org/translated/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — sibling content under the AAO catalog domain we already host. Adopters who hardcoded the earlier mirror URL MUST migrate to the new path; the canonical-formats.mdx migration section documents the move. No transitional redirect is currently published (the earlier subdomain was never provisioned).\n\nFor seller-bespoke formats (a publisher's `acme_homepage_takeover` that doesn't fit IAB conventions), each `v1_format_ref[].agent_url` is the seller's own agent_url and the id is seller-namespaced. These won't appear in `v1-canonical-mapping.json`'s registry; they're seller-asserted only.").optional(), "format_schema": z.object({ "uri": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL identifying the extension. `https://` is mandatory — `http://`, `file://`, `data:`, and other schemes are rejected at the schema layer (defense-in-depth on top of the fetch-contract normative rules). The URI base is the owning agent's URL; the path identifies the extension within that agent. Example: 'https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel'. The full fetch contract — SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds — is documented on `product-format-declaration.json#format_schema` and applies to ALL fetches of this reference shape regardless of whether the field is named `format_schema` (load-bearing for validation) or `platform_extensions` (informational); the *transport* rules are identical, only the *consumption* semantics differ."), "digest": z.string().regex(new RegExp("^sha256:[a-f0-9]{64}$")).describe("SHA-256 content digest of the extension definition (sha256:). Used to detect drift — if the agent revises the extension, the digest changes and cached definitions become invalid.") }).catchall(z.any()).describe("REQUIRED when `format_kind: \"custom\"`; otherwise MUST be absent. URI+digest reference to a fetchable schema describing this custom shape's actual `params` and `slots`. Same hosting model as `platform_extensions`: open-ecosystem publishers host the artifact at the canonical URI on their subdomain; closed-platform / walled-garden shapes resolve through the AAO mirror at `https://creative.adcontextprotocol.org/translated/...`. Buyer agents fetch by `uri@digest` (immutable per digest, aggressive caching, `Cache-Control: public, max-age=31536000, immutable`), validate `params` and `slots` against the fetched schema, and reason about manifests structurally — same mechanic as platform_extensions but at the format-structure level. Without `format_schema`, custom shapes would be opaque to buyer agents and the protocol would regress to per-seller integration code; that's why the schema is required, not optional.\n\n**Fetch contract (normative)** — `format_schema` is load-bearing for validation (unlike `platform_extensions`, which is informational on the *consumption* side). The *transport* rules below apply identically to BOTH fields — any SDK fetching a `platform-extension-ref.json` URI MUST apply this contract regardless of whether the field name is `format_schema` or `platform_extensions`. A shared SDK fetch path that drops to the weakest bar undermines `format_schema`'s hardening. The consumption distinction (load-bearing vs informational) is about *what the body means*; the transport distinction is `https`-and-allowlisted regardless.\n\n- **Transport**: `https` only. Buyers MUST reject `http://`, `file://`, `data:`, and any non-`https` scheme. The URI MUST resolve to a JSON document that is itself a valid JSON Schema (Draft 07 or 2020-12; producers MUST declare `$schema`).\n- **SSRF protection**: buyers MUST resolve the URI hostname and reject if any resolved address is in RFC 1918 private space (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`), loopback (`127.0.0.0/8`, `::1`), link-local (`169.254.0.0/16`, `fe80::/10`), CGNAT (`100.64.0.0/10`), or any RFC 6761 special-use name (`.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. Buyers MUST pin the connection to the resolved IP (or re-resolve and re-validate the allowlist per request) to defeat DNS rebinding.\n- **HTTP redirects**: MUST be disabled. If a follow is implemented at all, the redirect target MUST pass the same scheme + SSRF + allowlist checks; otherwise the fetch hard-fails. Open redirects on same-origin paths are otherwise a free SSRF primitive.\n- **Response size cap**: response body MUST be capped at 1 MiB. Enforce during streaming, not after full buffering. Over-cap hard-fails identically to digest mismatch.\n- **Timeout**: SDKs SHOULD apply a fetch timeout ≤5 seconds. Timeout SHOULD be treated identically to an HTTP 5xx response (transient — retry policy at the SDK's discretion; on persistent failure surface as unresolved and skip the declaration for this session).\n- **Digest verification**: SHA-256 of the response body MUST equal `digest`. **Digest mismatch is a hard fail** — the buyer MUST treat the format declaration as unresolvable and MUST NOT validate manifests against the mismatched body. A divergent digest is either a malicious substitution or producer error; either way, falling back to the un-verified body breaks the trust model. Digest format: `sha256:` prefix + 64 lowercase hex characters. Cache key is `uri@digest`; digest mismatch MUST NOT be cached as a negative result keyed on `uri` alone (defeats CDN-flap recovery), and MUST be distinguishable in telemetry from network 5xx / 404 (sustained mismatch is a substitution-attack signal, not a flap).\n- **Sandboxing of `$ref`**: fetched schemas MAY use `$ref`. Buyers MUST resolve `$ref` only to URIs that are (a) same-origin as the parent `format_schema.uri` after RFC 3986 §6 normalization (lowercase scheme + host, strip default port, normalize path dot-segments, no userinfo component), OR (b) hosted under the AAO catalog domain (`https://creative.adcontextprotocol.org/...`), OR (c) intra-document JSON Pointer refs (`#/...`) bounded to the parent document's parsed tree. Cross-origin `$ref` to arbitrary URIs MUST be rejected. `$ref: file://...` MUST be rejected unconditionally. Transitive `$ref` chains MUST be bounded at depth ≤8 AND `$ref` count ≤256 across the resolved tree (depth 8 with breadth 100 per level is 10^16 nodes — depth alone is not enough). Publishers SHOULD inline rather than $ref where possible.\n- **Schema-compile bounds (DoS protection)**: validators MUST bound CPU/memory on fetched schemas. Recommended: compiled-schema keyword count ≤10 000, `pattern` regexes evaluated with a non-backtracking engine (re2) OR under a per-pattern timeout, per-manifest validation budget ≤250 ms (exceeded budget → treat manifest as invalid, surface telemetry signal). Without these, a 'valid' schema with catastrophic regex backtracking or exponential `allOf`/`anyOf` expansion pins a CPU forever.\n- **Cache**: buyers cache fetched schemas by `uri@digest` and treat them as immutable (the same hosting contract as `platform_extensions`). On `404`, network partition, or persistent fetch failure, buyers SHOULD degrade gracefully (treat the declaration as unresolved, skip it for the current `get_products` response, surface via `errors[]` with the relevant code) rather than failing the entire session.\n- **Schema-not-valid handling**: if the fetched body parses as JSON but is not a valid JSON Schema, the buyer MUST treat the declaration as unresolvable (same as digest mismatch) and surface via `errors[]`. Validators MUST NOT attempt partial validation against an invalid schema.\n- **AAO catalog trust**: `https://creative.adcontextprotocol.org/*` is a single trust anchor in the same-origin allowlist; compromise of the catalog domain or its CA compromises every buyer agent. Catalog-served bodies MUST be digest-pinned identically to origin fetches (the digest is on the *parent* `format_schema.uri@digest`, not on the catalog response). Future hardening (signed bodies, transparency log) is tracked separately.").optional() }).and(z.union([z.object({ "format_kind": z.literal("image"), "params": z.intersection(z.object({ "experimental": z.boolean().describe("When true, this canonical or seller narrowing may not work as declared. Adopters SHOULD preflight it with validate_input or in a sandbox and SHOULD NOT route production budget without testing; experimental status never makes the deprecated v1 path preferable. Drivers include unsettled spec shape, an adopter runtime gap, and custom shapes awaiting promotion. This replaces the earlier status plus runtime_status axes. Sellers SHOULD set experimental whenever a canonical or declaration is not production-ready.").default(false), "deprecated": z.boolean().describe("When true, this canonical (or a seller's specific narrowing of it) is going away. Existing adopters are supported through the deprecation cycle; new adoption is discouraged. Pair with `migration_target_version` to indicate when the canonical is expected to be removed. Distinct from `experimental`: an experimental canonical may stabilize and stop being experimental; a deprecated canonical is on a sunset path.").default(false), "v1_translatable": z.boolean().describe("Whether this canonical has any v1 named-format equivalent. `true` (default) — the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1→v2 projection via `v1-canonical-mapping.json` is meaningful. `false` — the canonical is inherently new in v2 and has no v1 form; v1's `list_creative_formats` couldn't express it because the underlying concept (algorithmic surface composition, AI-surface mentions, retail-media catalog placements, multi-card carousels) didn't exist as a v1 named-format archetype.\n\nLets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The 4 inherently-v2 canonicals at 3.1 GA: `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`.").default(true), "since_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version that introduced this canonical (e.g., '3.1', '3.2'). Lets adopters reason about minimum protocol version requirements when consuming a format declaration. Patch precision is intentionally rejected — canonicals are introduced at minor-version boundaries.").optional(), "migration_target_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version by which the working group expects this canonical to stabilize, surface a breaking revision, or (when `deprecated: true`) be removed. Patch precision is intentionally rejected — canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown').").optional(), "composition_model": z.enum(["deterministic","algorithmic"]).describe("Whether the surface composes deterministically (buyer can predict per-slot rendering — sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing — responsive_creative, agent_placement).").optional(), "provenance_required": z.boolean().describe("When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent.").optional(), "platform_extensions": z.array(z.object({ "uri": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL identifying the extension. `https://` is mandatory — `http://`, `file://`, `data:`, and other schemes are rejected at the schema layer (defense-in-depth on top of the fetch-contract normative rules). The URI base is the owning agent's URL; the path identifies the extension within that agent. Example: 'https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel'. The full fetch contract — SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds — is documented on `product-format-declaration.json#format_schema` and applies to ALL fetches of this reference shape regardless of whether the field is named `format_schema` (load-bearing for validation) or `platform_extensions` (informational); the *transport* rules are identical, only the *consumption* semantics differ."), "digest": z.string().regex(new RegExp("^sha256:[a-f0-9]{64}$")).describe("SHA-256 content digest of the extension definition (sha256:). Used to detect drift — if the agent revises the extension, the digest changes and cached definitions become invalid.") }).catchall(z.any()).describe("Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` — divergent digests in one response indicate producer-side error (e.g., concurrent extension revision mid-render). Buyers encountering divergent digests for the same URI MUST fail closed: treat all references to that URI as unresolved and surface a validation error rather than picking one branch silently. **Across responses**, digest divergence is normal — extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.")).describe("Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\n\n**Collision precedence (normative).** When two or more `platform_extensions[]` entries on the same declaration extend the same target (e.g., both extend `tracking`) with overlapping field names, **array order is authoritative — later entries override earlier ones on a per-field basis** (last-in-array-wins). SDKs MUST surface the overlap via the `errors[]` array on the `get_products` response with a structured code (`FORMAT_DECLARATION_DIVERGENT` is appropriate when the overlap appears across dual-emitted shapes; a producer-self-emitted overlap on a single declaration SHOULD use the same code with `error.details: { collision_kind: \"platform_extension_field\", target, overlapping_fields, winning_extension_uri }`). Producers SHOULD avoid the collision by emitting one extension per target or by partitioning fields across extensions; the deterministic precedence is for last-resort consistency across SDK implementations, not a sanctioned merging strategy.").optional(), "synthesis_nondeterministic": z.boolean().describe("When true, the format's production pipeline is genuinely nondeterministic — the platform cannot guarantee that synthesis from a given input set produces in-spec output. Veo / Sora / Runway-class generative video, and other AI-synthesis flows where output dimensions, duration, or quality vary per run. Implies a different validation contract: predictive `validate_input` is impossible; the platform's own post-synthesis QA loop applies; if the QA loop exhausts without producing a valid artifact, `build_creative` returns task_failed with a synthesis_failed reason. Distinct from `composition_model` (which describes how the surface composes per-slot rendering, not whether synthesis is deterministic). When false or absent, the format's production is predictable enough that `validate_input` can predict output properties from input properties.\n\n**Compatibility with `asset_source` / `item_production_model`**: `synthesis_nondeterministic: true` MAY pair with any of `seller_pre_rendered_from_brief`, `seller_human_designed`, or `agent_synthesized` (the QA loop is concept-level, not source-specific — 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.").default(false), "slots": z.array(z.object({ "asset_group_id": z.string().describe("Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings."), "asset_type": z.enum(["image","video","audio","text","markdown","url","html","css","javascript","vast","daast","webhook","brief","catalog","published_post","zip","card","object","pixel_tracker","vast_tracker","daast_tracker"]).describe("Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `published_post` is an existing-post reference asset, not uploaded media bytes and not a catalog row. `card` is the multi-card carousel element type (see card-asset.json). `pixel_tracker` / `vast_tracker` / `daast_tracker` are the renderer-fired measurement-tracker primitives — see `/schemas/core/assets/pixel-tracker-asset.json` and the VAST / DAAST tracker schemas. `object` is a last-resort fallback for structured non-asset inputs that don't fit any primitive asset_type — prefer specific types whenever possible."), "required": z.boolean().describe("Whether this slot is required for a valid manifest.").default(false), "min": z.number().int().gte(0).describe("Minimum count for repeatable / pool slots.").optional(), "max": z.number().int().gte(1).describe("Maximum count for repeatable / pool slots.").optional(), "max_chars": z.number().int().gte(1).describe("Per-slot character limit. Valid only when `asset_type` is `text`, `markdown`, or `brief`. Mutually exclusive with `max_size_kb` (which applies to binary asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "max_size_kb": z.number().int().gte(1).describe("Per-slot file size limit in kilobytes. Valid only when `asset_type` is `image`, `video`, `audio`, or `zip`. Mutually exclusive with `max_chars` (which applies to text asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Accepted intrinsic-pixel densities for this image-bearing slot. Valid when `asset_type` is `image`, and on a `card` slot where it constrains each card's image media (video media is unaffected). This makes density available to every canonical carrying image assets (native, carousel, responsive, companion images, and image itself), not only `format_kind: image`. When the image canonical also declares top-level `params.pixel_ratios`, the effective set is the intersection; an empty intersection is invalid. One matching asset satisfies the slot unless `required_pixel_ratios` requires rendition coverage.").optional(), "required_pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Required density coverage for an image rendition set. Valid only when `asset_type` is `image` and `pixel_ratios` is also declared. Every value MUST appear in the effective accepted set after intersecting any top-level image `params.pixel_ratios`, and the manifest slot value MUST be an array containing exactly one matching image rendition for each required ratio. Other accepted ratios remain optional. For example, `pixel_ratios: [1, 1.5, 2]` with `required_pixel_ratios: [1, 2]` requires the 1x and 2x renditions while making 1.5x optional. SDKs enforce intersection, subset, coverage, and duplicate-ratio rules because JSON Schema draft-07 cannot express them generically.").optional(), "logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("When `asset_group_id` is `logo`, renderer-facing brand.json logo slots acceptable for this format slot. Producers selecting from brand.json SHOULD prefer `logos[]` entries whose `slots[]` intersects this list, then apply `visual_guidelines.logo_usage_rules[]`.").optional(), "required_logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Subset of `logo_slots` for which this format expects explicit logo coverage. A manifest or brand-derived logo pool SHOULD include at least one usable logo for each required slot; if coverage is missing, builders SHOULD surface a validation warning or approval mapping instead of guessing from prose.").optional(), "description": z.string().describe("Human-readable description of what the slot expects from the buyer.").optional(), "consumed_for_production": z.boolean().describe("Dispatch hint for `build_creative` and v1↔v2 wire translators: when `true`, the slot's value is consumed as INPUT to a production step (host-read script, brief copy fed to generative synthesis, catalog feed driving per-SKU rendering) and is not rendered verbatim. When `false` (default), the slot's value is rendered verbatim on the placement (image bytes, video file, display tag).\n\nMotivates the v1↔v2 dispatch table: pre-v2 buyers shipped production-consumed inputs separately in a `inputs` map on the build_creative request; v2 collapses inputs and rendered assets into a single `assets` map keyed by `asset_group_id`. SDK translators between v1 and v2 use this flag per canonical to know which assets in the v2 manifest map back to v1 `inputs` vs v1 `assets`. Without the per-slot flag the dispatch table lives in adopter code and every SDK gets it slightly different.\n\nProducers SHOULD set this explicitly on slots whose consumption pattern isn't obvious (host-read scripts on `audio_hosted`, briefs on generative `video_hosted`, catalog feeds on `sponsored_placement`). For canonicals where every slot is render-verbatim (`image`, `display_tag`, `video_vast`), the default `false` is sufficient and the flag MAY be omitted.").default(false) }).catchall(z.any()).and(z.intersection(z.intersection(z.any(), z.intersection(z.any(), z.any())), z.intersection(z.any(), z.intersection(z.any(), z.any()))))).describe("Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.").optional(), "required_connections": z.array(z.object({ "provider": z.string().describe("Stable provider or platform namespace, preferably lowercase. Examples: `social.example`, `shortvideo.example`, or a seller-defined namespace. Omit only when the requirement is provider-agnostic, or when an `authorization_url` fully routes the human to the correct provider-specific connection flow.").optional(), "connection_type": z.enum(["advertiser_account","publisher_identity","post_authorization"]).describe("Kind of downstream connection required. `advertiser_account` is the platform account used to buy/manage ads. `publisher_identity` is the creator, page, channel, organization, or profile that owns source posts. `post_authorization` is a post-scoped grant when the platform authorizes individual posts instead of, or in addition to, the owning identity."), "required_for": z.array(z.string().min(1)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Concrete AdCP protocol operation names that require this downstream connection. Sellers SHOULD include this in product declarations when the requirement is known ahead of time, and in AUTHORIZATION_REQUIRED details when it explains the failed operation. Prefer specific operation names such as `list_creatives`, `sync_creatives`, `create_media_buy`, `get_media_buy_delivery`, or `get_creative_delivery` over broad category labels such as `reporting`.").optional(), "scope": z.enum(["account","identity","post","unknown"]).describe("Granularity of the downstream grant.").optional(), "status": z.enum(["connected","missing","pending","expired","revoked","not_required","unknown"]).describe("Current seller-observed state for this downstream connection when known. Product declarations MAY omit status or use `unknown`; AUTHORIZATION_REQUIRED details SHOULD use `missing`, `expired`, or `revoked` for the connection that blocked the call.").optional(), "connection_id": z.string().describe("Seller-defined identifier for an already-created downstream connection. Omit when no connection exists yet or when exposing it would leak platform/account state.").optional(), "resource_ref": z.object({ "platform_account_id": z.string().describe("Provider-native advertiser or business account id, when safe to disclose.").optional(), "identity_id": z.string().describe("Provider-native creator, page, channel, organization, or profile id, when safe to disclose.").optional(), "handle": z.string().describe("Provider-native public handle for the owning identity, when available.").optional(), "profile_url": z.string().url().describe("Public URL for the owning identity, when available.").optional(), "post_id": z.string().describe("Provider-native post id, when the grant is post-scoped or the failed request referenced a specific post.").optional(), "post_url": z.string().url().describe("Public URL for the referenced post, when available.").optional() }).catchall(z.any()).describe("Optional opaque provider-native resource hint, such as a platform account id, profile URL, handle, channel id, post id, or post URL. This is a hint for routing authorization, not proof that authorization exists.").optional(), "authorization_url": z.string().url().describe("Seller-hosted or provider-hosted URL where a human can complete or restore this downstream connection.").optional(), "authorization_instructions": z.string().describe("Human-readable instructions for completing or restoring this downstream connection.").optional(), "expires_at": z.string().datetime().describe("Expiration time for the downstream grant, when known.").optional() }).catchall(z.any()).and(z.any()).describe("A seller/platform-side connection or grant required by a product, format, or request. This is not the AdCP caller credential: the AdCP request is still authenticated once, and the seller uses these stored downstream connections to call a platform or service on the buyer's behalf. Use this shape for platforms that require more than one downstream grant, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.")).describe("Downstream platform connections or grants required to use this format declaration. These are in addition to the single AdCP caller credential. Use this when a platform product requires multiple downstream grants, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.").optional(), "reference_mutability": z.enum(["immutable_snapshot","mutable_requires_reapproval","mutable_auto_recheck"]).describe("Policy for formats whose `slots` accept a `published_post` reference. `immutable_snapshot`: seller snapshots the referenced post at approval and later source changes do not change the served creative. `mutable_requires_reapproval`: the source post may change and material changes require review before continued serving. `mutable_auto_recheck`: the source post may change and the seller continuously or periodically rechecks authorization/policy without requiring buyer resubmission. Omit when the format has no `published_post` slot.").optional(), "production_window_business_days": z.number().int().gte(0).describe("Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative).").optional() }).catchall(z.any()).describe("Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints)."), z.union([z.any().refine((value) => !z.union([z.any(), z.any(), z.any(), z.any(), z.any()]).safeParse(value).success, "Invalid input: Should NOT be valid against schema"), z.any().refine((value) => !z.union([z.any(), z.any(), z.any(), z.any(), z.any(), z.any()]).safeParse(value).success, "Invalid input: Should NOT be valid against schema"), z.union([z.any(), z.any(), z.any(), z.any()]), z.any().refine((value) => !z.union([z.any(), z.any(), z.any(), z.any(), z.any(), z.any(), z.any()]).safeParse(value).success, "Invalid input: Should NOT be valid against schema")]).describe("Exactly one of: (a) fixed (`width` + `height` both set), (b) multi-size (`sizes` set), (c) responsive (any of `min_width`/`max_width`/`min_height`/`max_height` set), (d) none (no size constraint declared — accepts any dimensions). Combining modes (e.g., `width` + `sizes`) is rejected at schema layer; same rule on `html5` and `display_tag` canonicals.")).describe("Static image creative format. Slots: `image_main` (image asset, file or hosted URL), optional `headline` (text), `body_text` (text), `cta` (text/enum), `landing_page_url` (url). Tracking model: impression pixel + click URL via universal_macros, with optional viewability pixel. Distinct from `html5` (interactive bundles) and `display_tag` (third-party served). AR/dimensions narrow to specific sizes via product parameters — covers IAB display sizes (300x250, 728x90, 970x250, etc.) without a separate iab_size enum.") }), z.object({ "format_kind": z.literal("html5"), "params": z.intersection(z.object({ "experimental": z.boolean().describe("When true, this canonical or seller narrowing may not work as declared. Adopters SHOULD preflight it with validate_input or in a sandbox and SHOULD NOT route production budget without testing; experimental status never makes the deprecated v1 path preferable. Drivers include unsettled spec shape, an adopter runtime gap, and custom shapes awaiting promotion. This replaces the earlier status plus runtime_status axes. Sellers SHOULD set experimental whenever a canonical or declaration is not production-ready.").default(false), "deprecated": z.boolean().describe("When true, this canonical (or a seller's specific narrowing of it) is going away. Existing adopters are supported through the deprecation cycle; new adoption is discouraged. Pair with `migration_target_version` to indicate when the canonical is expected to be removed. Distinct from `experimental`: an experimental canonical may stabilize and stop being experimental; a deprecated canonical is on a sunset path.").default(false), "v1_translatable": z.boolean().describe("Whether this canonical has any v1 named-format equivalent. `true` (default) — the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1→v2 projection via `v1-canonical-mapping.json` is meaningful. `false` — the canonical is inherently new in v2 and has no v1 form; v1's `list_creative_formats` couldn't express it because the underlying concept (algorithmic surface composition, AI-surface mentions, retail-media catalog placements, multi-card carousels) didn't exist as a v1 named-format archetype.\n\nLets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The 4 inherently-v2 canonicals at 3.1 GA: `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`.").default(true), "since_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version that introduced this canonical (e.g., '3.1', '3.2'). Lets adopters reason about minimum protocol version requirements when consuming a format declaration. Patch precision is intentionally rejected — canonicals are introduced at minor-version boundaries.").optional(), "migration_target_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version by which the working group expects this canonical to stabilize, surface a breaking revision, or (when `deprecated: true`) be removed. Patch precision is intentionally rejected — canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown').").optional(), "composition_model": z.enum(["deterministic","algorithmic"]).describe("Whether the surface composes deterministically (buyer can predict per-slot rendering — sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing — responsive_creative, agent_placement).").optional(), "provenance_required": z.boolean().describe("When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent.").optional(), "platform_extensions": z.array(z.object({ "uri": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL identifying the extension. `https://` is mandatory — `http://`, `file://`, `data:`, and other schemes are rejected at the schema layer (defense-in-depth on top of the fetch-contract normative rules). The URI base is the owning agent's URL; the path identifies the extension within that agent. Example: 'https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel'. The full fetch contract — SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds — is documented on `product-format-declaration.json#format_schema` and applies to ALL fetches of this reference shape regardless of whether the field is named `format_schema` (load-bearing for validation) or `platform_extensions` (informational); the *transport* rules are identical, only the *consumption* semantics differ."), "digest": z.string().regex(new RegExp("^sha256:[a-f0-9]{64}$")).describe("SHA-256 content digest of the extension definition (sha256:). Used to detect drift — if the agent revises the extension, the digest changes and cached definitions become invalid.") }).catchall(z.any()).describe("Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` — divergent digests in one response indicate producer-side error (e.g., concurrent extension revision mid-render). Buyers encountering divergent digests for the same URI MUST fail closed: treat all references to that URI as unresolved and surface a validation error rather than picking one branch silently. **Across responses**, digest divergence is normal — extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.")).describe("Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\n\n**Collision precedence (normative).** When two or more `platform_extensions[]` entries on the same declaration extend the same target (e.g., both extend `tracking`) with overlapping field names, **array order is authoritative — later entries override earlier ones on a per-field basis** (last-in-array-wins). SDKs MUST surface the overlap via the `errors[]` array on the `get_products` response with a structured code (`FORMAT_DECLARATION_DIVERGENT` is appropriate when the overlap appears across dual-emitted shapes; a producer-self-emitted overlap on a single declaration SHOULD use the same code with `error.details: { collision_kind: \"platform_extension_field\", target, overlapping_fields, winning_extension_uri }`). Producers SHOULD avoid the collision by emitting one extension per target or by partitioning fields across extensions; the deterministic precedence is for last-resort consistency across SDK implementations, not a sanctioned merging strategy.").optional(), "synthesis_nondeterministic": z.boolean().describe("When true, the format's production pipeline is genuinely nondeterministic — the platform cannot guarantee that synthesis from a given input set produces in-spec output. Veo / Sora / Runway-class generative video, and other AI-synthesis flows where output dimensions, duration, or quality vary per run. Implies a different validation contract: predictive `validate_input` is impossible; the platform's own post-synthesis QA loop applies; if the QA loop exhausts without producing a valid artifact, `build_creative` returns task_failed with a synthesis_failed reason. Distinct from `composition_model` (which describes how the surface composes per-slot rendering, not whether synthesis is deterministic). When false or absent, the format's production is predictable enough that `validate_input` can predict output properties from input properties.\n\n**Compatibility with `asset_source` / `item_production_model`**: `synthesis_nondeterministic: true` MAY pair with any of `seller_pre_rendered_from_brief`, `seller_human_designed`, or `agent_synthesized` (the QA loop is concept-level, not source-specific — 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.").default(false), "slots": z.array(z.object({ "asset_group_id": z.string().describe("Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings."), "asset_type": z.enum(["image","video","audio","text","markdown","url","html","css","javascript","vast","daast","webhook","brief","catalog","published_post","zip","card","object","pixel_tracker","vast_tracker","daast_tracker"]).describe("Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `published_post` is an existing-post reference asset, not uploaded media bytes and not a catalog row. `card` is the multi-card carousel element type (see card-asset.json). `pixel_tracker` / `vast_tracker` / `daast_tracker` are the renderer-fired measurement-tracker primitives — see `/schemas/core/assets/pixel-tracker-asset.json` and the VAST / DAAST tracker schemas. `object` is a last-resort fallback for structured non-asset inputs that don't fit any primitive asset_type — prefer specific types whenever possible."), "required": z.boolean().describe("Whether this slot is required for a valid manifest.").default(false), "min": z.number().int().gte(0).describe("Minimum count for repeatable / pool slots.").optional(), "max": z.number().int().gte(1).describe("Maximum count for repeatable / pool slots.").optional(), "max_chars": z.number().int().gte(1).describe("Per-slot character limit. Valid only when `asset_type` is `text`, `markdown`, or `brief`. Mutually exclusive with `max_size_kb` (which applies to binary asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "max_size_kb": z.number().int().gte(1).describe("Per-slot file size limit in kilobytes. Valid only when `asset_type` is `image`, `video`, `audio`, or `zip`. Mutually exclusive with `max_chars` (which applies to text asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Accepted intrinsic-pixel densities for this image-bearing slot. Valid when `asset_type` is `image`, and on a `card` slot where it constrains each card's image media (video media is unaffected). This makes density available to every canonical carrying image assets (native, carousel, responsive, companion images, and image itself), not only `format_kind: image`. When the image canonical also declares top-level `params.pixel_ratios`, the effective set is the intersection; an empty intersection is invalid. One matching asset satisfies the slot unless `required_pixel_ratios` requires rendition coverage.").optional(), "required_pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Required density coverage for an image rendition set. Valid only when `asset_type` is `image` and `pixel_ratios` is also declared. Every value MUST appear in the effective accepted set after intersecting any top-level image `params.pixel_ratios`, and the manifest slot value MUST be an array containing exactly one matching image rendition for each required ratio. Other accepted ratios remain optional. For example, `pixel_ratios: [1, 1.5, 2]` with `required_pixel_ratios: [1, 2]` requires the 1x and 2x renditions while making 1.5x optional. SDKs enforce intersection, subset, coverage, and duplicate-ratio rules because JSON Schema draft-07 cannot express them generically.").optional(), "logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("When `asset_group_id` is `logo`, renderer-facing brand.json logo slots acceptable for this format slot. Producers selecting from brand.json SHOULD prefer `logos[]` entries whose `slots[]` intersects this list, then apply `visual_guidelines.logo_usage_rules[]`.").optional(), "required_logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Subset of `logo_slots` for which this format expects explicit logo coverage. A manifest or brand-derived logo pool SHOULD include at least one usable logo for each required slot; if coverage is missing, builders SHOULD surface a validation warning or approval mapping instead of guessing from prose.").optional(), "description": z.string().describe("Human-readable description of what the slot expects from the buyer.").optional(), "consumed_for_production": z.boolean().describe("Dispatch hint for `build_creative` and v1↔v2 wire translators: when `true`, the slot's value is consumed as INPUT to a production step (host-read script, brief copy fed to generative synthesis, catalog feed driving per-SKU rendering) and is not rendered verbatim. When `false` (default), the slot's value is rendered verbatim on the placement (image bytes, video file, display tag).\n\nMotivates the v1↔v2 dispatch table: pre-v2 buyers shipped production-consumed inputs separately in a `inputs` map on the build_creative request; v2 collapses inputs and rendered assets into a single `assets` map keyed by `asset_group_id`. SDK translators between v1 and v2 use this flag per canonical to know which assets in the v2 manifest map back to v1 `inputs` vs v1 `assets`. Without the per-slot flag the dispatch table lives in adopter code and every SDK gets it slightly different.\n\nProducers SHOULD set this explicitly on slots whose consumption pattern isn't obvious (host-read scripts on `audio_hosted`, briefs on generative `video_hosted`, catalog feeds on `sponsored_placement`). For canonicals where every slot is render-verbatim (`image`, `display_tag`, `video_vast`), the default `false` is sufficient and the flag MAY be omitted.").default(false) }).catchall(z.any()).and(z.intersection(z.intersection(z.any(), z.intersection(z.any(), z.any())), z.intersection(z.any(), z.intersection(z.any(), z.any()))))).describe("Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.").optional(), "required_connections": z.array(z.object({ "provider": z.string().describe("Stable provider or platform namespace, preferably lowercase. Examples: `social.example`, `shortvideo.example`, or a seller-defined namespace. Omit only when the requirement is provider-agnostic, or when an `authorization_url` fully routes the human to the correct provider-specific connection flow.").optional(), "connection_type": z.enum(["advertiser_account","publisher_identity","post_authorization"]).describe("Kind of downstream connection required. `advertiser_account` is the platform account used to buy/manage ads. `publisher_identity` is the creator, page, channel, organization, or profile that owns source posts. `post_authorization` is a post-scoped grant when the platform authorizes individual posts instead of, or in addition to, the owning identity."), "required_for": z.array(z.string().min(1)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Concrete AdCP protocol operation names that require this downstream connection. Sellers SHOULD include this in product declarations when the requirement is known ahead of time, and in AUTHORIZATION_REQUIRED details when it explains the failed operation. Prefer specific operation names such as `list_creatives`, `sync_creatives`, `create_media_buy`, `get_media_buy_delivery`, or `get_creative_delivery` over broad category labels such as `reporting`.").optional(), "scope": z.enum(["account","identity","post","unknown"]).describe("Granularity of the downstream grant.").optional(), "status": z.enum(["connected","missing","pending","expired","revoked","not_required","unknown"]).describe("Current seller-observed state for this downstream connection when known. Product declarations MAY omit status or use `unknown`; AUTHORIZATION_REQUIRED details SHOULD use `missing`, `expired`, or `revoked` for the connection that blocked the call.").optional(), "connection_id": z.string().describe("Seller-defined identifier for an already-created downstream connection. Omit when no connection exists yet or when exposing it would leak platform/account state.").optional(), "resource_ref": z.object({ "platform_account_id": z.string().describe("Provider-native advertiser or business account id, when safe to disclose.").optional(), "identity_id": z.string().describe("Provider-native creator, page, channel, organization, or profile id, when safe to disclose.").optional(), "handle": z.string().describe("Provider-native public handle for the owning identity, when available.").optional(), "profile_url": z.string().url().describe("Public URL for the owning identity, when available.").optional(), "post_id": z.string().describe("Provider-native post id, when the grant is post-scoped or the failed request referenced a specific post.").optional(), "post_url": z.string().url().describe("Public URL for the referenced post, when available.").optional() }).catchall(z.any()).describe("Optional opaque provider-native resource hint, such as a platform account id, profile URL, handle, channel id, post id, or post URL. This is a hint for routing authorization, not proof that authorization exists.").optional(), "authorization_url": z.string().url().describe("Seller-hosted or provider-hosted URL where a human can complete or restore this downstream connection.").optional(), "authorization_instructions": z.string().describe("Human-readable instructions for completing or restoring this downstream connection.").optional(), "expires_at": z.string().datetime().describe("Expiration time for the downstream grant, when known.").optional() }).catchall(z.any()).and(z.any()).describe("A seller/platform-side connection or grant required by a product, format, or request. This is not the AdCP caller credential: the AdCP request is still authenticated once, and the seller uses these stored downstream connections to call a platform or service on the buyer's behalf. Use this shape for platforms that require more than one downstream grant, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.")).describe("Downstream platform connections or grants required to use this format declaration. These are in addition to the single AdCP caller credential. Use this when a platform product requires multiple downstream grants, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.").optional(), "reference_mutability": z.enum(["immutable_snapshot","mutable_requires_reapproval","mutable_auto_recheck"]).describe("Policy for formats whose `slots` accept a `published_post` reference. `immutable_snapshot`: seller snapshots the referenced post at approval and later source changes do not change the served creative. `mutable_requires_reapproval`: the source post may change and material changes require review before continued serving. `mutable_auto_recheck`: the source post may change and the seller continuously or periodically rechecks authorization/policy without requiring buyer resubmission. Omit when the format has no `published_post` slot.").optional(), "production_window_business_days": z.number().int().gte(0).describe("Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative).").optional() }).catchall(z.any()).describe("Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints)."), z.union([z.any().refine((value) => !z.union([z.any(), z.any(), z.any(), z.any(), z.any()]).safeParse(value).success, "Invalid input: Should NOT be valid against schema"), z.any().refine((value) => !z.union([z.any(), z.any(), z.any(), z.any(), z.any(), z.any()]).safeParse(value).success, "Invalid input: Should NOT be valid against schema"), z.union([z.any(), z.any(), z.any(), z.any()]), z.any().refine((value) => !z.union([z.any(), z.any(), z.any(), z.any(), z.any(), z.any(), z.any()]).safeParse(value).success, "Invalid input: Should NOT be valid against schema")]).describe("Exactly one of: (a) fixed (`width` + `height` both set), (b) multi-size (`sizes` set), (c) responsive (any of `min_width`/`max_width`/`min_height`/`max_height` set), (d) none (no size constraint declared — accepts any dimensions). Combining modes is rejected at schema layer.")).describe("Interactive HTML5 banner delivered as a zip archive. Slot: `html5_bundle` (zip asset). Tracking model: MRAID + IAB Open Measurement (OM-SDK) + click-tag macro substitution + backup image fallback. Receivers unpack the zip, validate internal structure, and serve from CDN. Distinct from `image` (static, non-interactive) and `display_tag` (third-party served). The zip's entry point is typically `index.html`; click handling uses `clickTag` (or `clickTAG`) macro substitution.") }), z.object({ "format_kind": z.literal("display_tag"), "params": z.intersection(z.object({ "experimental": z.boolean().describe("When true, this canonical or seller narrowing may not work as declared. Adopters SHOULD preflight it with validate_input or in a sandbox and SHOULD NOT route production budget without testing; experimental status never makes the deprecated v1 path preferable. Drivers include unsettled spec shape, an adopter runtime gap, and custom shapes awaiting promotion. This replaces the earlier status plus runtime_status axes. Sellers SHOULD set experimental whenever a canonical or declaration is not production-ready.").default(false), "deprecated": z.boolean().describe("When true, this canonical (or a seller's specific narrowing of it) is going away. Existing adopters are supported through the deprecation cycle; new adoption is discouraged. Pair with `migration_target_version` to indicate when the canonical is expected to be removed. Distinct from `experimental`: an experimental canonical may stabilize and stop being experimental; a deprecated canonical is on a sunset path.").default(false), "v1_translatable": z.boolean().describe("Whether this canonical has any v1 named-format equivalent. `true` (default) — the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1→v2 projection via `v1-canonical-mapping.json` is meaningful. `false` — the canonical is inherently new in v2 and has no v1 form; v1's `list_creative_formats` couldn't express it because the underlying concept (algorithmic surface composition, AI-surface mentions, retail-media catalog placements, multi-card carousels) didn't exist as a v1 named-format archetype.\n\nLets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The 4 inherently-v2 canonicals at 3.1 GA: `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`.").default(true), "since_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version that introduced this canonical (e.g., '3.1', '3.2'). Lets adopters reason about minimum protocol version requirements when consuming a format declaration. Patch precision is intentionally rejected — canonicals are introduced at minor-version boundaries.").optional(), "migration_target_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version by which the working group expects this canonical to stabilize, surface a breaking revision, or (when `deprecated: true`) be removed. Patch precision is intentionally rejected — canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown').").optional(), "composition_model": z.enum(["deterministic","algorithmic"]).describe("Whether the surface composes deterministically (buyer can predict per-slot rendering — sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing — responsive_creative, agent_placement).").optional(), "provenance_required": z.boolean().describe("When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent.").optional(), "platform_extensions": z.array(z.object({ "uri": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL identifying the extension. `https://` is mandatory — `http://`, `file://`, `data:`, and other schemes are rejected at the schema layer (defense-in-depth on top of the fetch-contract normative rules). The URI base is the owning agent's URL; the path identifies the extension within that agent. Example: 'https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel'. The full fetch contract — SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds — is documented on `product-format-declaration.json#format_schema` and applies to ALL fetches of this reference shape regardless of whether the field is named `format_schema` (load-bearing for validation) or `platform_extensions` (informational); the *transport* rules are identical, only the *consumption* semantics differ."), "digest": z.string().regex(new RegExp("^sha256:[a-f0-9]{64}$")).describe("SHA-256 content digest of the extension definition (sha256:). Used to detect drift — if the agent revises the extension, the digest changes and cached definitions become invalid.") }).catchall(z.any()).describe("Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` — divergent digests in one response indicate producer-side error (e.g., concurrent extension revision mid-render). Buyers encountering divergent digests for the same URI MUST fail closed: treat all references to that URI as unresolved and surface a validation error rather than picking one branch silently. **Across responses**, digest divergence is normal — extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.")).describe("Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\n\n**Collision precedence (normative).** When two or more `platform_extensions[]` entries on the same declaration extend the same target (e.g., both extend `tracking`) with overlapping field names, **array order is authoritative — later entries override earlier ones on a per-field basis** (last-in-array-wins). SDKs MUST surface the overlap via the `errors[]` array on the `get_products` response with a structured code (`FORMAT_DECLARATION_DIVERGENT` is appropriate when the overlap appears across dual-emitted shapes; a producer-self-emitted overlap on a single declaration SHOULD use the same code with `error.details: { collision_kind: \"platform_extension_field\", target, overlapping_fields, winning_extension_uri }`). Producers SHOULD avoid the collision by emitting one extension per target or by partitioning fields across extensions; the deterministic precedence is for last-resort consistency across SDK implementations, not a sanctioned merging strategy.").optional(), "synthesis_nondeterministic": z.boolean().describe("When true, the format's production pipeline is genuinely nondeterministic — the platform cannot guarantee that synthesis from a given input set produces in-spec output. Veo / Sora / Runway-class generative video, and other AI-synthesis flows where output dimensions, duration, or quality vary per run. Implies a different validation contract: predictive `validate_input` is impossible; the platform's own post-synthesis QA loop applies; if the QA loop exhausts without producing a valid artifact, `build_creative` returns task_failed with a synthesis_failed reason. Distinct from `composition_model` (which describes how the surface composes per-slot rendering, not whether synthesis is deterministic). When false or absent, the format's production is predictable enough that `validate_input` can predict output properties from input properties.\n\n**Compatibility with `asset_source` / `item_production_model`**: `synthesis_nondeterministic: true` MAY pair with any of `seller_pre_rendered_from_brief`, `seller_human_designed`, or `agent_synthesized` (the QA loop is concept-level, not source-specific — 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.").default(false), "slots": z.array(z.object({ "asset_group_id": z.string().describe("Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings."), "asset_type": z.enum(["image","video","audio","text","markdown","url","html","css","javascript","vast","daast","webhook","brief","catalog","published_post","zip","card","object","pixel_tracker","vast_tracker","daast_tracker"]).describe("Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `published_post` is an existing-post reference asset, not uploaded media bytes and not a catalog row. `card` is the multi-card carousel element type (see card-asset.json). `pixel_tracker` / `vast_tracker` / `daast_tracker` are the renderer-fired measurement-tracker primitives — see `/schemas/core/assets/pixel-tracker-asset.json` and the VAST / DAAST tracker schemas. `object` is a last-resort fallback for structured non-asset inputs that don't fit any primitive asset_type — prefer specific types whenever possible."), "required": z.boolean().describe("Whether this slot is required for a valid manifest.").default(false), "min": z.number().int().gte(0).describe("Minimum count for repeatable / pool slots.").optional(), "max": z.number().int().gte(1).describe("Maximum count for repeatable / pool slots.").optional(), "max_chars": z.number().int().gte(1).describe("Per-slot character limit. Valid only when `asset_type` is `text`, `markdown`, or `brief`. Mutually exclusive with `max_size_kb` (which applies to binary asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "max_size_kb": z.number().int().gte(1).describe("Per-slot file size limit in kilobytes. Valid only when `asset_type` is `image`, `video`, `audio`, or `zip`. Mutually exclusive with `max_chars` (which applies to text asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Accepted intrinsic-pixel densities for this image-bearing slot. Valid when `asset_type` is `image`, and on a `card` slot where it constrains each card's image media (video media is unaffected). This makes density available to every canonical carrying image assets (native, carousel, responsive, companion images, and image itself), not only `format_kind: image`. When the image canonical also declares top-level `params.pixel_ratios`, the effective set is the intersection; an empty intersection is invalid. One matching asset satisfies the slot unless `required_pixel_ratios` requires rendition coverage.").optional(), "required_pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Required density coverage for an image rendition set. Valid only when `asset_type` is `image` and `pixel_ratios` is also declared. Every value MUST appear in the effective accepted set after intersecting any top-level image `params.pixel_ratios`, and the manifest slot value MUST be an array containing exactly one matching image rendition for each required ratio. Other accepted ratios remain optional. For example, `pixel_ratios: [1, 1.5, 2]` with `required_pixel_ratios: [1, 2]` requires the 1x and 2x renditions while making 1.5x optional. SDKs enforce intersection, subset, coverage, and duplicate-ratio rules because JSON Schema draft-07 cannot express them generically.").optional(), "logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("When `asset_group_id` is `logo`, renderer-facing brand.json logo slots acceptable for this format slot. Producers selecting from brand.json SHOULD prefer `logos[]` entries whose `slots[]` intersects this list, then apply `visual_guidelines.logo_usage_rules[]`.").optional(), "required_logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Subset of `logo_slots` for which this format expects explicit logo coverage. A manifest or brand-derived logo pool SHOULD include at least one usable logo for each required slot; if coverage is missing, builders SHOULD surface a validation warning or approval mapping instead of guessing from prose.").optional(), "description": z.string().describe("Human-readable description of what the slot expects from the buyer.").optional(), "consumed_for_production": z.boolean().describe("Dispatch hint for `build_creative` and v1↔v2 wire translators: when `true`, the slot's value is consumed as INPUT to a production step (host-read script, brief copy fed to generative synthesis, catalog feed driving per-SKU rendering) and is not rendered verbatim. When `false` (default), the slot's value is rendered verbatim on the placement (image bytes, video file, display tag).\n\nMotivates the v1↔v2 dispatch table: pre-v2 buyers shipped production-consumed inputs separately in a `inputs` map on the build_creative request; v2 collapses inputs and rendered assets into a single `assets` map keyed by `asset_group_id`. SDK translators between v1 and v2 use this flag per canonical to know which assets in the v2 manifest map back to v1 `inputs` vs v1 `assets`. Without the per-slot flag the dispatch table lives in adopter code and every SDK gets it slightly different.\n\nProducers SHOULD set this explicitly on slots whose consumption pattern isn't obvious (host-read scripts on `audio_hosted`, briefs on generative `video_hosted`, catalog feeds on `sponsored_placement`). For canonicals where every slot is render-verbatim (`image`, `display_tag`, `video_vast`), the default `false` is sufficient and the flag MAY be omitted.").default(false) }).catchall(z.any()).and(z.intersection(z.intersection(z.any(), z.intersection(z.any(), z.any())), z.intersection(z.any(), z.intersection(z.any(), z.any()))))).describe("Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.").optional(), "required_connections": z.array(z.object({ "provider": z.string().describe("Stable provider or platform namespace, preferably lowercase. Examples: `social.example`, `shortvideo.example`, or a seller-defined namespace. Omit only when the requirement is provider-agnostic, or when an `authorization_url` fully routes the human to the correct provider-specific connection flow.").optional(), "connection_type": z.enum(["advertiser_account","publisher_identity","post_authorization"]).describe("Kind of downstream connection required. `advertiser_account` is the platform account used to buy/manage ads. `publisher_identity` is the creator, page, channel, organization, or profile that owns source posts. `post_authorization` is a post-scoped grant when the platform authorizes individual posts instead of, or in addition to, the owning identity."), "required_for": z.array(z.string().min(1)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Concrete AdCP protocol operation names that require this downstream connection. Sellers SHOULD include this in product declarations when the requirement is known ahead of time, and in AUTHORIZATION_REQUIRED details when it explains the failed operation. Prefer specific operation names such as `list_creatives`, `sync_creatives`, `create_media_buy`, `get_media_buy_delivery`, or `get_creative_delivery` over broad category labels such as `reporting`.").optional(), "scope": z.enum(["account","identity","post","unknown"]).describe("Granularity of the downstream grant.").optional(), "status": z.enum(["connected","missing","pending","expired","revoked","not_required","unknown"]).describe("Current seller-observed state for this downstream connection when known. Product declarations MAY omit status or use `unknown`; AUTHORIZATION_REQUIRED details SHOULD use `missing`, `expired`, or `revoked` for the connection that blocked the call.").optional(), "connection_id": z.string().describe("Seller-defined identifier for an already-created downstream connection. Omit when no connection exists yet or when exposing it would leak platform/account state.").optional(), "resource_ref": z.object({ "platform_account_id": z.string().describe("Provider-native advertiser or business account id, when safe to disclose.").optional(), "identity_id": z.string().describe("Provider-native creator, page, channel, organization, or profile id, when safe to disclose.").optional(), "handle": z.string().describe("Provider-native public handle for the owning identity, when available.").optional(), "profile_url": z.string().url().describe("Public URL for the owning identity, when available.").optional(), "post_id": z.string().describe("Provider-native post id, when the grant is post-scoped or the failed request referenced a specific post.").optional(), "post_url": z.string().url().describe("Public URL for the referenced post, when available.").optional() }).catchall(z.any()).describe("Optional opaque provider-native resource hint, such as a platform account id, profile URL, handle, channel id, post id, or post URL. This is a hint for routing authorization, not proof that authorization exists.").optional(), "authorization_url": z.string().url().describe("Seller-hosted or provider-hosted URL where a human can complete or restore this downstream connection.").optional(), "authorization_instructions": z.string().describe("Human-readable instructions for completing or restoring this downstream connection.").optional(), "expires_at": z.string().datetime().describe("Expiration time for the downstream grant, when known.").optional() }).catchall(z.any()).and(z.any()).describe("A seller/platform-side connection or grant required by a product, format, or request. This is not the AdCP caller credential: the AdCP request is still authenticated once, and the seller uses these stored downstream connections to call a platform or service on the buyer's behalf. Use this shape for platforms that require more than one downstream grant, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.")).describe("Downstream platform connections or grants required to use this format declaration. These are in addition to the single AdCP caller credential. Use this when a platform product requires multiple downstream grants, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.").optional(), "reference_mutability": z.enum(["immutable_snapshot","mutable_requires_reapproval","mutable_auto_recheck"]).describe("Policy for formats whose `slots` accept a `published_post` reference. `immutable_snapshot`: seller snapshots the referenced post at approval and later source changes do not change the served creative. `mutable_requires_reapproval`: the source post may change and material changes require review before continued serving. `mutable_auto_recheck`: the source post may change and the seller continuously or periodically rechecks authorization/policy without requiring buyer resubmission. Omit when the format has no `published_post` slot.").optional(), "production_window_business_days": z.number().int().gte(0).describe("Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative).").optional() }).catchall(z.any()).describe("Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints)."), z.union([z.any().refine((value) => !z.union([z.any(), z.any(), z.any(), z.any(), z.any()]).safeParse(value).success, "Invalid input: Should NOT be valid against schema"), z.any().refine((value) => !z.union([z.any(), z.any(), z.any(), z.any(), z.any(), z.any()]).safeParse(value).success, "Invalid input: Should NOT be valid against schema"), z.union([z.any(), z.any(), z.any(), z.any()]), z.any().refine((value) => !z.union([z.any(), z.any(), z.any(), z.any(), z.any(), z.any(), z.any()]).safeParse(value).success, "Invalid input: Should NOT be valid against schema")]).describe("Exactly one of: (a) fixed (`width` + `height` both set), (b) multi-size (`sizes` set), (c) responsive (any of `min_width`/`max_width`/`min_height`/`max_height` set), (d) none (no size constraint declared — accepts any dimensions). Combining modes is rejected at schema layer.")).describe("Third-party-served display tag (JS, iframe, or 1×1 redirect). The buyer's adserver hosts the creative; the seller calls the tag URL at impression time. Slot: `tag_url` (url asset with appropriate `url_type`). Tracking model: opaque to seller — third party serves and measures. Click tracking via redirect URL substitution using universal_macros. Distinct from `image` (static asset hosted by seller) and `html5` (zip bundle hosted by seller).") }), z.object({ "format_kind": z.literal("image_carousel"), "params": z.object({ "experimental": z.boolean().describe("When true, this canonical or seller narrowing may not work as declared. Adopters SHOULD preflight it with validate_input or in a sandbox and SHOULD NOT route production budget without testing; experimental status never makes the deprecated v1 path preferable. Drivers include unsettled spec shape, an adopter runtime gap, and custom shapes awaiting promotion. This replaces the earlier status plus runtime_status axes. Sellers SHOULD set experimental whenever a canonical or declaration is not production-ready.").default(false), "deprecated": z.boolean().describe("When true, this canonical (or a seller's specific narrowing of it) is going away. Existing adopters are supported through the deprecation cycle; new adoption is discouraged. Pair with `migration_target_version` to indicate when the canonical is expected to be removed. Distinct from `experimental`: an experimental canonical may stabilize and stop being experimental; a deprecated canonical is on a sunset path.").default(false), "v1_translatable": z.boolean().describe("Whether this canonical has any v1 named-format equivalent. `true` (default) — the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1→v2 projection via `v1-canonical-mapping.json` is meaningful. `false` — the canonical is inherently new in v2 and has no v1 form; v1's `list_creative_formats` couldn't express it because the underlying concept (algorithmic surface composition, AI-surface mentions, retail-media catalog placements, multi-card carousels) didn't exist as a v1 named-format archetype.\n\nLets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The 4 inherently-v2 canonicals at 3.1 GA: `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`.").default(true), "since_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version that introduced this canonical (e.g., '3.1', '3.2'). Lets adopters reason about minimum protocol version requirements when consuming a format declaration. Patch precision is intentionally rejected — canonicals are introduced at minor-version boundaries.").optional(), "migration_target_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version by which the working group expects this canonical to stabilize, surface a breaking revision, or (when `deprecated: true`) be removed. Patch precision is intentionally rejected — canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown').").optional(), "composition_model": z.enum(["deterministic","algorithmic"]).describe("Whether the surface composes deterministically (buyer can predict per-slot rendering — sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing — responsive_creative, agent_placement).").optional(), "provenance_required": z.boolean().describe("When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent.").optional(), "platform_extensions": z.array(z.object({ "uri": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL identifying the extension. `https://` is mandatory — `http://`, `file://`, `data:`, and other schemes are rejected at the schema layer (defense-in-depth on top of the fetch-contract normative rules). The URI base is the owning agent's URL; the path identifies the extension within that agent. Example: 'https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel'. The full fetch contract — SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds — is documented on `product-format-declaration.json#format_schema` and applies to ALL fetches of this reference shape regardless of whether the field is named `format_schema` (load-bearing for validation) or `platform_extensions` (informational); the *transport* rules are identical, only the *consumption* semantics differ."), "digest": z.string().regex(new RegExp("^sha256:[a-f0-9]{64}$")).describe("SHA-256 content digest of the extension definition (sha256:). Used to detect drift — if the agent revises the extension, the digest changes and cached definitions become invalid.") }).catchall(z.any()).describe("Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` — divergent digests in one response indicate producer-side error (e.g., concurrent extension revision mid-render). Buyers encountering divergent digests for the same URI MUST fail closed: treat all references to that URI as unresolved and surface a validation error rather than picking one branch silently. **Across responses**, digest divergence is normal — extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.")).describe("Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\n\n**Collision precedence (normative).** When two or more `platform_extensions[]` entries on the same declaration extend the same target (e.g., both extend `tracking`) with overlapping field names, **array order is authoritative — later entries override earlier ones on a per-field basis** (last-in-array-wins). SDKs MUST surface the overlap via the `errors[]` array on the `get_products` response with a structured code (`FORMAT_DECLARATION_DIVERGENT` is appropriate when the overlap appears across dual-emitted shapes; a producer-self-emitted overlap on a single declaration SHOULD use the same code with `error.details: { collision_kind: \"platform_extension_field\", target, overlapping_fields, winning_extension_uri }`). Producers SHOULD avoid the collision by emitting one extension per target or by partitioning fields across extensions; the deterministic precedence is for last-resort consistency across SDK implementations, not a sanctioned merging strategy.").optional(), "synthesis_nondeterministic": z.boolean().describe("When true, the format's production pipeline is genuinely nondeterministic — the platform cannot guarantee that synthesis from a given input set produces in-spec output. Veo / Sora / Runway-class generative video, and other AI-synthesis flows where output dimensions, duration, or quality vary per run. Implies a different validation contract: predictive `validate_input` is impossible; the platform's own post-synthesis QA loop applies; if the QA loop exhausts without producing a valid artifact, `build_creative` returns task_failed with a synthesis_failed reason. Distinct from `composition_model` (which describes how the surface composes per-slot rendering, not whether synthesis is deterministic). When false or absent, the format's production is predictable enough that `validate_input` can predict output properties from input properties.\n\n**Compatibility with `asset_source` / `item_production_model`**: `synthesis_nondeterministic: true` MAY pair with any of `seller_pre_rendered_from_brief`, `seller_human_designed`, or `agent_synthesized` (the QA loop is concept-level, not source-specific — 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.").default(false), "slots": z.array(z.object({ "asset_group_id": z.string().describe("Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings."), "asset_type": z.enum(["image","video","audio","text","markdown","url","html","css","javascript","vast","daast","webhook","brief","catalog","published_post","zip","card","object","pixel_tracker","vast_tracker","daast_tracker"]).describe("Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `published_post` is an existing-post reference asset, not uploaded media bytes and not a catalog row. `card` is the multi-card carousel element type (see card-asset.json). `pixel_tracker` / `vast_tracker` / `daast_tracker` are the renderer-fired measurement-tracker primitives — see `/schemas/core/assets/pixel-tracker-asset.json` and the VAST / DAAST tracker schemas. `object` is a last-resort fallback for structured non-asset inputs that don't fit any primitive asset_type — prefer specific types whenever possible."), "required": z.boolean().describe("Whether this slot is required for a valid manifest.").default(false), "min": z.number().int().gte(0).describe("Minimum count for repeatable / pool slots.").optional(), "max": z.number().int().gte(1).describe("Maximum count for repeatable / pool slots.").optional(), "max_chars": z.number().int().gte(1).describe("Per-slot character limit. Valid only when `asset_type` is `text`, `markdown`, or `brief`. Mutually exclusive with `max_size_kb` (which applies to binary asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "max_size_kb": z.number().int().gte(1).describe("Per-slot file size limit in kilobytes. Valid only when `asset_type` is `image`, `video`, `audio`, or `zip`. Mutually exclusive with `max_chars` (which applies to text asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Accepted intrinsic-pixel densities for this image-bearing slot. Valid when `asset_type` is `image`, and on a `card` slot where it constrains each card's image media (video media is unaffected). This makes density available to every canonical carrying image assets (native, carousel, responsive, companion images, and image itself), not only `format_kind: image`. When the image canonical also declares top-level `params.pixel_ratios`, the effective set is the intersection; an empty intersection is invalid. One matching asset satisfies the slot unless `required_pixel_ratios` requires rendition coverage.").optional(), "required_pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Required density coverage for an image rendition set. Valid only when `asset_type` is `image` and `pixel_ratios` is also declared. Every value MUST appear in the effective accepted set after intersecting any top-level image `params.pixel_ratios`, and the manifest slot value MUST be an array containing exactly one matching image rendition for each required ratio. Other accepted ratios remain optional. For example, `pixel_ratios: [1, 1.5, 2]` with `required_pixel_ratios: [1, 2]` requires the 1x and 2x renditions while making 1.5x optional. SDKs enforce intersection, subset, coverage, and duplicate-ratio rules because JSON Schema draft-07 cannot express them generically.").optional(), "logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("When `asset_group_id` is `logo`, renderer-facing brand.json logo slots acceptable for this format slot. Producers selecting from brand.json SHOULD prefer `logos[]` entries whose `slots[]` intersects this list, then apply `visual_guidelines.logo_usage_rules[]`.").optional(), "required_logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Subset of `logo_slots` for which this format expects explicit logo coverage. A manifest or brand-derived logo pool SHOULD include at least one usable logo for each required slot; if coverage is missing, builders SHOULD surface a validation warning or approval mapping instead of guessing from prose.").optional(), "description": z.string().describe("Human-readable description of what the slot expects from the buyer.").optional(), "consumed_for_production": z.boolean().describe("Dispatch hint for `build_creative` and v1↔v2 wire translators: when `true`, the slot's value is consumed as INPUT to a production step (host-read script, brief copy fed to generative synthesis, catalog feed driving per-SKU rendering) and is not rendered verbatim. When `false` (default), the slot's value is rendered verbatim on the placement (image bytes, video file, display tag).\n\nMotivates the v1↔v2 dispatch table: pre-v2 buyers shipped production-consumed inputs separately in a `inputs` map on the build_creative request; v2 collapses inputs and rendered assets into a single `assets` map keyed by `asset_group_id`. SDK translators between v1 and v2 use this flag per canonical to know which assets in the v2 manifest map back to v1 `inputs` vs v1 `assets`. Without the per-slot flag the dispatch table lives in adopter code and every SDK gets it slightly different.\n\nProducers SHOULD set this explicitly on slots whose consumption pattern isn't obvious (host-read scripts on `audio_hosted`, briefs on generative `video_hosted`, catalog feeds on `sponsored_placement`). For canonicals where every slot is render-verbatim (`image`, `display_tag`, `video_vast`), the default `false` is sufficient and the flag MAY be omitted.").default(false) }).catchall(z.any()).and(z.intersection(z.intersection(z.any(), z.intersection(z.any(), z.any())), z.intersection(z.any(), z.intersection(z.any(), z.any()))))).describe("Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.").optional(), "required_connections": z.array(z.object({ "provider": z.string().describe("Stable provider or platform namespace, preferably lowercase. Examples: `social.example`, `shortvideo.example`, or a seller-defined namespace. Omit only when the requirement is provider-agnostic, or when an `authorization_url` fully routes the human to the correct provider-specific connection flow.").optional(), "connection_type": z.enum(["advertiser_account","publisher_identity","post_authorization"]).describe("Kind of downstream connection required. `advertiser_account` is the platform account used to buy/manage ads. `publisher_identity` is the creator, page, channel, organization, or profile that owns source posts. `post_authorization` is a post-scoped grant when the platform authorizes individual posts instead of, or in addition to, the owning identity."), "required_for": z.array(z.string().min(1)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Concrete AdCP protocol operation names that require this downstream connection. Sellers SHOULD include this in product declarations when the requirement is known ahead of time, and in AUTHORIZATION_REQUIRED details when it explains the failed operation. Prefer specific operation names such as `list_creatives`, `sync_creatives`, `create_media_buy`, `get_media_buy_delivery`, or `get_creative_delivery` over broad category labels such as `reporting`.").optional(), "scope": z.enum(["account","identity","post","unknown"]).describe("Granularity of the downstream grant.").optional(), "status": z.enum(["connected","missing","pending","expired","revoked","not_required","unknown"]).describe("Current seller-observed state for this downstream connection when known. Product declarations MAY omit status or use `unknown`; AUTHORIZATION_REQUIRED details SHOULD use `missing`, `expired`, or `revoked` for the connection that blocked the call.").optional(), "connection_id": z.string().describe("Seller-defined identifier for an already-created downstream connection. Omit when no connection exists yet or when exposing it would leak platform/account state.").optional(), "resource_ref": z.object({ "platform_account_id": z.string().describe("Provider-native advertiser or business account id, when safe to disclose.").optional(), "identity_id": z.string().describe("Provider-native creator, page, channel, organization, or profile id, when safe to disclose.").optional(), "handle": z.string().describe("Provider-native public handle for the owning identity, when available.").optional(), "profile_url": z.string().url().describe("Public URL for the owning identity, when available.").optional(), "post_id": z.string().describe("Provider-native post id, when the grant is post-scoped or the failed request referenced a specific post.").optional(), "post_url": z.string().url().describe("Public URL for the referenced post, when available.").optional() }).catchall(z.any()).describe("Optional opaque provider-native resource hint, such as a platform account id, profile URL, handle, channel id, post id, or post URL. This is a hint for routing authorization, not proof that authorization exists.").optional(), "authorization_url": z.string().url().describe("Seller-hosted or provider-hosted URL where a human can complete or restore this downstream connection.").optional(), "authorization_instructions": z.string().describe("Human-readable instructions for completing or restoring this downstream connection.").optional(), "expires_at": z.string().datetime().describe("Expiration time for the downstream grant, when known.").optional() }).catchall(z.any()).and(z.any()).describe("A seller/platform-side connection or grant required by a product, format, or request. This is not the AdCP caller credential: the AdCP request is still authenticated once, and the seller uses these stored downstream connections to call a platform or service on the buyer's behalf. Use this shape for platforms that require more than one downstream grant, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.")).describe("Downstream platform connections or grants required to use this format declaration. These are in addition to the single AdCP caller credential. Use this when a platform product requires multiple downstream grants, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.").optional(), "reference_mutability": z.enum(["immutable_snapshot","mutable_requires_reapproval","mutable_auto_recheck"]).describe("Policy for formats whose `slots` accept a `published_post` reference. `immutable_snapshot`: seller snapshots the referenced post at approval and later source changes do not change the served creative. `mutable_requires_reapproval`: the source post may change and material changes require review before continued serving. `mutable_auto_recheck`: the source post may change and the seller continuously or periodically rechecks authorization/policy without requiring buyer resubmission. Omit when the format has no `published_post` slot.").optional(), "production_window_business_days": z.number().int().gte(0).describe("Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative).").optional() }).catchall(z.any()).describe("Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).").describe("Multi-card swipeable carousel. The buyer ships a `cards` slot whose value is an **array** of [card-asset](/schemas/core/assets/card-asset.json) objects (a single key with an array value — NOT one key per card, NOT dotted/bracketed paths). Each card-asset carries: `asset_type: \"card\"`, `media` (an image or video asset), optional `headline` (text), optional `landing_page_url` (url asset). Per-card structure is the same across all cards; mixed orientations not allowed within a single carousel. Tracking model: per-card impression and engagement pixels + carousel-level engagement (swipe, view-time). Allowed asset types for a card's `media` field: `image` and `video` (Meta-style mixed-media); platforms can narrow to image-only or video-only via `allowed_card_media_asset_types`.\n\nThe manifest's `assets.cards` value is an array of card-asset objects. Example: `\"cards\": [{\"asset_type\": \"card\", \"media\": {\"asset_type\": \"image\", \"url\": \"...\"}, \"headline\": \"Buy now\", \"landing_page_url\": {\"asset_type\": \"url\", \"url_type\": \"clickthrough\", \"url\": \"...\"}}, ...]`. Each card-asset validates against the card schema; per-card platform extensions attach via the card's `platform_extensions` field, never via inline non-canonical keys.") }), z.object({ "format_kind": z.literal("video_hosted"), "params": z.object({ "experimental": z.boolean().describe("When true, this canonical or seller narrowing may not work as declared. Adopters SHOULD preflight it with validate_input or in a sandbox and SHOULD NOT route production budget without testing; experimental status never makes the deprecated v1 path preferable. Drivers include unsettled spec shape, an adopter runtime gap, and custom shapes awaiting promotion. This replaces the earlier status plus runtime_status axes. Sellers SHOULD set experimental whenever a canonical or declaration is not production-ready.").default(false), "deprecated": z.boolean().describe("When true, this canonical (or a seller's specific narrowing of it) is going away. Existing adopters are supported through the deprecation cycle; new adoption is discouraged. Pair with `migration_target_version` to indicate when the canonical is expected to be removed. Distinct from `experimental`: an experimental canonical may stabilize and stop being experimental; a deprecated canonical is on a sunset path.").default(false), "v1_translatable": z.boolean().describe("Whether this canonical has any v1 named-format equivalent. `true` (default) — the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1→v2 projection via `v1-canonical-mapping.json` is meaningful. `false` — the canonical is inherently new in v2 and has no v1 form; v1's `list_creative_formats` couldn't express it because the underlying concept (algorithmic surface composition, AI-surface mentions, retail-media catalog placements, multi-card carousels) didn't exist as a v1 named-format archetype.\n\nLets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The 4 inherently-v2 canonicals at 3.1 GA: `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`.").default(true), "since_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version that introduced this canonical (e.g., '3.1', '3.2'). Lets adopters reason about minimum protocol version requirements when consuming a format declaration. Patch precision is intentionally rejected — canonicals are introduced at minor-version boundaries.").optional(), "migration_target_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version by which the working group expects this canonical to stabilize, surface a breaking revision, or (when `deprecated: true`) be removed. Patch precision is intentionally rejected — canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown').").optional(), "composition_model": z.enum(["deterministic","algorithmic"]).describe("Whether the surface composes deterministically (buyer can predict per-slot rendering — sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing — responsive_creative, agent_placement).").optional(), "provenance_required": z.boolean().describe("When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent.").optional(), "platform_extensions": z.array(z.object({ "uri": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL identifying the extension. `https://` is mandatory — `http://`, `file://`, `data:`, and other schemes are rejected at the schema layer (defense-in-depth on top of the fetch-contract normative rules). The URI base is the owning agent's URL; the path identifies the extension within that agent. Example: 'https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel'. The full fetch contract — SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds — is documented on `product-format-declaration.json#format_schema` and applies to ALL fetches of this reference shape regardless of whether the field is named `format_schema` (load-bearing for validation) or `platform_extensions` (informational); the *transport* rules are identical, only the *consumption* semantics differ."), "digest": z.string().regex(new RegExp("^sha256:[a-f0-9]{64}$")).describe("SHA-256 content digest of the extension definition (sha256:). Used to detect drift — if the agent revises the extension, the digest changes and cached definitions become invalid.") }).catchall(z.any()).describe("Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` — divergent digests in one response indicate producer-side error (e.g., concurrent extension revision mid-render). Buyers encountering divergent digests for the same URI MUST fail closed: treat all references to that URI as unresolved and surface a validation error rather than picking one branch silently. **Across responses**, digest divergence is normal — extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.")).describe("Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\n\n**Collision precedence (normative).** When two or more `platform_extensions[]` entries on the same declaration extend the same target (e.g., both extend `tracking`) with overlapping field names, **array order is authoritative — later entries override earlier ones on a per-field basis** (last-in-array-wins). SDKs MUST surface the overlap via the `errors[]` array on the `get_products` response with a structured code (`FORMAT_DECLARATION_DIVERGENT` is appropriate when the overlap appears across dual-emitted shapes; a producer-self-emitted overlap on a single declaration SHOULD use the same code with `error.details: { collision_kind: \"platform_extension_field\", target, overlapping_fields, winning_extension_uri }`). Producers SHOULD avoid the collision by emitting one extension per target or by partitioning fields across extensions; the deterministic precedence is for last-resort consistency across SDK implementations, not a sanctioned merging strategy.").optional(), "synthesis_nondeterministic": z.boolean().describe("When true, the format's production pipeline is genuinely nondeterministic — the platform cannot guarantee that synthesis from a given input set produces in-spec output. Veo / Sora / Runway-class generative video, and other AI-synthesis flows where output dimensions, duration, or quality vary per run. Implies a different validation contract: predictive `validate_input` is impossible; the platform's own post-synthesis QA loop applies; if the QA loop exhausts without producing a valid artifact, `build_creative` returns task_failed with a synthesis_failed reason. Distinct from `composition_model` (which describes how the surface composes per-slot rendering, not whether synthesis is deterministic). When false or absent, the format's production is predictable enough that `validate_input` can predict output properties from input properties.\n\n**Compatibility with `asset_source` / `item_production_model`**: `synthesis_nondeterministic: true` MAY pair with any of `seller_pre_rendered_from_brief`, `seller_human_designed`, or `agent_synthesized` (the QA loop is concept-level, not source-specific — 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.").default(false), "slots": z.array(z.object({ "asset_group_id": z.string().describe("Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings."), "asset_type": z.enum(["image","video","audio","text","markdown","url","html","css","javascript","vast","daast","webhook","brief","catalog","published_post","zip","card","object","pixel_tracker","vast_tracker","daast_tracker"]).describe("Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `published_post` is an existing-post reference asset, not uploaded media bytes and not a catalog row. `card` is the multi-card carousel element type (see card-asset.json). `pixel_tracker` / `vast_tracker` / `daast_tracker` are the renderer-fired measurement-tracker primitives — see `/schemas/core/assets/pixel-tracker-asset.json` and the VAST / DAAST tracker schemas. `object` is a last-resort fallback for structured non-asset inputs that don't fit any primitive asset_type — prefer specific types whenever possible."), "required": z.boolean().describe("Whether this slot is required for a valid manifest.").default(false), "min": z.number().int().gte(0).describe("Minimum count for repeatable / pool slots.").optional(), "max": z.number().int().gte(1).describe("Maximum count for repeatable / pool slots.").optional(), "max_chars": z.number().int().gte(1).describe("Per-slot character limit. Valid only when `asset_type` is `text`, `markdown`, or `brief`. Mutually exclusive with `max_size_kb` (which applies to binary asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "max_size_kb": z.number().int().gte(1).describe("Per-slot file size limit in kilobytes. Valid only when `asset_type` is `image`, `video`, `audio`, or `zip`. Mutually exclusive with `max_chars` (which applies to text asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Accepted intrinsic-pixel densities for this image-bearing slot. Valid when `asset_type` is `image`, and on a `card` slot where it constrains each card's image media (video media is unaffected). This makes density available to every canonical carrying image assets (native, carousel, responsive, companion images, and image itself), not only `format_kind: image`. When the image canonical also declares top-level `params.pixel_ratios`, the effective set is the intersection; an empty intersection is invalid. One matching asset satisfies the slot unless `required_pixel_ratios` requires rendition coverage.").optional(), "required_pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Required density coverage for an image rendition set. Valid only when `asset_type` is `image` and `pixel_ratios` is also declared. Every value MUST appear in the effective accepted set after intersecting any top-level image `params.pixel_ratios`, and the manifest slot value MUST be an array containing exactly one matching image rendition for each required ratio. Other accepted ratios remain optional. For example, `pixel_ratios: [1, 1.5, 2]` with `required_pixel_ratios: [1, 2]` requires the 1x and 2x renditions while making 1.5x optional. SDKs enforce intersection, subset, coverage, and duplicate-ratio rules because JSON Schema draft-07 cannot express them generically.").optional(), "logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("When `asset_group_id` is `logo`, renderer-facing brand.json logo slots acceptable for this format slot. Producers selecting from brand.json SHOULD prefer `logos[]` entries whose `slots[]` intersects this list, then apply `visual_guidelines.logo_usage_rules[]`.").optional(), "required_logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Subset of `logo_slots` for which this format expects explicit logo coverage. A manifest or brand-derived logo pool SHOULD include at least one usable logo for each required slot; if coverage is missing, builders SHOULD surface a validation warning or approval mapping instead of guessing from prose.").optional(), "description": z.string().describe("Human-readable description of what the slot expects from the buyer.").optional(), "consumed_for_production": z.boolean().describe("Dispatch hint for `build_creative` and v1↔v2 wire translators: when `true`, the slot's value is consumed as INPUT to a production step (host-read script, brief copy fed to generative synthesis, catalog feed driving per-SKU rendering) and is not rendered verbatim. When `false` (default), the slot's value is rendered verbatim on the placement (image bytes, video file, display tag).\n\nMotivates the v1↔v2 dispatch table: pre-v2 buyers shipped production-consumed inputs separately in a `inputs` map on the build_creative request; v2 collapses inputs and rendered assets into a single `assets` map keyed by `asset_group_id`. SDK translators between v1 and v2 use this flag per canonical to know which assets in the v2 manifest map back to v1 `inputs` vs v1 `assets`. Without the per-slot flag the dispatch table lives in adopter code and every SDK gets it slightly different.\n\nProducers SHOULD set this explicitly on slots whose consumption pattern isn't obvious (host-read scripts on `audio_hosted`, briefs on generative `video_hosted`, catalog feeds on `sponsored_placement`). For canonicals where every slot is render-verbatim (`image`, `display_tag`, `video_vast`), the default `false` is sufficient and the flag MAY be omitted.").default(false) }).catchall(z.any()).and(z.intersection(z.intersection(z.any(), z.intersection(z.any(), z.any())), z.intersection(z.any(), z.intersection(z.any(), z.any()))))).describe("Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.").optional(), "required_connections": z.array(z.object({ "provider": z.string().describe("Stable provider or platform namespace, preferably lowercase. Examples: `social.example`, `shortvideo.example`, or a seller-defined namespace. Omit only when the requirement is provider-agnostic, or when an `authorization_url` fully routes the human to the correct provider-specific connection flow.").optional(), "connection_type": z.enum(["advertiser_account","publisher_identity","post_authorization"]).describe("Kind of downstream connection required. `advertiser_account` is the platform account used to buy/manage ads. `publisher_identity` is the creator, page, channel, organization, or profile that owns source posts. `post_authorization` is a post-scoped grant when the platform authorizes individual posts instead of, or in addition to, the owning identity."), "required_for": z.array(z.string().min(1)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Concrete AdCP protocol operation names that require this downstream connection. Sellers SHOULD include this in product declarations when the requirement is known ahead of time, and in AUTHORIZATION_REQUIRED details when it explains the failed operation. Prefer specific operation names such as `list_creatives`, `sync_creatives`, `create_media_buy`, `get_media_buy_delivery`, or `get_creative_delivery` over broad category labels such as `reporting`.").optional(), "scope": z.enum(["account","identity","post","unknown"]).describe("Granularity of the downstream grant.").optional(), "status": z.enum(["connected","missing","pending","expired","revoked","not_required","unknown"]).describe("Current seller-observed state for this downstream connection when known. Product declarations MAY omit status or use `unknown`; AUTHORIZATION_REQUIRED details SHOULD use `missing`, `expired`, or `revoked` for the connection that blocked the call.").optional(), "connection_id": z.string().describe("Seller-defined identifier for an already-created downstream connection. Omit when no connection exists yet or when exposing it would leak platform/account state.").optional(), "resource_ref": z.object({ "platform_account_id": z.string().describe("Provider-native advertiser or business account id, when safe to disclose.").optional(), "identity_id": z.string().describe("Provider-native creator, page, channel, organization, or profile id, when safe to disclose.").optional(), "handle": z.string().describe("Provider-native public handle for the owning identity, when available.").optional(), "profile_url": z.string().url().describe("Public URL for the owning identity, when available.").optional(), "post_id": z.string().describe("Provider-native post id, when the grant is post-scoped or the failed request referenced a specific post.").optional(), "post_url": z.string().url().describe("Public URL for the referenced post, when available.").optional() }).catchall(z.any()).describe("Optional opaque provider-native resource hint, such as a platform account id, profile URL, handle, channel id, post id, or post URL. This is a hint for routing authorization, not proof that authorization exists.").optional(), "authorization_url": z.string().url().describe("Seller-hosted or provider-hosted URL where a human can complete or restore this downstream connection.").optional(), "authorization_instructions": z.string().describe("Human-readable instructions for completing or restoring this downstream connection.").optional(), "expires_at": z.string().datetime().describe("Expiration time for the downstream grant, when known.").optional() }).catchall(z.any()).and(z.any()).describe("A seller/platform-side connection or grant required by a product, format, or request. This is not the AdCP caller credential: the AdCP request is still authenticated once, and the seller uses these stored downstream connections to call a platform or service on the buyer's behalf. Use this shape for platforms that require more than one downstream grant, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.")).describe("Downstream platform connections or grants required to use this format declaration. These are in addition to the single AdCP caller credential. Use this when a platform product requires multiple downstream grants, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.").optional(), "reference_mutability": z.enum(["immutable_snapshot","mutable_requires_reapproval","mutable_auto_recheck"]).describe("Policy for formats whose `slots` accept a `published_post` reference. `immutable_snapshot`: seller snapshots the referenced post at approval and later source changes do not change the served creative. `mutable_requires_reapproval`: the source post may change and material changes require review before continued serving. `mutable_auto_recheck`: the source post may change and the seller continuously or periodically rechecks authorization/policy without requiring buyer resubmission. Omit when the format has no `published_post` slot.").optional(), "production_window_business_days": z.number().int().gte(0).describe("Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative).").optional() }).catchall(z.any()).describe("Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).").describe("Direct video file (mp4/webm/mov) hosted by the buyer. Slot: `video_main` (video asset, file or hosted URL), optional `headline`, `brand_name`, `cta`, `companion_banner`, `landing_page_url`. Tracking model: IAB Open Measurement SDK + external impression/click/quartile pixels via universal_macros. Orientation is a parameter (vertical 9:16 / horizontal 16:9 / square 1:1); slot shape includes optional `brand_name` (typical for vertical short-form) and optional `companion_banner` (typical for horizontal instream). Distinct from `video_vast` (VAST tag, inherent VAST event tracking) — receivers fire impression and click pixels at delivery time.") }), z.object({ "format_kind": z.literal("video_vast"), "params": z.object({ "experimental": z.boolean().describe("When true, this canonical or seller narrowing may not work as declared. Adopters SHOULD preflight it with validate_input or in a sandbox and SHOULD NOT route production budget without testing; experimental status never makes the deprecated v1 path preferable. Drivers include unsettled spec shape, an adopter runtime gap, and custom shapes awaiting promotion. This replaces the earlier status plus runtime_status axes. Sellers SHOULD set experimental whenever a canonical or declaration is not production-ready.").default(false), "deprecated": z.boolean().describe("When true, this canonical (or a seller's specific narrowing of it) is going away. Existing adopters are supported through the deprecation cycle; new adoption is discouraged. Pair with `migration_target_version` to indicate when the canonical is expected to be removed. Distinct from `experimental`: an experimental canonical may stabilize and stop being experimental; a deprecated canonical is on a sunset path.").default(false), "v1_translatable": z.boolean().describe("Whether this canonical has any v1 named-format equivalent. `true` (default) — the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1→v2 projection via `v1-canonical-mapping.json` is meaningful. `false` — the canonical is inherently new in v2 and has no v1 form; v1's `list_creative_formats` couldn't express it because the underlying concept (algorithmic surface composition, AI-surface mentions, retail-media catalog placements, multi-card carousels) didn't exist as a v1 named-format archetype.\n\nLets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The 4 inherently-v2 canonicals at 3.1 GA: `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`.").default(true), "since_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version that introduced this canonical (e.g., '3.1', '3.2'). Lets adopters reason about minimum protocol version requirements when consuming a format declaration. Patch precision is intentionally rejected — canonicals are introduced at minor-version boundaries.").optional(), "migration_target_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version by which the working group expects this canonical to stabilize, surface a breaking revision, or (when `deprecated: true`) be removed. Patch precision is intentionally rejected — canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown').").optional(), "composition_model": z.enum(["deterministic","algorithmic"]).describe("Whether the surface composes deterministically (buyer can predict per-slot rendering — sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing — responsive_creative, agent_placement).").optional(), "provenance_required": z.boolean().describe("When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent.").optional(), "platform_extensions": z.array(z.object({ "uri": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL identifying the extension. `https://` is mandatory — `http://`, `file://`, `data:`, and other schemes are rejected at the schema layer (defense-in-depth on top of the fetch-contract normative rules). The URI base is the owning agent's URL; the path identifies the extension within that agent. Example: 'https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel'. The full fetch contract — SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds — is documented on `product-format-declaration.json#format_schema` and applies to ALL fetches of this reference shape regardless of whether the field is named `format_schema` (load-bearing for validation) or `platform_extensions` (informational); the *transport* rules are identical, only the *consumption* semantics differ."), "digest": z.string().regex(new RegExp("^sha256:[a-f0-9]{64}$")).describe("SHA-256 content digest of the extension definition (sha256:). Used to detect drift — if the agent revises the extension, the digest changes and cached definitions become invalid.") }).catchall(z.any()).describe("Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` — divergent digests in one response indicate producer-side error (e.g., concurrent extension revision mid-render). Buyers encountering divergent digests for the same URI MUST fail closed: treat all references to that URI as unresolved and surface a validation error rather than picking one branch silently. **Across responses**, digest divergence is normal — extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.")).describe("Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\n\n**Collision precedence (normative).** When two or more `platform_extensions[]` entries on the same declaration extend the same target (e.g., both extend `tracking`) with overlapping field names, **array order is authoritative — later entries override earlier ones on a per-field basis** (last-in-array-wins). SDKs MUST surface the overlap via the `errors[]` array on the `get_products` response with a structured code (`FORMAT_DECLARATION_DIVERGENT` is appropriate when the overlap appears across dual-emitted shapes; a producer-self-emitted overlap on a single declaration SHOULD use the same code with `error.details: { collision_kind: \"platform_extension_field\", target, overlapping_fields, winning_extension_uri }`). Producers SHOULD avoid the collision by emitting one extension per target or by partitioning fields across extensions; the deterministic precedence is for last-resort consistency across SDK implementations, not a sanctioned merging strategy.").optional(), "synthesis_nondeterministic": z.boolean().describe("When true, the format's production pipeline is genuinely nondeterministic — the platform cannot guarantee that synthesis from a given input set produces in-spec output. Veo / Sora / Runway-class generative video, and other AI-synthesis flows where output dimensions, duration, or quality vary per run. Implies a different validation contract: predictive `validate_input` is impossible; the platform's own post-synthesis QA loop applies; if the QA loop exhausts without producing a valid artifact, `build_creative` returns task_failed with a synthesis_failed reason. Distinct from `composition_model` (which describes how the surface composes per-slot rendering, not whether synthesis is deterministic). When false or absent, the format's production is predictable enough that `validate_input` can predict output properties from input properties.\n\n**Compatibility with `asset_source` / `item_production_model`**: `synthesis_nondeterministic: true` MAY pair with any of `seller_pre_rendered_from_brief`, `seller_human_designed`, or `agent_synthesized` (the QA loop is concept-level, not source-specific — 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.").default(false), "slots": z.array(z.object({ "asset_group_id": z.string().describe("Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings."), "asset_type": z.enum(["image","video","audio","text","markdown","url","html","css","javascript","vast","daast","webhook","brief","catalog","published_post","zip","card","object","pixel_tracker","vast_tracker","daast_tracker"]).describe("Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `published_post` is an existing-post reference asset, not uploaded media bytes and not a catalog row. `card` is the multi-card carousel element type (see card-asset.json). `pixel_tracker` / `vast_tracker` / `daast_tracker` are the renderer-fired measurement-tracker primitives — see `/schemas/core/assets/pixel-tracker-asset.json` and the VAST / DAAST tracker schemas. `object` is a last-resort fallback for structured non-asset inputs that don't fit any primitive asset_type — prefer specific types whenever possible."), "required": z.boolean().describe("Whether this slot is required for a valid manifest.").default(false), "min": z.number().int().gte(0).describe("Minimum count for repeatable / pool slots.").optional(), "max": z.number().int().gte(1).describe("Maximum count for repeatable / pool slots.").optional(), "max_chars": z.number().int().gte(1).describe("Per-slot character limit. Valid only when `asset_type` is `text`, `markdown`, or `brief`. Mutually exclusive with `max_size_kb` (which applies to binary asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "max_size_kb": z.number().int().gte(1).describe("Per-slot file size limit in kilobytes. Valid only when `asset_type` is `image`, `video`, `audio`, or `zip`. Mutually exclusive with `max_chars` (which applies to text asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Accepted intrinsic-pixel densities for this image-bearing slot. Valid when `asset_type` is `image`, and on a `card` slot where it constrains each card's image media (video media is unaffected). This makes density available to every canonical carrying image assets (native, carousel, responsive, companion images, and image itself), not only `format_kind: image`. When the image canonical also declares top-level `params.pixel_ratios`, the effective set is the intersection; an empty intersection is invalid. One matching asset satisfies the slot unless `required_pixel_ratios` requires rendition coverage.").optional(), "required_pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Required density coverage for an image rendition set. Valid only when `asset_type` is `image` and `pixel_ratios` is also declared. Every value MUST appear in the effective accepted set after intersecting any top-level image `params.pixel_ratios`, and the manifest slot value MUST be an array containing exactly one matching image rendition for each required ratio. Other accepted ratios remain optional. For example, `pixel_ratios: [1, 1.5, 2]` with `required_pixel_ratios: [1, 2]` requires the 1x and 2x renditions while making 1.5x optional. SDKs enforce intersection, subset, coverage, and duplicate-ratio rules because JSON Schema draft-07 cannot express them generically.").optional(), "logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("When `asset_group_id` is `logo`, renderer-facing brand.json logo slots acceptable for this format slot. Producers selecting from brand.json SHOULD prefer `logos[]` entries whose `slots[]` intersects this list, then apply `visual_guidelines.logo_usage_rules[]`.").optional(), "required_logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Subset of `logo_slots` for which this format expects explicit logo coverage. A manifest or brand-derived logo pool SHOULD include at least one usable logo for each required slot; if coverage is missing, builders SHOULD surface a validation warning or approval mapping instead of guessing from prose.").optional(), "description": z.string().describe("Human-readable description of what the slot expects from the buyer.").optional(), "consumed_for_production": z.boolean().describe("Dispatch hint for `build_creative` and v1↔v2 wire translators: when `true`, the slot's value is consumed as INPUT to a production step (host-read script, brief copy fed to generative synthesis, catalog feed driving per-SKU rendering) and is not rendered verbatim. When `false` (default), the slot's value is rendered verbatim on the placement (image bytes, video file, display tag).\n\nMotivates the v1↔v2 dispatch table: pre-v2 buyers shipped production-consumed inputs separately in a `inputs` map on the build_creative request; v2 collapses inputs and rendered assets into a single `assets` map keyed by `asset_group_id`. SDK translators between v1 and v2 use this flag per canonical to know which assets in the v2 manifest map back to v1 `inputs` vs v1 `assets`. Without the per-slot flag the dispatch table lives in adopter code and every SDK gets it slightly different.\n\nProducers SHOULD set this explicitly on slots whose consumption pattern isn't obvious (host-read scripts on `audio_hosted`, briefs on generative `video_hosted`, catalog feeds on `sponsored_placement`). For canonicals where every slot is render-verbatim (`image`, `display_tag`, `video_vast`), the default `false` is sufficient and the flag MAY be omitted.").default(false) }).catchall(z.any()).and(z.intersection(z.intersection(z.any(), z.intersection(z.any(), z.any())), z.intersection(z.any(), z.intersection(z.any(), z.any()))))).describe("Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.").optional(), "required_connections": z.array(z.object({ "provider": z.string().describe("Stable provider or platform namespace, preferably lowercase. Examples: `social.example`, `shortvideo.example`, or a seller-defined namespace. Omit only when the requirement is provider-agnostic, or when an `authorization_url` fully routes the human to the correct provider-specific connection flow.").optional(), "connection_type": z.enum(["advertiser_account","publisher_identity","post_authorization"]).describe("Kind of downstream connection required. `advertiser_account` is the platform account used to buy/manage ads. `publisher_identity` is the creator, page, channel, organization, or profile that owns source posts. `post_authorization` is a post-scoped grant when the platform authorizes individual posts instead of, or in addition to, the owning identity."), "required_for": z.array(z.string().min(1)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Concrete AdCP protocol operation names that require this downstream connection. Sellers SHOULD include this in product declarations when the requirement is known ahead of time, and in AUTHORIZATION_REQUIRED details when it explains the failed operation. Prefer specific operation names such as `list_creatives`, `sync_creatives`, `create_media_buy`, `get_media_buy_delivery`, or `get_creative_delivery` over broad category labels such as `reporting`.").optional(), "scope": z.enum(["account","identity","post","unknown"]).describe("Granularity of the downstream grant.").optional(), "status": z.enum(["connected","missing","pending","expired","revoked","not_required","unknown"]).describe("Current seller-observed state for this downstream connection when known. Product declarations MAY omit status or use `unknown`; AUTHORIZATION_REQUIRED details SHOULD use `missing`, `expired`, or `revoked` for the connection that blocked the call.").optional(), "connection_id": z.string().describe("Seller-defined identifier for an already-created downstream connection. Omit when no connection exists yet or when exposing it would leak platform/account state.").optional(), "resource_ref": z.object({ "platform_account_id": z.string().describe("Provider-native advertiser or business account id, when safe to disclose.").optional(), "identity_id": z.string().describe("Provider-native creator, page, channel, organization, or profile id, when safe to disclose.").optional(), "handle": z.string().describe("Provider-native public handle for the owning identity, when available.").optional(), "profile_url": z.string().url().describe("Public URL for the owning identity, when available.").optional(), "post_id": z.string().describe("Provider-native post id, when the grant is post-scoped or the failed request referenced a specific post.").optional(), "post_url": z.string().url().describe("Public URL for the referenced post, when available.").optional() }).catchall(z.any()).describe("Optional opaque provider-native resource hint, such as a platform account id, profile URL, handle, channel id, post id, or post URL. This is a hint for routing authorization, not proof that authorization exists.").optional(), "authorization_url": z.string().url().describe("Seller-hosted or provider-hosted URL where a human can complete or restore this downstream connection.").optional(), "authorization_instructions": z.string().describe("Human-readable instructions for completing or restoring this downstream connection.").optional(), "expires_at": z.string().datetime().describe("Expiration time for the downstream grant, when known.").optional() }).catchall(z.any()).and(z.any()).describe("A seller/platform-side connection or grant required by a product, format, or request. This is not the AdCP caller credential: the AdCP request is still authenticated once, and the seller uses these stored downstream connections to call a platform or service on the buyer's behalf. Use this shape for platforms that require more than one downstream grant, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.")).describe("Downstream platform connections or grants required to use this format declaration. These are in addition to the single AdCP caller credential. Use this when a platform product requires multiple downstream grants, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.").optional(), "reference_mutability": z.enum(["immutable_snapshot","mutable_requires_reapproval","mutable_auto_recheck"]).describe("Policy for formats whose `slots` accept a `published_post` reference. `immutable_snapshot`: seller snapshots the referenced post at approval and later source changes do not change the served creative. `mutable_requires_reapproval`: the source post may change and material changes require review before continued serving. `mutable_auto_recheck`: the source post may change and the seller continuously or periodically rechecks authorization/policy without requiring buyer resubmission. Omit when the format has no `published_post` slot.").optional(), "production_window_business_days": z.number().int().gte(0).describe("Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative).").optional() }).catchall(z.any()).describe("Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).").describe("VAST-tag-delivered video creative. Slot: `vast_tag` (vast asset, URL or inline XML, VAST 2.x-4.x). Tracking model: VAST events inherent to the spec — `impression`, `firstQuartile`, `midpoint`, `thirdQuartile`, `complete`, `start`, `pause`, `resume`, `mute`, `unmute`, `expand`, `collapse`, `fullscreen`, `creativeView`, `clickTracking`, `error`. VPAID interactivity via `vpaid_enabled: true` flag. SIMID extensions for interactive video supported as VAST extensions. Orientation is a parameter (vertical / horizontal / square). Distinct from `video_hosted` (direct file with external tracking).") }), z.object({ "format_kind": z.literal("audio_hosted"), "params": z.object({ "experimental": z.boolean().describe("When true, this canonical or seller narrowing may not work as declared. Adopters SHOULD preflight it with validate_input or in a sandbox and SHOULD NOT route production budget without testing; experimental status never makes the deprecated v1 path preferable. Drivers include unsettled spec shape, an adopter runtime gap, and custom shapes awaiting promotion. This replaces the earlier status plus runtime_status axes. Sellers SHOULD set experimental whenever a canonical or declaration is not production-ready.").default(false), "deprecated": z.boolean().describe("When true, this canonical (or a seller's specific narrowing of it) is going away. Existing adopters are supported through the deprecation cycle; new adoption is discouraged. Pair with `migration_target_version` to indicate when the canonical is expected to be removed. Distinct from `experimental`: an experimental canonical may stabilize and stop being experimental; a deprecated canonical is on a sunset path.").default(false), "v1_translatable": z.boolean().describe("Whether this canonical has any v1 named-format equivalent. `true` (default) — the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1→v2 projection via `v1-canonical-mapping.json` is meaningful. `false` — the canonical is inherently new in v2 and has no v1 form; v1's `list_creative_formats` couldn't express it because the underlying concept (algorithmic surface composition, AI-surface mentions, retail-media catalog placements, multi-card carousels) didn't exist as a v1 named-format archetype.\n\nLets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The 4 inherently-v2 canonicals at 3.1 GA: `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`.").default(true), "since_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version that introduced this canonical (e.g., '3.1', '3.2'). Lets adopters reason about minimum protocol version requirements when consuming a format declaration. Patch precision is intentionally rejected — canonicals are introduced at minor-version boundaries.").optional(), "migration_target_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version by which the working group expects this canonical to stabilize, surface a breaking revision, or (when `deprecated: true`) be removed. Patch precision is intentionally rejected — canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown').").optional(), "composition_model": z.enum(["deterministic","algorithmic"]).describe("Whether the surface composes deterministically (buyer can predict per-slot rendering — sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing — responsive_creative, agent_placement).").optional(), "provenance_required": z.boolean().describe("When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent.").optional(), "platform_extensions": z.array(z.object({ "uri": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL identifying the extension. `https://` is mandatory — `http://`, `file://`, `data:`, and other schemes are rejected at the schema layer (defense-in-depth on top of the fetch-contract normative rules). The URI base is the owning agent's URL; the path identifies the extension within that agent. Example: 'https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel'. The full fetch contract — SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds — is documented on `product-format-declaration.json#format_schema` and applies to ALL fetches of this reference shape regardless of whether the field is named `format_schema` (load-bearing for validation) or `platform_extensions` (informational); the *transport* rules are identical, only the *consumption* semantics differ."), "digest": z.string().regex(new RegExp("^sha256:[a-f0-9]{64}$")).describe("SHA-256 content digest of the extension definition (sha256:). Used to detect drift — if the agent revises the extension, the digest changes and cached definitions become invalid.") }).catchall(z.any()).describe("Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` — divergent digests in one response indicate producer-side error (e.g., concurrent extension revision mid-render). Buyers encountering divergent digests for the same URI MUST fail closed: treat all references to that URI as unresolved and surface a validation error rather than picking one branch silently. **Across responses**, digest divergence is normal — extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.")).describe("Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\n\n**Collision precedence (normative).** When two or more `platform_extensions[]` entries on the same declaration extend the same target (e.g., both extend `tracking`) with overlapping field names, **array order is authoritative — later entries override earlier ones on a per-field basis** (last-in-array-wins). SDKs MUST surface the overlap via the `errors[]` array on the `get_products` response with a structured code (`FORMAT_DECLARATION_DIVERGENT` is appropriate when the overlap appears across dual-emitted shapes; a producer-self-emitted overlap on a single declaration SHOULD use the same code with `error.details: { collision_kind: \"platform_extension_field\", target, overlapping_fields, winning_extension_uri }`). Producers SHOULD avoid the collision by emitting one extension per target or by partitioning fields across extensions; the deterministic precedence is for last-resort consistency across SDK implementations, not a sanctioned merging strategy.").optional(), "synthesis_nondeterministic": z.boolean().describe("When true, the format's production pipeline is genuinely nondeterministic — the platform cannot guarantee that synthesis from a given input set produces in-spec output. Veo / Sora / Runway-class generative video, and other AI-synthesis flows where output dimensions, duration, or quality vary per run. Implies a different validation contract: predictive `validate_input` is impossible; the platform's own post-synthesis QA loop applies; if the QA loop exhausts without producing a valid artifact, `build_creative` returns task_failed with a synthesis_failed reason. Distinct from `composition_model` (which describes how the surface composes per-slot rendering, not whether synthesis is deterministic). When false or absent, the format's production is predictable enough that `validate_input` can predict output properties from input properties.\n\n**Compatibility with `asset_source` / `item_production_model`**: `synthesis_nondeterministic: true` MAY pair with any of `seller_pre_rendered_from_brief`, `seller_human_designed`, or `agent_synthesized` (the QA loop is concept-level, not source-specific — 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.").default(false), "slots": z.array(z.object({ "asset_group_id": z.string().describe("Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings."), "asset_type": z.enum(["image","video","audio","text","markdown","url","html","css","javascript","vast","daast","webhook","brief","catalog","published_post","zip","card","object","pixel_tracker","vast_tracker","daast_tracker"]).describe("Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `published_post` is an existing-post reference asset, not uploaded media bytes and not a catalog row. `card` is the multi-card carousel element type (see card-asset.json). `pixel_tracker` / `vast_tracker` / `daast_tracker` are the renderer-fired measurement-tracker primitives — see `/schemas/core/assets/pixel-tracker-asset.json` and the VAST / DAAST tracker schemas. `object` is a last-resort fallback for structured non-asset inputs that don't fit any primitive asset_type — prefer specific types whenever possible."), "required": z.boolean().describe("Whether this slot is required for a valid manifest.").default(false), "min": z.number().int().gte(0).describe("Minimum count for repeatable / pool slots.").optional(), "max": z.number().int().gte(1).describe("Maximum count for repeatable / pool slots.").optional(), "max_chars": z.number().int().gte(1).describe("Per-slot character limit. Valid only when `asset_type` is `text`, `markdown`, or `brief`. Mutually exclusive with `max_size_kb` (which applies to binary asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "max_size_kb": z.number().int().gte(1).describe("Per-slot file size limit in kilobytes. Valid only when `asset_type` is `image`, `video`, `audio`, or `zip`. Mutually exclusive with `max_chars` (which applies to text asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Accepted intrinsic-pixel densities for this image-bearing slot. Valid when `asset_type` is `image`, and on a `card` slot where it constrains each card's image media (video media is unaffected). This makes density available to every canonical carrying image assets (native, carousel, responsive, companion images, and image itself), not only `format_kind: image`. When the image canonical also declares top-level `params.pixel_ratios`, the effective set is the intersection; an empty intersection is invalid. One matching asset satisfies the slot unless `required_pixel_ratios` requires rendition coverage.").optional(), "required_pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Required density coverage for an image rendition set. Valid only when `asset_type` is `image` and `pixel_ratios` is also declared. Every value MUST appear in the effective accepted set after intersecting any top-level image `params.pixel_ratios`, and the manifest slot value MUST be an array containing exactly one matching image rendition for each required ratio. Other accepted ratios remain optional. For example, `pixel_ratios: [1, 1.5, 2]` with `required_pixel_ratios: [1, 2]` requires the 1x and 2x renditions while making 1.5x optional. SDKs enforce intersection, subset, coverage, and duplicate-ratio rules because JSON Schema draft-07 cannot express them generically.").optional(), "logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("When `asset_group_id` is `logo`, renderer-facing brand.json logo slots acceptable for this format slot. Producers selecting from brand.json SHOULD prefer `logos[]` entries whose `slots[]` intersects this list, then apply `visual_guidelines.logo_usage_rules[]`.").optional(), "required_logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Subset of `logo_slots` for which this format expects explicit logo coverage. A manifest or brand-derived logo pool SHOULD include at least one usable logo for each required slot; if coverage is missing, builders SHOULD surface a validation warning or approval mapping instead of guessing from prose.").optional(), "description": z.string().describe("Human-readable description of what the slot expects from the buyer.").optional(), "consumed_for_production": z.boolean().describe("Dispatch hint for `build_creative` and v1↔v2 wire translators: when `true`, the slot's value is consumed as INPUT to a production step (host-read script, brief copy fed to generative synthesis, catalog feed driving per-SKU rendering) and is not rendered verbatim. When `false` (default), the slot's value is rendered verbatim on the placement (image bytes, video file, display tag).\n\nMotivates the v1↔v2 dispatch table: pre-v2 buyers shipped production-consumed inputs separately in a `inputs` map on the build_creative request; v2 collapses inputs and rendered assets into a single `assets` map keyed by `asset_group_id`. SDK translators between v1 and v2 use this flag per canonical to know which assets in the v2 manifest map back to v1 `inputs` vs v1 `assets`. Without the per-slot flag the dispatch table lives in adopter code and every SDK gets it slightly different.\n\nProducers SHOULD set this explicitly on slots whose consumption pattern isn't obvious (host-read scripts on `audio_hosted`, briefs on generative `video_hosted`, catalog feeds on `sponsored_placement`). For canonicals where every slot is render-verbatim (`image`, `display_tag`, `video_vast`), the default `false` is sufficient and the flag MAY be omitted.").default(false) }).catchall(z.any()).and(z.intersection(z.intersection(z.any(), z.intersection(z.any(), z.any())), z.intersection(z.any(), z.intersection(z.any(), z.any()))))).describe("Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.").optional(), "required_connections": z.array(z.object({ "provider": z.string().describe("Stable provider or platform namespace, preferably lowercase. Examples: `social.example`, `shortvideo.example`, or a seller-defined namespace. Omit only when the requirement is provider-agnostic, or when an `authorization_url` fully routes the human to the correct provider-specific connection flow.").optional(), "connection_type": z.enum(["advertiser_account","publisher_identity","post_authorization"]).describe("Kind of downstream connection required. `advertiser_account` is the platform account used to buy/manage ads. `publisher_identity` is the creator, page, channel, organization, or profile that owns source posts. `post_authorization` is a post-scoped grant when the platform authorizes individual posts instead of, or in addition to, the owning identity."), "required_for": z.array(z.string().min(1)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Concrete AdCP protocol operation names that require this downstream connection. Sellers SHOULD include this in product declarations when the requirement is known ahead of time, and in AUTHORIZATION_REQUIRED details when it explains the failed operation. Prefer specific operation names such as `list_creatives`, `sync_creatives`, `create_media_buy`, `get_media_buy_delivery`, or `get_creative_delivery` over broad category labels such as `reporting`.").optional(), "scope": z.enum(["account","identity","post","unknown"]).describe("Granularity of the downstream grant.").optional(), "status": z.enum(["connected","missing","pending","expired","revoked","not_required","unknown"]).describe("Current seller-observed state for this downstream connection when known. Product declarations MAY omit status or use `unknown`; AUTHORIZATION_REQUIRED details SHOULD use `missing`, `expired`, or `revoked` for the connection that blocked the call.").optional(), "connection_id": z.string().describe("Seller-defined identifier for an already-created downstream connection. Omit when no connection exists yet or when exposing it would leak platform/account state.").optional(), "resource_ref": z.object({ "platform_account_id": z.string().describe("Provider-native advertiser or business account id, when safe to disclose.").optional(), "identity_id": z.string().describe("Provider-native creator, page, channel, organization, or profile id, when safe to disclose.").optional(), "handle": z.string().describe("Provider-native public handle for the owning identity, when available.").optional(), "profile_url": z.string().url().describe("Public URL for the owning identity, when available.").optional(), "post_id": z.string().describe("Provider-native post id, when the grant is post-scoped or the failed request referenced a specific post.").optional(), "post_url": z.string().url().describe("Public URL for the referenced post, when available.").optional() }).catchall(z.any()).describe("Optional opaque provider-native resource hint, such as a platform account id, profile URL, handle, channel id, post id, or post URL. This is a hint for routing authorization, not proof that authorization exists.").optional(), "authorization_url": z.string().url().describe("Seller-hosted or provider-hosted URL where a human can complete or restore this downstream connection.").optional(), "authorization_instructions": z.string().describe("Human-readable instructions for completing or restoring this downstream connection.").optional(), "expires_at": z.string().datetime().describe("Expiration time for the downstream grant, when known.").optional() }).catchall(z.any()).and(z.any()).describe("A seller/platform-side connection or grant required by a product, format, or request. This is not the AdCP caller credential: the AdCP request is still authenticated once, and the seller uses these stored downstream connections to call a platform or service on the buyer's behalf. Use this shape for platforms that require more than one downstream grant, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.")).describe("Downstream platform connections or grants required to use this format declaration. These are in addition to the single AdCP caller credential. Use this when a platform product requires multiple downstream grants, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.").optional(), "reference_mutability": z.enum(["immutable_snapshot","mutable_requires_reapproval","mutable_auto_recheck"]).describe("Policy for formats whose `slots` accept a `published_post` reference. `immutable_snapshot`: seller snapshots the referenced post at approval and later source changes do not change the served creative. `mutable_requires_reapproval`: the source post may change and material changes require review before continued serving. `mutable_auto_recheck`: the source post may change and the seller continuously or periodically rechecks authorization/policy without requiring buyer resubmission. Omit when the format has no `published_post` slot.").optional(), "production_window_business_days": z.number().int().gte(0).describe("Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative).").optional() }).catchall(z.any()).describe("Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).").describe("Direct audio creative — buyer ships an `audio` asset (mp3/aac/wav) for asset-driven products, or ships a `script` / `creative_brief` text asset for products where the seller produces audio internally (podcast host-reads, TTS synthesis). Optional companion slots: `companion_image`, `brand_name`, `landing_page_url`. Tracking model: standard impression + completion + companion-image-click pixels via universal_macros. Distinct from `audio_daast` (DAAST tag, inherent DAAST event tracking). For host-reads and synthesized audio, the format declares `asset_source: 'publisher_host_recorded'` or `'agent_synthesized'` plus `buyer_asset_acceptance: 'rejected'`; the format's `slots` declaration enumerates which assets the buyer ships (e.g., `script` text asset for host-reads). The seller decides how to consume each asset (render verbatim vs produce audio from text) — there is no separate manifest 'inputs' map; everything the buyer ships goes in `assets`.") }), z.object({ "format_kind": z.literal("audio_daast"), "params": z.object({ "experimental": z.boolean().describe("When true, this canonical or seller narrowing may not work as declared. Adopters SHOULD preflight it with validate_input or in a sandbox and SHOULD NOT route production budget without testing; experimental status never makes the deprecated v1 path preferable. Drivers include unsettled spec shape, an adopter runtime gap, and custom shapes awaiting promotion. This replaces the earlier status plus runtime_status axes. Sellers SHOULD set experimental whenever a canonical or declaration is not production-ready.").default(false), "deprecated": z.boolean().describe("When true, this canonical (or a seller's specific narrowing of it) is going away. Existing adopters are supported through the deprecation cycle; new adoption is discouraged. Pair with `migration_target_version` to indicate when the canonical is expected to be removed. Distinct from `experimental`: an experimental canonical may stabilize and stop being experimental; a deprecated canonical is on a sunset path.").default(false), "v1_translatable": z.boolean().describe("Whether this canonical has any v1 named-format equivalent. `true` (default) — the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1→v2 projection via `v1-canonical-mapping.json` is meaningful. `false` — the canonical is inherently new in v2 and has no v1 form; v1's `list_creative_formats` couldn't express it because the underlying concept (algorithmic surface composition, AI-surface mentions, retail-media catalog placements, multi-card carousels) didn't exist as a v1 named-format archetype.\n\nLets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The 4 inherently-v2 canonicals at 3.1 GA: `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`.").default(true), "since_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version that introduced this canonical (e.g., '3.1', '3.2'). Lets adopters reason about minimum protocol version requirements when consuming a format declaration. Patch precision is intentionally rejected — canonicals are introduced at minor-version boundaries.").optional(), "migration_target_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version by which the working group expects this canonical to stabilize, surface a breaking revision, or (when `deprecated: true`) be removed. Patch precision is intentionally rejected — canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown').").optional(), "composition_model": z.enum(["deterministic","algorithmic"]).describe("Whether the surface composes deterministically (buyer can predict per-slot rendering — sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing — responsive_creative, agent_placement).").optional(), "provenance_required": z.boolean().describe("When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent.").optional(), "platform_extensions": z.array(z.object({ "uri": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL identifying the extension. `https://` is mandatory — `http://`, `file://`, `data:`, and other schemes are rejected at the schema layer (defense-in-depth on top of the fetch-contract normative rules). The URI base is the owning agent's URL; the path identifies the extension within that agent. Example: 'https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel'. The full fetch contract — SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds — is documented on `product-format-declaration.json#format_schema` and applies to ALL fetches of this reference shape regardless of whether the field is named `format_schema` (load-bearing for validation) or `platform_extensions` (informational); the *transport* rules are identical, only the *consumption* semantics differ."), "digest": z.string().regex(new RegExp("^sha256:[a-f0-9]{64}$")).describe("SHA-256 content digest of the extension definition (sha256:). Used to detect drift — if the agent revises the extension, the digest changes and cached definitions become invalid.") }).catchall(z.any()).describe("Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` — divergent digests in one response indicate producer-side error (e.g., concurrent extension revision mid-render). Buyers encountering divergent digests for the same URI MUST fail closed: treat all references to that URI as unresolved and surface a validation error rather than picking one branch silently. **Across responses**, digest divergence is normal — extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.")).describe("Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\n\n**Collision precedence (normative).** When two or more `platform_extensions[]` entries on the same declaration extend the same target (e.g., both extend `tracking`) with overlapping field names, **array order is authoritative — later entries override earlier ones on a per-field basis** (last-in-array-wins). SDKs MUST surface the overlap via the `errors[]` array on the `get_products` response with a structured code (`FORMAT_DECLARATION_DIVERGENT` is appropriate when the overlap appears across dual-emitted shapes; a producer-self-emitted overlap on a single declaration SHOULD use the same code with `error.details: { collision_kind: \"platform_extension_field\", target, overlapping_fields, winning_extension_uri }`). Producers SHOULD avoid the collision by emitting one extension per target or by partitioning fields across extensions; the deterministic precedence is for last-resort consistency across SDK implementations, not a sanctioned merging strategy.").optional(), "synthesis_nondeterministic": z.boolean().describe("When true, the format's production pipeline is genuinely nondeterministic — the platform cannot guarantee that synthesis from a given input set produces in-spec output. Veo / Sora / Runway-class generative video, and other AI-synthesis flows where output dimensions, duration, or quality vary per run. Implies a different validation contract: predictive `validate_input` is impossible; the platform's own post-synthesis QA loop applies; if the QA loop exhausts without producing a valid artifact, `build_creative` returns task_failed with a synthesis_failed reason. Distinct from `composition_model` (which describes how the surface composes per-slot rendering, not whether synthesis is deterministic). When false or absent, the format's production is predictable enough that `validate_input` can predict output properties from input properties.\n\n**Compatibility with `asset_source` / `item_production_model`**: `synthesis_nondeterministic: true` MAY pair with any of `seller_pre_rendered_from_brief`, `seller_human_designed`, or `agent_synthesized` (the QA loop is concept-level, not source-specific — 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.").default(false), "slots": z.array(z.object({ "asset_group_id": z.string().describe("Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings."), "asset_type": z.enum(["image","video","audio","text","markdown","url","html","css","javascript","vast","daast","webhook","brief","catalog","published_post","zip","card","object","pixel_tracker","vast_tracker","daast_tracker"]).describe("Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `published_post` is an existing-post reference asset, not uploaded media bytes and not a catalog row. `card` is the multi-card carousel element type (see card-asset.json). `pixel_tracker` / `vast_tracker` / `daast_tracker` are the renderer-fired measurement-tracker primitives — see `/schemas/core/assets/pixel-tracker-asset.json` and the VAST / DAAST tracker schemas. `object` is a last-resort fallback for structured non-asset inputs that don't fit any primitive asset_type — prefer specific types whenever possible."), "required": z.boolean().describe("Whether this slot is required for a valid manifest.").default(false), "min": z.number().int().gte(0).describe("Minimum count for repeatable / pool slots.").optional(), "max": z.number().int().gte(1).describe("Maximum count for repeatable / pool slots.").optional(), "max_chars": z.number().int().gte(1).describe("Per-slot character limit. Valid only when `asset_type` is `text`, `markdown`, or `brief`. Mutually exclusive with `max_size_kb` (which applies to binary asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "max_size_kb": z.number().int().gte(1).describe("Per-slot file size limit in kilobytes. Valid only when `asset_type` is `image`, `video`, `audio`, or `zip`. Mutually exclusive with `max_chars` (which applies to text asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Accepted intrinsic-pixel densities for this image-bearing slot. Valid when `asset_type` is `image`, and on a `card` slot where it constrains each card's image media (video media is unaffected). This makes density available to every canonical carrying image assets (native, carousel, responsive, companion images, and image itself), not only `format_kind: image`. When the image canonical also declares top-level `params.pixel_ratios`, the effective set is the intersection; an empty intersection is invalid. One matching asset satisfies the slot unless `required_pixel_ratios` requires rendition coverage.").optional(), "required_pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Required density coverage for an image rendition set. Valid only when `asset_type` is `image` and `pixel_ratios` is also declared. Every value MUST appear in the effective accepted set after intersecting any top-level image `params.pixel_ratios`, and the manifest slot value MUST be an array containing exactly one matching image rendition for each required ratio. Other accepted ratios remain optional. For example, `pixel_ratios: [1, 1.5, 2]` with `required_pixel_ratios: [1, 2]` requires the 1x and 2x renditions while making 1.5x optional. SDKs enforce intersection, subset, coverage, and duplicate-ratio rules because JSON Schema draft-07 cannot express them generically.").optional(), "logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("When `asset_group_id` is `logo`, renderer-facing brand.json logo slots acceptable for this format slot. Producers selecting from brand.json SHOULD prefer `logos[]` entries whose `slots[]` intersects this list, then apply `visual_guidelines.logo_usage_rules[]`.").optional(), "required_logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Subset of `logo_slots` for which this format expects explicit logo coverage. A manifest or brand-derived logo pool SHOULD include at least one usable logo for each required slot; if coverage is missing, builders SHOULD surface a validation warning or approval mapping instead of guessing from prose.").optional(), "description": z.string().describe("Human-readable description of what the slot expects from the buyer.").optional(), "consumed_for_production": z.boolean().describe("Dispatch hint for `build_creative` and v1↔v2 wire translators: when `true`, the slot's value is consumed as INPUT to a production step (host-read script, brief copy fed to generative synthesis, catalog feed driving per-SKU rendering) and is not rendered verbatim. When `false` (default), the slot's value is rendered verbatim on the placement (image bytes, video file, display tag).\n\nMotivates the v1↔v2 dispatch table: pre-v2 buyers shipped production-consumed inputs separately in a `inputs` map on the build_creative request; v2 collapses inputs and rendered assets into a single `assets` map keyed by `asset_group_id`. SDK translators between v1 and v2 use this flag per canonical to know which assets in the v2 manifest map back to v1 `inputs` vs v1 `assets`. Without the per-slot flag the dispatch table lives in adopter code and every SDK gets it slightly different.\n\nProducers SHOULD set this explicitly on slots whose consumption pattern isn't obvious (host-read scripts on `audio_hosted`, briefs on generative `video_hosted`, catalog feeds on `sponsored_placement`). For canonicals where every slot is render-verbatim (`image`, `display_tag`, `video_vast`), the default `false` is sufficient and the flag MAY be omitted.").default(false) }).catchall(z.any()).and(z.intersection(z.intersection(z.any(), z.intersection(z.any(), z.any())), z.intersection(z.any(), z.intersection(z.any(), z.any()))))).describe("Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.").optional(), "required_connections": z.array(z.object({ "provider": z.string().describe("Stable provider or platform namespace, preferably lowercase. Examples: `social.example`, `shortvideo.example`, or a seller-defined namespace. Omit only when the requirement is provider-agnostic, or when an `authorization_url` fully routes the human to the correct provider-specific connection flow.").optional(), "connection_type": z.enum(["advertiser_account","publisher_identity","post_authorization"]).describe("Kind of downstream connection required. `advertiser_account` is the platform account used to buy/manage ads. `publisher_identity` is the creator, page, channel, organization, or profile that owns source posts. `post_authorization` is a post-scoped grant when the platform authorizes individual posts instead of, or in addition to, the owning identity."), "required_for": z.array(z.string().min(1)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Concrete AdCP protocol operation names that require this downstream connection. Sellers SHOULD include this in product declarations when the requirement is known ahead of time, and in AUTHORIZATION_REQUIRED details when it explains the failed operation. Prefer specific operation names such as `list_creatives`, `sync_creatives`, `create_media_buy`, `get_media_buy_delivery`, or `get_creative_delivery` over broad category labels such as `reporting`.").optional(), "scope": z.enum(["account","identity","post","unknown"]).describe("Granularity of the downstream grant.").optional(), "status": z.enum(["connected","missing","pending","expired","revoked","not_required","unknown"]).describe("Current seller-observed state for this downstream connection when known. Product declarations MAY omit status or use `unknown`; AUTHORIZATION_REQUIRED details SHOULD use `missing`, `expired`, or `revoked` for the connection that blocked the call.").optional(), "connection_id": z.string().describe("Seller-defined identifier for an already-created downstream connection. Omit when no connection exists yet or when exposing it would leak platform/account state.").optional(), "resource_ref": z.object({ "platform_account_id": z.string().describe("Provider-native advertiser or business account id, when safe to disclose.").optional(), "identity_id": z.string().describe("Provider-native creator, page, channel, organization, or profile id, when safe to disclose.").optional(), "handle": z.string().describe("Provider-native public handle for the owning identity, when available.").optional(), "profile_url": z.string().url().describe("Public URL for the owning identity, when available.").optional(), "post_id": z.string().describe("Provider-native post id, when the grant is post-scoped or the failed request referenced a specific post.").optional(), "post_url": z.string().url().describe("Public URL for the referenced post, when available.").optional() }).catchall(z.any()).describe("Optional opaque provider-native resource hint, such as a platform account id, profile URL, handle, channel id, post id, or post URL. This is a hint for routing authorization, not proof that authorization exists.").optional(), "authorization_url": z.string().url().describe("Seller-hosted or provider-hosted URL where a human can complete or restore this downstream connection.").optional(), "authorization_instructions": z.string().describe("Human-readable instructions for completing or restoring this downstream connection.").optional(), "expires_at": z.string().datetime().describe("Expiration time for the downstream grant, when known.").optional() }).catchall(z.any()).and(z.any()).describe("A seller/platform-side connection or grant required by a product, format, or request. This is not the AdCP caller credential: the AdCP request is still authenticated once, and the seller uses these stored downstream connections to call a platform or service on the buyer's behalf. Use this shape for platforms that require more than one downstream grant, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.")).describe("Downstream platform connections or grants required to use this format declaration. These are in addition to the single AdCP caller credential. Use this when a platform product requires multiple downstream grants, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.").optional(), "reference_mutability": z.enum(["immutable_snapshot","mutable_requires_reapproval","mutable_auto_recheck"]).describe("Policy for formats whose `slots` accept a `published_post` reference. `immutable_snapshot`: seller snapshots the referenced post at approval and later source changes do not change the served creative. `mutable_requires_reapproval`: the source post may change and material changes require review before continued serving. `mutable_auto_recheck`: the source post may change and the seller continuously or periodically rechecks authorization/policy without requiring buyer resubmission. Omit when the format has no `published_post` slot.").optional(), "production_window_business_days": z.number().int().gte(0).describe("Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative).").optional() }).catchall(z.any()).describe("Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).").describe("DAAST-tag-delivered audio creative (audio analog of VAST). Slot: `daast_tag` (daast asset, URL or inline XML). Tracking model: DAAST events inherent to the spec — `impression`, `firstQuartile`, `midpoint`, `thirdQuartile`, `complete`, `start`, `pause`, `resume`, `mute`, `unmute`, `clickTracking`, `error`. Distinct from `audio_hosted` (direct file with external tracking).") }), z.object({ "format_kind": z.literal("sponsored_placement"), "params": z.object({ "experimental": z.boolean().describe("When true, this canonical or seller narrowing may not work as declared. Adopters SHOULD preflight it with validate_input or in a sandbox and SHOULD NOT route production budget without testing; experimental status never makes the deprecated v1 path preferable. Drivers include unsettled spec shape, an adopter runtime gap, and custom shapes awaiting promotion. This replaces the earlier status plus runtime_status axes. Sellers SHOULD set experimental whenever a canonical or declaration is not production-ready.").default(false), "deprecated": z.boolean().describe("When true, this canonical (or a seller's specific narrowing of it) is going away. Existing adopters are supported through the deprecation cycle; new adoption is discouraged. Pair with `migration_target_version` to indicate when the canonical is expected to be removed. Distinct from `experimental`: an experimental canonical may stabilize and stop being experimental; a deprecated canonical is on a sunset path.").default(false), "v1_translatable": z.boolean().describe("Whether this canonical has any v1 named-format equivalent. `true` (default) — the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1→v2 projection via `v1-canonical-mapping.json` is meaningful. `false` — the canonical is inherently new in v2 and has no v1 form; v1's `list_creative_formats` couldn't express it because the underlying concept (algorithmic surface composition, AI-surface mentions, retail-media catalog placements, multi-card carousels) didn't exist as a v1 named-format archetype.\n\nLets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The 4 inherently-v2 canonicals at 3.1 GA: `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`.").default(true), "since_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version that introduced this canonical (e.g., '3.1', '3.2'). Lets adopters reason about minimum protocol version requirements when consuming a format declaration. Patch precision is intentionally rejected — canonicals are introduced at minor-version boundaries.").optional(), "migration_target_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version by which the working group expects this canonical to stabilize, surface a breaking revision, or (when `deprecated: true`) be removed. Patch precision is intentionally rejected — canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown').").optional(), "composition_model": z.enum(["deterministic","algorithmic"]).describe("Whether the surface composes deterministically (buyer can predict per-slot rendering — sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing — responsive_creative, agent_placement).").optional(), "provenance_required": z.boolean().describe("When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent.").optional(), "platform_extensions": z.array(z.object({ "uri": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL identifying the extension. `https://` is mandatory — `http://`, `file://`, `data:`, and other schemes are rejected at the schema layer (defense-in-depth on top of the fetch-contract normative rules). The URI base is the owning agent's URL; the path identifies the extension within that agent. Example: 'https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel'. The full fetch contract — SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds — is documented on `product-format-declaration.json#format_schema` and applies to ALL fetches of this reference shape regardless of whether the field is named `format_schema` (load-bearing for validation) or `platform_extensions` (informational); the *transport* rules are identical, only the *consumption* semantics differ."), "digest": z.string().regex(new RegExp("^sha256:[a-f0-9]{64}$")).describe("SHA-256 content digest of the extension definition (sha256:). Used to detect drift — if the agent revises the extension, the digest changes and cached definitions become invalid.") }).catchall(z.any()).describe("Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` — divergent digests in one response indicate producer-side error (e.g., concurrent extension revision mid-render). Buyers encountering divergent digests for the same URI MUST fail closed: treat all references to that URI as unresolved and surface a validation error rather than picking one branch silently. **Across responses**, digest divergence is normal — extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.")).describe("Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\n\n**Collision precedence (normative).** When two or more `platform_extensions[]` entries on the same declaration extend the same target (e.g., both extend `tracking`) with overlapping field names, **array order is authoritative — later entries override earlier ones on a per-field basis** (last-in-array-wins). SDKs MUST surface the overlap via the `errors[]` array on the `get_products` response with a structured code (`FORMAT_DECLARATION_DIVERGENT` is appropriate when the overlap appears across dual-emitted shapes; a producer-self-emitted overlap on a single declaration SHOULD use the same code with `error.details: { collision_kind: \"platform_extension_field\", target, overlapping_fields, winning_extension_uri }`). Producers SHOULD avoid the collision by emitting one extension per target or by partitioning fields across extensions; the deterministic precedence is for last-resort consistency across SDK implementations, not a sanctioned merging strategy.").optional(), "synthesis_nondeterministic": z.boolean().describe("When true, the format's production pipeline is genuinely nondeterministic — the platform cannot guarantee that synthesis from a given input set produces in-spec output. Veo / Sora / Runway-class generative video, and other AI-synthesis flows where output dimensions, duration, or quality vary per run. Implies a different validation contract: predictive `validate_input` is impossible; the platform's own post-synthesis QA loop applies; if the QA loop exhausts without producing a valid artifact, `build_creative` returns task_failed with a synthesis_failed reason. Distinct from `composition_model` (which describes how the surface composes per-slot rendering, not whether synthesis is deterministic). When false or absent, the format's production is predictable enough that `validate_input` can predict output properties from input properties.\n\n**Compatibility with `asset_source` / `item_production_model`**: `synthesis_nondeterministic: true` MAY pair with any of `seller_pre_rendered_from_brief`, `seller_human_designed`, or `agent_synthesized` (the QA loop is concept-level, not source-specific — 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.").default(false), "slots": z.array(z.object({ "asset_group_id": z.string().describe("Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings."), "asset_type": z.enum(["image","video","audio","text","markdown","url","html","css","javascript","vast","daast","webhook","brief","catalog","published_post","zip","card","object","pixel_tracker","vast_tracker","daast_tracker"]).describe("Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `published_post` is an existing-post reference asset, not uploaded media bytes and not a catalog row. `card` is the multi-card carousel element type (see card-asset.json). `pixel_tracker` / `vast_tracker` / `daast_tracker` are the renderer-fired measurement-tracker primitives — see `/schemas/core/assets/pixel-tracker-asset.json` and the VAST / DAAST tracker schemas. `object` is a last-resort fallback for structured non-asset inputs that don't fit any primitive asset_type — prefer specific types whenever possible."), "required": z.boolean().describe("Whether this slot is required for a valid manifest.").default(false), "min": z.number().int().gte(0).describe("Minimum count for repeatable / pool slots.").optional(), "max": z.number().int().gte(1).describe("Maximum count for repeatable / pool slots.").optional(), "max_chars": z.number().int().gte(1).describe("Per-slot character limit. Valid only when `asset_type` is `text`, `markdown`, or `brief`. Mutually exclusive with `max_size_kb` (which applies to binary asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "max_size_kb": z.number().int().gte(1).describe("Per-slot file size limit in kilobytes. Valid only when `asset_type` is `image`, `video`, `audio`, or `zip`. Mutually exclusive with `max_chars` (which applies to text asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Accepted intrinsic-pixel densities for this image-bearing slot. Valid when `asset_type` is `image`, and on a `card` slot where it constrains each card's image media (video media is unaffected). This makes density available to every canonical carrying image assets (native, carousel, responsive, companion images, and image itself), not only `format_kind: image`. When the image canonical also declares top-level `params.pixel_ratios`, the effective set is the intersection; an empty intersection is invalid. One matching asset satisfies the slot unless `required_pixel_ratios` requires rendition coverage.").optional(), "required_pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Required density coverage for an image rendition set. Valid only when `asset_type` is `image` and `pixel_ratios` is also declared. Every value MUST appear in the effective accepted set after intersecting any top-level image `params.pixel_ratios`, and the manifest slot value MUST be an array containing exactly one matching image rendition for each required ratio. Other accepted ratios remain optional. For example, `pixel_ratios: [1, 1.5, 2]` with `required_pixel_ratios: [1, 2]` requires the 1x and 2x renditions while making 1.5x optional. SDKs enforce intersection, subset, coverage, and duplicate-ratio rules because JSON Schema draft-07 cannot express them generically.").optional(), "logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("When `asset_group_id` is `logo`, renderer-facing brand.json logo slots acceptable for this format slot. Producers selecting from brand.json SHOULD prefer `logos[]` entries whose `slots[]` intersects this list, then apply `visual_guidelines.logo_usage_rules[]`.").optional(), "required_logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Subset of `logo_slots` for which this format expects explicit logo coverage. A manifest or brand-derived logo pool SHOULD include at least one usable logo for each required slot; if coverage is missing, builders SHOULD surface a validation warning or approval mapping instead of guessing from prose.").optional(), "description": z.string().describe("Human-readable description of what the slot expects from the buyer.").optional(), "consumed_for_production": z.boolean().describe("Dispatch hint for `build_creative` and v1↔v2 wire translators: when `true`, the slot's value is consumed as INPUT to a production step (host-read script, brief copy fed to generative synthesis, catalog feed driving per-SKU rendering) and is not rendered verbatim. When `false` (default), the slot's value is rendered verbatim on the placement (image bytes, video file, display tag).\n\nMotivates the v1↔v2 dispatch table: pre-v2 buyers shipped production-consumed inputs separately in a `inputs` map on the build_creative request; v2 collapses inputs and rendered assets into a single `assets` map keyed by `asset_group_id`. SDK translators between v1 and v2 use this flag per canonical to know which assets in the v2 manifest map back to v1 `inputs` vs v1 `assets`. Without the per-slot flag the dispatch table lives in adopter code and every SDK gets it slightly different.\n\nProducers SHOULD set this explicitly on slots whose consumption pattern isn't obvious (host-read scripts on `audio_hosted`, briefs on generative `video_hosted`, catalog feeds on `sponsored_placement`). For canonicals where every slot is render-verbatim (`image`, `display_tag`, `video_vast`), the default `false` is sufficient and the flag MAY be omitted.").default(false) }).catchall(z.any()).and(z.intersection(z.intersection(z.any(), z.intersection(z.any(), z.any())), z.intersection(z.any(), z.intersection(z.any(), z.any()))))).describe("Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.").optional(), "required_connections": z.array(z.object({ "provider": z.string().describe("Stable provider or platform namespace, preferably lowercase. Examples: `social.example`, `shortvideo.example`, or a seller-defined namespace. Omit only when the requirement is provider-agnostic, or when an `authorization_url` fully routes the human to the correct provider-specific connection flow.").optional(), "connection_type": z.enum(["advertiser_account","publisher_identity","post_authorization"]).describe("Kind of downstream connection required. `advertiser_account` is the platform account used to buy/manage ads. `publisher_identity` is the creator, page, channel, organization, or profile that owns source posts. `post_authorization` is a post-scoped grant when the platform authorizes individual posts instead of, or in addition to, the owning identity."), "required_for": z.array(z.string().min(1)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Concrete AdCP protocol operation names that require this downstream connection. Sellers SHOULD include this in product declarations when the requirement is known ahead of time, and in AUTHORIZATION_REQUIRED details when it explains the failed operation. Prefer specific operation names such as `list_creatives`, `sync_creatives`, `create_media_buy`, `get_media_buy_delivery`, or `get_creative_delivery` over broad category labels such as `reporting`.").optional(), "scope": z.enum(["account","identity","post","unknown"]).describe("Granularity of the downstream grant.").optional(), "status": z.enum(["connected","missing","pending","expired","revoked","not_required","unknown"]).describe("Current seller-observed state for this downstream connection when known. Product declarations MAY omit status or use `unknown`; AUTHORIZATION_REQUIRED details SHOULD use `missing`, `expired`, or `revoked` for the connection that blocked the call.").optional(), "connection_id": z.string().describe("Seller-defined identifier for an already-created downstream connection. Omit when no connection exists yet or when exposing it would leak platform/account state.").optional(), "resource_ref": z.object({ "platform_account_id": z.string().describe("Provider-native advertiser or business account id, when safe to disclose.").optional(), "identity_id": z.string().describe("Provider-native creator, page, channel, organization, or profile id, when safe to disclose.").optional(), "handle": z.string().describe("Provider-native public handle for the owning identity, when available.").optional(), "profile_url": z.string().url().describe("Public URL for the owning identity, when available.").optional(), "post_id": z.string().describe("Provider-native post id, when the grant is post-scoped or the failed request referenced a specific post.").optional(), "post_url": z.string().url().describe("Public URL for the referenced post, when available.").optional() }).catchall(z.any()).describe("Optional opaque provider-native resource hint, such as a platform account id, profile URL, handle, channel id, post id, or post URL. This is a hint for routing authorization, not proof that authorization exists.").optional(), "authorization_url": z.string().url().describe("Seller-hosted or provider-hosted URL where a human can complete or restore this downstream connection.").optional(), "authorization_instructions": z.string().describe("Human-readable instructions for completing or restoring this downstream connection.").optional(), "expires_at": z.string().datetime().describe("Expiration time for the downstream grant, when known.").optional() }).catchall(z.any()).and(z.any()).describe("A seller/platform-side connection or grant required by a product, format, or request. This is not the AdCP caller credential: the AdCP request is still authenticated once, and the seller uses these stored downstream connections to call a platform or service on the buyer's behalf. Use this shape for platforms that require more than one downstream grant, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.")).describe("Downstream platform connections or grants required to use this format declaration. These are in addition to the single AdCP caller credential. Use this when a platform product requires multiple downstream grants, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.").optional(), "reference_mutability": z.enum(["immutable_snapshot","mutable_requires_reapproval","mutable_auto_recheck"]).describe("Policy for formats whose `slots` accept a `published_post` reference. `immutable_snapshot`: seller snapshots the referenced post at approval and later source changes do not change the served creative. `mutable_requires_reapproval`: the source post may change and material changes require review before continued serving. `mutable_auto_recheck`: the source post may change and the seller continuously or periodically rechecks authorization/policy without requiring buyer resubmission. Omit when the format has no `published_post` slot.").optional(), "production_window_business_days": z.number().int().gte(0).describe("Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative).").optional() }).catchall(z.any()).describe("Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).").describe("Catalog-driven retail-media format. Slot: `source_catalog` (catalog asset — product/SKU/ASIN/GTIN catalog reference, REQUIRED), optional `hero_asset`, optional `landing_page_url`. Buyer supplies the catalog reference; surface composes per-item or multi-item rendering using its native placement template. **Composition is deterministic** — buyer can predict per-slot rendering from the catalog item structure. Tracking model: per-item impression + click + conversion (catalog-keyed via offering_id/sku/gtin macros). Covers Amazon Sponsored Products, Criteo Sponsored Products, CitrusAd Sponsored Products, Walmart Connect Sponsored Products, Pinterest Collection (catalog-driven mode).\n\n**Scope (normative — buyer-agent routing).** This canonical is the home for catalog-driven retail-media placements ONLY. The defining feature is the `source_catalog` slot — products under this canonical compose their creative *per catalog item* using the buyer-supplied catalog feed. Without a catalog feed there is nothing to render against. Buyer agents reading `format_kind: sponsored_placement` MUST attach a catalog reference; sellers MUST require `source_catalog` in the manifest.\n\n**Not this canonical (route elsewhere):**\n- IAB in-feed native ads, content-recommendation widgets (Taboola, Outbrain, Yahoo Native, AdMob Native, in-feed sponsored cards) — use `native_in_feed` (asset-bundle composition; no catalog).\n- Algorithmic surface that picks from a buyer-supplied asset pool (Google PMax, Meta Advantage+) — use `responsive_creative`.\n- Single-image or single-video creative — use `image` or `video_hosted`.\n\nThe earlier broader framing ('any sponsored placement') was too loose for buyer-agent routing — a buyer reading `sponsored_placement` couldn't disambiguate a catalog-driven Amazon SP from an in-feed Taboola widget. As of 3.1, the canonical is narrowed to catalog-keyed retail-media; native moves to `native_in_feed`. Distinct from `responsive_creative` (algorithmic combinator from buyer pool) and `agent_placement` (text/audio AI-surface composition).") }), z.object({ "format_kind": z.literal("native_in_feed"), "params": z.object({ "experimental": z.boolean().describe("When true, this canonical or seller narrowing may not work as declared. Adopters SHOULD preflight it with validate_input or in a sandbox and SHOULD NOT route production budget without testing; experimental status never makes the deprecated v1 path preferable. Drivers include unsettled spec shape, an adopter runtime gap, and custom shapes awaiting promotion. This replaces the earlier status plus runtime_status axes. Sellers SHOULD set experimental whenever a canonical or declaration is not production-ready.").default(false), "deprecated": z.boolean().describe("When true, this canonical (or a seller's specific narrowing of it) is going away. Existing adopters are supported through the deprecation cycle; new adoption is discouraged. Pair with `migration_target_version` to indicate when the canonical is expected to be removed. Distinct from `experimental`: an experimental canonical may stabilize and stop being experimental; a deprecated canonical is on a sunset path.").default(false), "v1_translatable": z.boolean().describe("Whether this canonical has any v1 named-format equivalent. `true` (default) — the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1→v2 projection via `v1-canonical-mapping.json` is meaningful. `false` — the canonical is inherently new in v2 and has no v1 form; v1's `list_creative_formats` couldn't express it because the underlying concept (algorithmic surface composition, AI-surface mentions, retail-media catalog placements, multi-card carousels) didn't exist as a v1 named-format archetype.\n\nLets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The 4 inherently-v2 canonicals at 3.1 GA: `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`.").default(true), "since_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version that introduced this canonical (e.g., '3.1', '3.2'). Lets adopters reason about minimum protocol version requirements when consuming a format declaration. Patch precision is intentionally rejected — canonicals are introduced at minor-version boundaries.").optional(), "migration_target_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version by which the working group expects this canonical to stabilize, surface a breaking revision, or (when `deprecated: true`) be removed. Patch precision is intentionally rejected — canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown').").optional(), "composition_model": z.enum(["deterministic","algorithmic"]).describe("Whether the surface composes deterministically (buyer can predict per-slot rendering — sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing — responsive_creative, agent_placement).").optional(), "provenance_required": z.boolean().describe("When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent.").optional(), "platform_extensions": z.array(z.object({ "uri": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL identifying the extension. `https://` is mandatory — `http://`, `file://`, `data:`, and other schemes are rejected at the schema layer (defense-in-depth on top of the fetch-contract normative rules). The URI base is the owning agent's URL; the path identifies the extension within that agent. Example: 'https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel'. The full fetch contract — SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds — is documented on `product-format-declaration.json#format_schema` and applies to ALL fetches of this reference shape regardless of whether the field is named `format_schema` (load-bearing for validation) or `platform_extensions` (informational); the *transport* rules are identical, only the *consumption* semantics differ."), "digest": z.string().regex(new RegExp("^sha256:[a-f0-9]{64}$")).describe("SHA-256 content digest of the extension definition (sha256:). Used to detect drift — if the agent revises the extension, the digest changes and cached definitions become invalid.") }).catchall(z.any()).describe("Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` — divergent digests in one response indicate producer-side error (e.g., concurrent extension revision mid-render). Buyers encountering divergent digests for the same URI MUST fail closed: treat all references to that URI as unresolved and surface a validation error rather than picking one branch silently. **Across responses**, digest divergence is normal — extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.")).describe("Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\n\n**Collision precedence (normative).** When two or more `platform_extensions[]` entries on the same declaration extend the same target (e.g., both extend `tracking`) with overlapping field names, **array order is authoritative — later entries override earlier ones on a per-field basis** (last-in-array-wins). SDKs MUST surface the overlap via the `errors[]` array on the `get_products` response with a structured code (`FORMAT_DECLARATION_DIVERGENT` is appropriate when the overlap appears across dual-emitted shapes; a producer-self-emitted overlap on a single declaration SHOULD use the same code with `error.details: { collision_kind: \"platform_extension_field\", target, overlapping_fields, winning_extension_uri }`). Producers SHOULD avoid the collision by emitting one extension per target or by partitioning fields across extensions; the deterministic precedence is for last-resort consistency across SDK implementations, not a sanctioned merging strategy.").optional(), "synthesis_nondeterministic": z.boolean().describe("When true, the format's production pipeline is genuinely nondeterministic — the platform cannot guarantee that synthesis from a given input set produces in-spec output. Veo / Sora / Runway-class generative video, and other AI-synthesis flows where output dimensions, duration, or quality vary per run. Implies a different validation contract: predictive `validate_input` is impossible; the platform's own post-synthesis QA loop applies; if the QA loop exhausts without producing a valid artifact, `build_creative` returns task_failed with a synthesis_failed reason. Distinct from `composition_model` (which describes how the surface composes per-slot rendering, not whether synthesis is deterministic). When false or absent, the format's production is predictable enough that `validate_input` can predict output properties from input properties.\n\n**Compatibility with `asset_source` / `item_production_model`**: `synthesis_nondeterministic: true` MAY pair with any of `seller_pre_rendered_from_brief`, `seller_human_designed`, or `agent_synthesized` (the QA loop is concept-level, not source-specific — 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.").default(false), "slots": z.array(z.object({ "asset_group_id": z.string().describe("Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings."), "asset_type": z.enum(["image","video","audio","text","markdown","url","html","css","javascript","vast","daast","webhook","brief","catalog","published_post","zip","card","object","pixel_tracker","vast_tracker","daast_tracker"]).describe("Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `published_post` is an existing-post reference asset, not uploaded media bytes and not a catalog row. `card` is the multi-card carousel element type (see card-asset.json). `pixel_tracker` / `vast_tracker` / `daast_tracker` are the renderer-fired measurement-tracker primitives — see `/schemas/core/assets/pixel-tracker-asset.json` and the VAST / DAAST tracker schemas. `object` is a last-resort fallback for structured non-asset inputs that don't fit any primitive asset_type — prefer specific types whenever possible."), "required": z.boolean().describe("Whether this slot is required for a valid manifest.").default(false), "min": z.number().int().gte(0).describe("Minimum count for repeatable / pool slots.").optional(), "max": z.number().int().gte(1).describe("Maximum count for repeatable / pool slots.").optional(), "max_chars": z.number().int().gte(1).describe("Per-slot character limit. Valid only when `asset_type` is `text`, `markdown`, or `brief`. Mutually exclusive with `max_size_kb` (which applies to binary asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "max_size_kb": z.number().int().gte(1).describe("Per-slot file size limit in kilobytes. Valid only when `asset_type` is `image`, `video`, `audio`, or `zip`. Mutually exclusive with `max_chars` (which applies to text asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Accepted intrinsic-pixel densities for this image-bearing slot. Valid when `asset_type` is `image`, and on a `card` slot where it constrains each card's image media (video media is unaffected). This makes density available to every canonical carrying image assets (native, carousel, responsive, companion images, and image itself), not only `format_kind: image`. When the image canonical also declares top-level `params.pixel_ratios`, the effective set is the intersection; an empty intersection is invalid. One matching asset satisfies the slot unless `required_pixel_ratios` requires rendition coverage.").optional(), "required_pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Required density coverage for an image rendition set. Valid only when `asset_type` is `image` and `pixel_ratios` is also declared. Every value MUST appear in the effective accepted set after intersecting any top-level image `params.pixel_ratios`, and the manifest slot value MUST be an array containing exactly one matching image rendition for each required ratio. Other accepted ratios remain optional. For example, `pixel_ratios: [1, 1.5, 2]` with `required_pixel_ratios: [1, 2]` requires the 1x and 2x renditions while making 1.5x optional. SDKs enforce intersection, subset, coverage, and duplicate-ratio rules because JSON Schema draft-07 cannot express them generically.").optional(), "logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("When `asset_group_id` is `logo`, renderer-facing brand.json logo slots acceptable for this format slot. Producers selecting from brand.json SHOULD prefer `logos[]` entries whose `slots[]` intersects this list, then apply `visual_guidelines.logo_usage_rules[]`.").optional(), "required_logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Subset of `logo_slots` for which this format expects explicit logo coverage. A manifest or brand-derived logo pool SHOULD include at least one usable logo for each required slot; if coverage is missing, builders SHOULD surface a validation warning or approval mapping instead of guessing from prose.").optional(), "description": z.string().describe("Human-readable description of what the slot expects from the buyer.").optional(), "consumed_for_production": z.boolean().describe("Dispatch hint for `build_creative` and v1↔v2 wire translators: when `true`, the slot's value is consumed as INPUT to a production step (host-read script, brief copy fed to generative synthesis, catalog feed driving per-SKU rendering) and is not rendered verbatim. When `false` (default), the slot's value is rendered verbatim on the placement (image bytes, video file, display tag).\n\nMotivates the v1↔v2 dispatch table: pre-v2 buyers shipped production-consumed inputs separately in a `inputs` map on the build_creative request; v2 collapses inputs and rendered assets into a single `assets` map keyed by `asset_group_id`. SDK translators between v1 and v2 use this flag per canonical to know which assets in the v2 manifest map back to v1 `inputs` vs v1 `assets`. Without the per-slot flag the dispatch table lives in adopter code and every SDK gets it slightly different.\n\nProducers SHOULD set this explicitly on slots whose consumption pattern isn't obvious (host-read scripts on `audio_hosted`, briefs on generative `video_hosted`, catalog feeds on `sponsored_placement`). For canonicals where every slot is render-verbatim (`image`, `display_tag`, `video_vast`), the default `false` is sufficient and the flag MAY be omitted.").default(false) }).catchall(z.any()).and(z.intersection(z.intersection(z.any(), z.intersection(z.any(), z.any())), z.intersection(z.any(), z.intersection(z.any(), z.any()))))).describe("Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.").optional(), "required_connections": z.array(z.object({ "provider": z.string().describe("Stable provider or platform namespace, preferably lowercase. Examples: `social.example`, `shortvideo.example`, or a seller-defined namespace. Omit only when the requirement is provider-agnostic, or when an `authorization_url` fully routes the human to the correct provider-specific connection flow.").optional(), "connection_type": z.enum(["advertiser_account","publisher_identity","post_authorization"]).describe("Kind of downstream connection required. `advertiser_account` is the platform account used to buy/manage ads. `publisher_identity` is the creator, page, channel, organization, or profile that owns source posts. `post_authorization` is a post-scoped grant when the platform authorizes individual posts instead of, or in addition to, the owning identity."), "required_for": z.array(z.string().min(1)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Concrete AdCP protocol operation names that require this downstream connection. Sellers SHOULD include this in product declarations when the requirement is known ahead of time, and in AUTHORIZATION_REQUIRED details when it explains the failed operation. Prefer specific operation names such as `list_creatives`, `sync_creatives`, `create_media_buy`, `get_media_buy_delivery`, or `get_creative_delivery` over broad category labels such as `reporting`.").optional(), "scope": z.enum(["account","identity","post","unknown"]).describe("Granularity of the downstream grant.").optional(), "status": z.enum(["connected","missing","pending","expired","revoked","not_required","unknown"]).describe("Current seller-observed state for this downstream connection when known. Product declarations MAY omit status or use `unknown`; AUTHORIZATION_REQUIRED details SHOULD use `missing`, `expired`, or `revoked` for the connection that blocked the call.").optional(), "connection_id": z.string().describe("Seller-defined identifier for an already-created downstream connection. Omit when no connection exists yet or when exposing it would leak platform/account state.").optional(), "resource_ref": z.object({ "platform_account_id": z.string().describe("Provider-native advertiser or business account id, when safe to disclose.").optional(), "identity_id": z.string().describe("Provider-native creator, page, channel, organization, or profile id, when safe to disclose.").optional(), "handle": z.string().describe("Provider-native public handle for the owning identity, when available.").optional(), "profile_url": z.string().url().describe("Public URL for the owning identity, when available.").optional(), "post_id": z.string().describe("Provider-native post id, when the grant is post-scoped or the failed request referenced a specific post.").optional(), "post_url": z.string().url().describe("Public URL for the referenced post, when available.").optional() }).catchall(z.any()).describe("Optional opaque provider-native resource hint, such as a platform account id, profile URL, handle, channel id, post id, or post URL. This is a hint for routing authorization, not proof that authorization exists.").optional(), "authorization_url": z.string().url().describe("Seller-hosted or provider-hosted URL where a human can complete or restore this downstream connection.").optional(), "authorization_instructions": z.string().describe("Human-readable instructions for completing or restoring this downstream connection.").optional(), "expires_at": z.string().datetime().describe("Expiration time for the downstream grant, when known.").optional() }).catchall(z.any()).and(z.any()).describe("A seller/platform-side connection or grant required by a product, format, or request. This is not the AdCP caller credential: the AdCP request is still authenticated once, and the seller uses these stored downstream connections to call a platform or service on the buyer's behalf. Use this shape for platforms that require more than one downstream grant, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.")).describe("Downstream platform connections or grants required to use this format declaration. These are in addition to the single AdCP caller credential. Use this when a platform product requires multiple downstream grants, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.").optional(), "reference_mutability": z.enum(["immutable_snapshot","mutable_requires_reapproval","mutable_auto_recheck"]).describe("Policy for formats whose `slots` accept a `published_post` reference. `immutable_snapshot`: seller snapshots the referenced post at approval and later source changes do not change the served creative. `mutable_requires_reapproval`: the source post may change and material changes require review before continued serving. `mutable_auto_recheck`: the source post may change and the seller continuously or periodically rechecks authorization/policy without requiring buyer resubmission. Omit when the format has no `published_post` slot.").optional(), "production_window_business_days": z.number().int().gte(0).describe("Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative).").optional() }).catchall(z.any()).describe("Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).").describe("IAB-shaped native creative for in-feed and content-recommendation surfaces. Default slots cover the primary IAB OpenRTB Native 1.2 asset types — `title` (Title Asset), `body_text` (Data Asset type 2), `main_image` (Image Asset main), `icon` (Image Asset icon), `cta` (Data Asset type 12), `advertiser_name` (Data Asset type 1), `sponsored_label` (Title-adjacent), `landing_page_url` (Link Asset), `display_url` (Data Asset type 11 — visible URL/domain, distinct from clickthrough), `rating` (Data Asset type 3 — app/product rating), `price` (Data Asset type 6 — product price), plus renderer-fired `impression_tracker` / `viewability_tracker` / `click_tracker` (`pixel_tracker`). Products MAY use `slots_override` to add other IAB Native data asset types (likes — type 4, downloads — type 5, saleprice — type 7, phone_number — type 8, address — type 9, desc2 — type 10, etc.) or to remove slots the surface doesn't render. The publisher's renderer assembles these into its own look-and-feel — feed card, content-recommendation slot, in-stream native unit. Buyer ships a single asset bundle; the surface chooses presentation.\n\n**Scope (normative — buyer-agent routing).** This canonical is the home for:\n- IAB OpenRTB Native 1.2 in-feed native ads (publisher feeds, app feeds)\n- Content-recommendation widgets (Taboola, Outbrain, Yahoo Recommendations)\n- AdMob Native / Yahoo Native publisher slots\n- In-feed sponsored placements without catalog dependency\n\n**Not this canonical:**\n- Catalog-driven retail-media (Amazon SP, Criteo SP, CitrusAd SP) — use `sponsored_placement` (requires `source_catalog`).\n- Algorithmic surface that picks from a buyer-supplied asset pool (Google PMax, Meta Advantage+) — use `responsive_creative`.\n- Multi-card carousel — use `image_carousel`.\n- Video-first native units where the asset is a hosted video file — use `video_hosted` with `applies_to_channels: [\"native\"]`.\n\nDistinct from `sponsored_placement` along the catalog axis: native_in_feed is asset-bundle composition; sponsored_placement is catalog-row composition. A buyer agent reading `format_kind: native_in_feed` knows to assemble title + image + body + CTA; reading `format_kind: sponsored_placement` knows to attach a catalog feed.") }), z.object({ "format_kind": z.literal("responsive_creative"), "params": z.object({ "experimental": z.boolean().describe("When true, this canonical or seller narrowing may not work as declared. Adopters SHOULD preflight it with validate_input or in a sandbox and SHOULD NOT route production budget without testing; experimental status never makes the deprecated v1 path preferable. Drivers include unsettled spec shape, an adopter runtime gap, and custom shapes awaiting promotion. This replaces the earlier status plus runtime_status axes. Sellers SHOULD set experimental whenever a canonical or declaration is not production-ready.").default(false), "deprecated": z.boolean().describe("When true, this canonical (or a seller's specific narrowing of it) is going away. Existing adopters are supported through the deprecation cycle; new adoption is discouraged. Pair with `migration_target_version` to indicate when the canonical is expected to be removed. Distinct from `experimental`: an experimental canonical may stabilize and stop being experimental; a deprecated canonical is on a sunset path.").default(false), "v1_translatable": z.boolean().describe("Whether this canonical has any v1 named-format equivalent. `true` (default) — the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1→v2 projection via `v1-canonical-mapping.json` is meaningful. `false` — the canonical is inherently new in v2 and has no v1 form; v1's `list_creative_formats` couldn't express it because the underlying concept (algorithmic surface composition, AI-surface mentions, retail-media catalog placements, multi-card carousels) didn't exist as a v1 named-format archetype.\n\nLets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The 4 inherently-v2 canonicals at 3.1 GA: `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`.").default(true), "since_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version that introduced this canonical (e.g., '3.1', '3.2'). Lets adopters reason about minimum protocol version requirements when consuming a format declaration. Patch precision is intentionally rejected — canonicals are introduced at minor-version boundaries.").optional(), "migration_target_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version by which the working group expects this canonical to stabilize, surface a breaking revision, or (when `deprecated: true`) be removed. Patch precision is intentionally rejected — canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown').").optional(), "composition_model": z.enum(["deterministic","algorithmic"]).describe("Whether the surface composes deterministically (buyer can predict per-slot rendering — sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing — responsive_creative, agent_placement).").optional(), "provenance_required": z.boolean().describe("When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent.").optional(), "platform_extensions": z.array(z.object({ "uri": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL identifying the extension. `https://` is mandatory — `http://`, `file://`, `data:`, and other schemes are rejected at the schema layer (defense-in-depth on top of the fetch-contract normative rules). The URI base is the owning agent's URL; the path identifies the extension within that agent. Example: 'https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel'. The full fetch contract — SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds — is documented on `product-format-declaration.json#format_schema` and applies to ALL fetches of this reference shape regardless of whether the field is named `format_schema` (load-bearing for validation) or `platform_extensions` (informational); the *transport* rules are identical, only the *consumption* semantics differ."), "digest": z.string().regex(new RegExp("^sha256:[a-f0-9]{64}$")).describe("SHA-256 content digest of the extension definition (sha256:). Used to detect drift — if the agent revises the extension, the digest changes and cached definitions become invalid.") }).catchall(z.any()).describe("Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` — divergent digests in one response indicate producer-side error (e.g., concurrent extension revision mid-render). Buyers encountering divergent digests for the same URI MUST fail closed: treat all references to that URI as unresolved and surface a validation error rather than picking one branch silently. **Across responses**, digest divergence is normal — extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.")).describe("Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\n\n**Collision precedence (normative).** When two or more `platform_extensions[]` entries on the same declaration extend the same target (e.g., both extend `tracking`) with overlapping field names, **array order is authoritative — later entries override earlier ones on a per-field basis** (last-in-array-wins). SDKs MUST surface the overlap via the `errors[]` array on the `get_products` response with a structured code (`FORMAT_DECLARATION_DIVERGENT` is appropriate when the overlap appears across dual-emitted shapes; a producer-self-emitted overlap on a single declaration SHOULD use the same code with `error.details: { collision_kind: \"platform_extension_field\", target, overlapping_fields, winning_extension_uri }`). Producers SHOULD avoid the collision by emitting one extension per target or by partitioning fields across extensions; the deterministic precedence is for last-resort consistency across SDK implementations, not a sanctioned merging strategy.").optional(), "synthesis_nondeterministic": z.boolean().describe("When true, the format's production pipeline is genuinely nondeterministic — the platform cannot guarantee that synthesis from a given input set produces in-spec output. Veo / Sora / Runway-class generative video, and other AI-synthesis flows where output dimensions, duration, or quality vary per run. Implies a different validation contract: predictive `validate_input` is impossible; the platform's own post-synthesis QA loop applies; if the QA loop exhausts without producing a valid artifact, `build_creative` returns task_failed with a synthesis_failed reason. Distinct from `composition_model` (which describes how the surface composes per-slot rendering, not whether synthesis is deterministic). When false or absent, the format's production is predictable enough that `validate_input` can predict output properties from input properties.\n\n**Compatibility with `asset_source` / `item_production_model`**: `synthesis_nondeterministic: true` MAY pair with any of `seller_pre_rendered_from_brief`, `seller_human_designed`, or `agent_synthesized` (the QA loop is concept-level, not source-specific — 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.").default(false), "slots": z.array(z.object({ "asset_group_id": z.string().describe("Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings."), "asset_type": z.enum(["image","video","audio","text","markdown","url","html","css","javascript","vast","daast","webhook","brief","catalog","published_post","zip","card","object","pixel_tracker","vast_tracker","daast_tracker"]).describe("Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `published_post` is an existing-post reference asset, not uploaded media bytes and not a catalog row. `card` is the multi-card carousel element type (see card-asset.json). `pixel_tracker` / `vast_tracker` / `daast_tracker` are the renderer-fired measurement-tracker primitives — see `/schemas/core/assets/pixel-tracker-asset.json` and the VAST / DAAST tracker schemas. `object` is a last-resort fallback for structured non-asset inputs that don't fit any primitive asset_type — prefer specific types whenever possible."), "required": z.boolean().describe("Whether this slot is required for a valid manifest.").default(false), "min": z.number().int().gte(0).describe("Minimum count for repeatable / pool slots.").optional(), "max": z.number().int().gte(1).describe("Maximum count for repeatable / pool slots.").optional(), "max_chars": z.number().int().gte(1).describe("Per-slot character limit. Valid only when `asset_type` is `text`, `markdown`, or `brief`. Mutually exclusive with `max_size_kb` (which applies to binary asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "max_size_kb": z.number().int().gte(1).describe("Per-slot file size limit in kilobytes. Valid only when `asset_type` is `image`, `video`, `audio`, or `zip`. Mutually exclusive with `max_chars` (which applies to text asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Accepted intrinsic-pixel densities for this image-bearing slot. Valid when `asset_type` is `image`, and on a `card` slot where it constrains each card's image media (video media is unaffected). This makes density available to every canonical carrying image assets (native, carousel, responsive, companion images, and image itself), not only `format_kind: image`. When the image canonical also declares top-level `params.pixel_ratios`, the effective set is the intersection; an empty intersection is invalid. One matching asset satisfies the slot unless `required_pixel_ratios` requires rendition coverage.").optional(), "required_pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Required density coverage for an image rendition set. Valid only when `asset_type` is `image` and `pixel_ratios` is also declared. Every value MUST appear in the effective accepted set after intersecting any top-level image `params.pixel_ratios`, and the manifest slot value MUST be an array containing exactly one matching image rendition for each required ratio. Other accepted ratios remain optional. For example, `pixel_ratios: [1, 1.5, 2]` with `required_pixel_ratios: [1, 2]` requires the 1x and 2x renditions while making 1.5x optional. SDKs enforce intersection, subset, coverage, and duplicate-ratio rules because JSON Schema draft-07 cannot express them generically.").optional(), "logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("When `asset_group_id` is `logo`, renderer-facing brand.json logo slots acceptable for this format slot. Producers selecting from brand.json SHOULD prefer `logos[]` entries whose `slots[]` intersects this list, then apply `visual_guidelines.logo_usage_rules[]`.").optional(), "required_logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Subset of `logo_slots` for which this format expects explicit logo coverage. A manifest or brand-derived logo pool SHOULD include at least one usable logo for each required slot; if coverage is missing, builders SHOULD surface a validation warning or approval mapping instead of guessing from prose.").optional(), "description": z.string().describe("Human-readable description of what the slot expects from the buyer.").optional(), "consumed_for_production": z.boolean().describe("Dispatch hint for `build_creative` and v1↔v2 wire translators: when `true`, the slot's value is consumed as INPUT to a production step (host-read script, brief copy fed to generative synthesis, catalog feed driving per-SKU rendering) and is not rendered verbatim. When `false` (default), the slot's value is rendered verbatim on the placement (image bytes, video file, display tag).\n\nMotivates the v1↔v2 dispatch table: pre-v2 buyers shipped production-consumed inputs separately in a `inputs` map on the build_creative request; v2 collapses inputs and rendered assets into a single `assets` map keyed by `asset_group_id`. SDK translators between v1 and v2 use this flag per canonical to know which assets in the v2 manifest map back to v1 `inputs` vs v1 `assets`. Without the per-slot flag the dispatch table lives in adopter code and every SDK gets it slightly different.\n\nProducers SHOULD set this explicitly on slots whose consumption pattern isn't obvious (host-read scripts on `audio_hosted`, briefs on generative `video_hosted`, catalog feeds on `sponsored_placement`). For canonicals where every slot is render-verbatim (`image`, `display_tag`, `video_vast`), the default `false` is sufficient and the flag MAY be omitted.").default(false) }).catchall(z.any()).and(z.intersection(z.intersection(z.any(), z.intersection(z.any(), z.any())), z.intersection(z.any(), z.intersection(z.any(), z.any()))))).describe("Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.").optional(), "required_connections": z.array(z.object({ "provider": z.string().describe("Stable provider or platform namespace, preferably lowercase. Examples: `social.example`, `shortvideo.example`, or a seller-defined namespace. Omit only when the requirement is provider-agnostic, or when an `authorization_url` fully routes the human to the correct provider-specific connection flow.").optional(), "connection_type": z.enum(["advertiser_account","publisher_identity","post_authorization"]).describe("Kind of downstream connection required. `advertiser_account` is the platform account used to buy/manage ads. `publisher_identity` is the creator, page, channel, organization, or profile that owns source posts. `post_authorization` is a post-scoped grant when the platform authorizes individual posts instead of, or in addition to, the owning identity."), "required_for": z.array(z.string().min(1)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Concrete AdCP protocol operation names that require this downstream connection. Sellers SHOULD include this in product declarations when the requirement is known ahead of time, and in AUTHORIZATION_REQUIRED details when it explains the failed operation. Prefer specific operation names such as `list_creatives`, `sync_creatives`, `create_media_buy`, `get_media_buy_delivery`, or `get_creative_delivery` over broad category labels such as `reporting`.").optional(), "scope": z.enum(["account","identity","post","unknown"]).describe("Granularity of the downstream grant.").optional(), "status": z.enum(["connected","missing","pending","expired","revoked","not_required","unknown"]).describe("Current seller-observed state for this downstream connection when known. Product declarations MAY omit status or use `unknown`; AUTHORIZATION_REQUIRED details SHOULD use `missing`, `expired`, or `revoked` for the connection that blocked the call.").optional(), "connection_id": z.string().describe("Seller-defined identifier for an already-created downstream connection. Omit when no connection exists yet or when exposing it would leak platform/account state.").optional(), "resource_ref": z.object({ "platform_account_id": z.string().describe("Provider-native advertiser or business account id, when safe to disclose.").optional(), "identity_id": z.string().describe("Provider-native creator, page, channel, organization, or profile id, when safe to disclose.").optional(), "handle": z.string().describe("Provider-native public handle for the owning identity, when available.").optional(), "profile_url": z.string().url().describe("Public URL for the owning identity, when available.").optional(), "post_id": z.string().describe("Provider-native post id, when the grant is post-scoped or the failed request referenced a specific post.").optional(), "post_url": z.string().url().describe("Public URL for the referenced post, when available.").optional() }).catchall(z.any()).describe("Optional opaque provider-native resource hint, such as a platform account id, profile URL, handle, channel id, post id, or post URL. This is a hint for routing authorization, not proof that authorization exists.").optional(), "authorization_url": z.string().url().describe("Seller-hosted or provider-hosted URL where a human can complete or restore this downstream connection.").optional(), "authorization_instructions": z.string().describe("Human-readable instructions for completing or restoring this downstream connection.").optional(), "expires_at": z.string().datetime().describe("Expiration time for the downstream grant, when known.").optional() }).catchall(z.any()).and(z.any()).describe("A seller/platform-side connection or grant required by a product, format, or request. This is not the AdCP caller credential: the AdCP request is still authenticated once, and the seller uses these stored downstream connections to call a platform or service on the buyer's behalf. Use this shape for platforms that require more than one downstream grant, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.")).describe("Downstream platform connections or grants required to use this format declaration. These are in addition to the single AdCP caller credential. Use this when a platform product requires multiple downstream grants, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.").optional(), "reference_mutability": z.enum(["immutable_snapshot","mutable_requires_reapproval","mutable_auto_recheck"]).describe("Policy for formats whose `slots` accept a `published_post` reference. `immutable_snapshot`: seller snapshots the referenced post at approval and later source changes do not change the served creative. `mutable_requires_reapproval`: the source post may change and material changes require review before continued serving. `mutable_auto_recheck`: the source post may change and the seller continuously or periodically rechecks authorization/policy without requiring buyer resubmission. Omit when the format has no `published_post` slot.").optional(), "production_window_business_days": z.number().int().gte(0).describe("Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative).").optional() }).catchall(z.any()).describe("Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).").describe("Buyer supplies a pool of typed assets (multiple headlines, descriptions, images, videos, logos); the surface algorithmically composes combinations per placement. **Composition is algorithmic** — surface picks combinations and reports per-asset performance breakdowns. Covers Google Responsive Display Ads (RDA), Responsive Search Ads (RSA), Performance Max (PMax), Demand Gen, and Meta Advantage+ creative. Industry term: \"Responsive\" (Google) / \"Advantage+ creative\" (Meta) / \"Dynamic Creative\" (older Meta term). Distinct from `sponsored_placement` (catalog-driven, deterministic) and `agent_placement` (AI-surface composition). The structured `slots` field below enumerates expected canonical asset_group_id slots; per-slot count/length narrowing lives in flat parameters (`headlines_min`, `headline_max_chars`, etc.).") }), z.object({ "format_kind": z.literal("agent_placement"), "params": z.object({ "experimental": z.boolean().describe("When true, this canonical or seller narrowing may not work as declared. Adopters SHOULD preflight it with validate_input or in a sandbox and SHOULD NOT route production budget without testing; experimental status never makes the deprecated v1 path preferable. Drivers include unsettled spec shape, an adopter runtime gap, and custom shapes awaiting promotion. This replaces the earlier status plus runtime_status axes. Sellers SHOULD set experimental whenever a canonical or declaration is not production-ready.").default(false), "deprecated": z.boolean().describe("When true, this canonical (or a seller's specific narrowing of it) is going away. Existing adopters are supported through the deprecation cycle; new adoption is discouraged. Pair with `migration_target_version` to indicate when the canonical is expected to be removed. Distinct from `experimental`: an experimental canonical may stabilize and stop being experimental; a deprecated canonical is on a sunset path.").default(false), "v1_translatable": z.boolean().describe("Whether this canonical has any v1 named-format equivalent. `true` (default) — the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1→v2 projection via `v1-canonical-mapping.json` is meaningful. `false` — the canonical is inherently new in v2 and has no v1 form; v1's `list_creative_formats` couldn't express it because the underlying concept (algorithmic surface composition, AI-surface mentions, retail-media catalog placements, multi-card carousels) didn't exist as a v1 named-format archetype.\n\nLets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The 4 inherently-v2 canonicals at 3.1 GA: `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`.").default(true), "since_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version that introduced this canonical (e.g., '3.1', '3.2'). Lets adopters reason about minimum protocol version requirements when consuming a format declaration. Patch precision is intentionally rejected — canonicals are introduced at minor-version boundaries.").optional(), "migration_target_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version by which the working group expects this canonical to stabilize, surface a breaking revision, or (when `deprecated: true`) be removed. Patch precision is intentionally rejected — canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown').").optional(), "composition_model": z.enum(["deterministic","algorithmic"]).describe("Whether the surface composes deterministically (buyer can predict per-slot rendering — sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing — responsive_creative, agent_placement).").optional(), "provenance_required": z.boolean().describe("When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent.").optional(), "platform_extensions": z.array(z.object({ "uri": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL identifying the extension. `https://` is mandatory — `http://`, `file://`, `data:`, and other schemes are rejected at the schema layer (defense-in-depth on top of the fetch-contract normative rules). The URI base is the owning agent's URL; the path identifies the extension within that agent. Example: 'https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel'. The full fetch contract — SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds — is documented on `product-format-declaration.json#format_schema` and applies to ALL fetches of this reference shape regardless of whether the field is named `format_schema` (load-bearing for validation) or `platform_extensions` (informational); the *transport* rules are identical, only the *consumption* semantics differ."), "digest": z.string().regex(new RegExp("^sha256:[a-f0-9]{64}$")).describe("SHA-256 content digest of the extension definition (sha256:). Used to detect drift — if the agent revises the extension, the digest changes and cached definitions become invalid.") }).catchall(z.any()).describe("Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` — divergent digests in one response indicate producer-side error (e.g., concurrent extension revision mid-render). Buyers encountering divergent digests for the same URI MUST fail closed: treat all references to that URI as unresolved and surface a validation error rather than picking one branch silently. **Across responses**, digest divergence is normal — extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.")).describe("Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\n\n**Collision precedence (normative).** When two or more `platform_extensions[]` entries on the same declaration extend the same target (e.g., both extend `tracking`) with overlapping field names, **array order is authoritative — later entries override earlier ones on a per-field basis** (last-in-array-wins). SDKs MUST surface the overlap via the `errors[]` array on the `get_products` response with a structured code (`FORMAT_DECLARATION_DIVERGENT` is appropriate when the overlap appears across dual-emitted shapes; a producer-self-emitted overlap on a single declaration SHOULD use the same code with `error.details: { collision_kind: \"platform_extension_field\", target, overlapping_fields, winning_extension_uri }`). Producers SHOULD avoid the collision by emitting one extension per target or by partitioning fields across extensions; the deterministic precedence is for last-resort consistency across SDK implementations, not a sanctioned merging strategy.").optional(), "synthesis_nondeterministic": z.boolean().describe("When true, the format's production pipeline is genuinely nondeterministic — the platform cannot guarantee that synthesis from a given input set produces in-spec output. Veo / Sora / Runway-class generative video, and other AI-synthesis flows where output dimensions, duration, or quality vary per run. Implies a different validation contract: predictive `validate_input` is impossible; the platform's own post-synthesis QA loop applies; if the QA loop exhausts without producing a valid artifact, `build_creative` returns task_failed with a synthesis_failed reason. Distinct from `composition_model` (which describes how the surface composes per-slot rendering, not whether synthesis is deterministic). When false or absent, the format's production is predictable enough that `validate_input` can predict output properties from input properties.\n\n**Compatibility with `asset_source` / `item_production_model`**: `synthesis_nondeterministic: true` MAY pair with any of `seller_pre_rendered_from_brief`, `seller_human_designed`, or `agent_synthesized` (the QA loop is concept-level, not source-specific — 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.").default(false), "slots": z.array(z.object({ "asset_group_id": z.string().describe("Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings."), "asset_type": z.enum(["image","video","audio","text","markdown","url","html","css","javascript","vast","daast","webhook","brief","catalog","published_post","zip","card","object","pixel_tracker","vast_tracker","daast_tracker"]).describe("Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `published_post` is an existing-post reference asset, not uploaded media bytes and not a catalog row. `card` is the multi-card carousel element type (see card-asset.json). `pixel_tracker` / `vast_tracker` / `daast_tracker` are the renderer-fired measurement-tracker primitives — see `/schemas/core/assets/pixel-tracker-asset.json` and the VAST / DAAST tracker schemas. `object` is a last-resort fallback for structured non-asset inputs that don't fit any primitive asset_type — prefer specific types whenever possible."), "required": z.boolean().describe("Whether this slot is required for a valid manifest.").default(false), "min": z.number().int().gte(0).describe("Minimum count for repeatable / pool slots.").optional(), "max": z.number().int().gte(1).describe("Maximum count for repeatable / pool slots.").optional(), "max_chars": z.number().int().gte(1).describe("Per-slot character limit. Valid only when `asset_type` is `text`, `markdown`, or `brief`. Mutually exclusive with `max_size_kb` (which applies to binary asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "max_size_kb": z.number().int().gte(1).describe("Per-slot file size limit in kilobytes. Valid only when `asset_type` is `image`, `video`, `audio`, or `zip`. Mutually exclusive with `max_chars` (which applies to text asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Accepted intrinsic-pixel densities for this image-bearing slot. Valid when `asset_type` is `image`, and on a `card` slot where it constrains each card's image media (video media is unaffected). This makes density available to every canonical carrying image assets (native, carousel, responsive, companion images, and image itself), not only `format_kind: image`. When the image canonical also declares top-level `params.pixel_ratios`, the effective set is the intersection; an empty intersection is invalid. One matching asset satisfies the slot unless `required_pixel_ratios` requires rendition coverage.").optional(), "required_pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Required density coverage for an image rendition set. Valid only when `asset_type` is `image` and `pixel_ratios` is also declared. Every value MUST appear in the effective accepted set after intersecting any top-level image `params.pixel_ratios`, and the manifest slot value MUST be an array containing exactly one matching image rendition for each required ratio. Other accepted ratios remain optional. For example, `pixel_ratios: [1, 1.5, 2]` with `required_pixel_ratios: [1, 2]` requires the 1x and 2x renditions while making 1.5x optional. SDKs enforce intersection, subset, coverage, and duplicate-ratio rules because JSON Schema draft-07 cannot express them generically.").optional(), "logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("When `asset_group_id` is `logo`, renderer-facing brand.json logo slots acceptable for this format slot. Producers selecting from brand.json SHOULD prefer `logos[]` entries whose `slots[]` intersects this list, then apply `visual_guidelines.logo_usage_rules[]`.").optional(), "required_logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Subset of `logo_slots` for which this format expects explicit logo coverage. A manifest or brand-derived logo pool SHOULD include at least one usable logo for each required slot; if coverage is missing, builders SHOULD surface a validation warning or approval mapping instead of guessing from prose.").optional(), "description": z.string().describe("Human-readable description of what the slot expects from the buyer.").optional(), "consumed_for_production": z.boolean().describe("Dispatch hint for `build_creative` and v1↔v2 wire translators: when `true`, the slot's value is consumed as INPUT to a production step (host-read script, brief copy fed to generative synthesis, catalog feed driving per-SKU rendering) and is not rendered verbatim. When `false` (default), the slot's value is rendered verbatim on the placement (image bytes, video file, display tag).\n\nMotivates the v1↔v2 dispatch table: pre-v2 buyers shipped production-consumed inputs separately in a `inputs` map on the build_creative request; v2 collapses inputs and rendered assets into a single `assets` map keyed by `asset_group_id`. SDK translators between v1 and v2 use this flag per canonical to know which assets in the v2 manifest map back to v1 `inputs` vs v1 `assets`. Without the per-slot flag the dispatch table lives in adopter code and every SDK gets it slightly different.\n\nProducers SHOULD set this explicitly on slots whose consumption pattern isn't obvious (host-read scripts on `audio_hosted`, briefs on generative `video_hosted`, catalog feeds on `sponsored_placement`). For canonicals where every slot is render-verbatim (`image`, `display_tag`, `video_vast`), the default `false` is sufficient and the flag MAY be omitted.").default(false) }).catchall(z.any()).and(z.intersection(z.intersection(z.any(), z.intersection(z.any(), z.any())), z.intersection(z.any(), z.intersection(z.any(), z.any()))))).describe("Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.").optional(), "required_connections": z.array(z.object({ "provider": z.string().describe("Stable provider or platform namespace, preferably lowercase. Examples: `social.example`, `shortvideo.example`, or a seller-defined namespace. Omit only when the requirement is provider-agnostic, or when an `authorization_url` fully routes the human to the correct provider-specific connection flow.").optional(), "connection_type": z.enum(["advertiser_account","publisher_identity","post_authorization"]).describe("Kind of downstream connection required. `advertiser_account` is the platform account used to buy/manage ads. `publisher_identity` is the creator, page, channel, organization, or profile that owns source posts. `post_authorization` is a post-scoped grant when the platform authorizes individual posts instead of, or in addition to, the owning identity."), "required_for": z.array(z.string().min(1)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Concrete AdCP protocol operation names that require this downstream connection. Sellers SHOULD include this in product declarations when the requirement is known ahead of time, and in AUTHORIZATION_REQUIRED details when it explains the failed operation. Prefer specific operation names such as `list_creatives`, `sync_creatives`, `create_media_buy`, `get_media_buy_delivery`, or `get_creative_delivery` over broad category labels such as `reporting`.").optional(), "scope": z.enum(["account","identity","post","unknown"]).describe("Granularity of the downstream grant.").optional(), "status": z.enum(["connected","missing","pending","expired","revoked","not_required","unknown"]).describe("Current seller-observed state for this downstream connection when known. Product declarations MAY omit status or use `unknown`; AUTHORIZATION_REQUIRED details SHOULD use `missing`, `expired`, or `revoked` for the connection that blocked the call.").optional(), "connection_id": z.string().describe("Seller-defined identifier for an already-created downstream connection. Omit when no connection exists yet or when exposing it would leak platform/account state.").optional(), "resource_ref": z.object({ "platform_account_id": z.string().describe("Provider-native advertiser or business account id, when safe to disclose.").optional(), "identity_id": z.string().describe("Provider-native creator, page, channel, organization, or profile id, when safe to disclose.").optional(), "handle": z.string().describe("Provider-native public handle for the owning identity, when available.").optional(), "profile_url": z.string().url().describe("Public URL for the owning identity, when available.").optional(), "post_id": z.string().describe("Provider-native post id, when the grant is post-scoped or the failed request referenced a specific post.").optional(), "post_url": z.string().url().describe("Public URL for the referenced post, when available.").optional() }).catchall(z.any()).describe("Optional opaque provider-native resource hint, such as a platform account id, profile URL, handle, channel id, post id, or post URL. This is a hint for routing authorization, not proof that authorization exists.").optional(), "authorization_url": z.string().url().describe("Seller-hosted or provider-hosted URL where a human can complete or restore this downstream connection.").optional(), "authorization_instructions": z.string().describe("Human-readable instructions for completing or restoring this downstream connection.").optional(), "expires_at": z.string().datetime().describe("Expiration time for the downstream grant, when known.").optional() }).catchall(z.any()).and(z.any()).describe("A seller/platform-side connection or grant required by a product, format, or request. This is not the AdCP caller credential: the AdCP request is still authenticated once, and the seller uses these stored downstream connections to call a platform or service on the buyer's behalf. Use this shape for platforms that require more than one downstream grant, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.")).describe("Downstream platform connections or grants required to use this format declaration. These are in addition to the single AdCP caller credential. Use this when a platform product requires multiple downstream grants, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.").optional(), "reference_mutability": z.enum(["immutable_snapshot","mutable_requires_reapproval","mutable_auto_recheck"]).describe("Policy for formats whose `slots` accept a `published_post` reference. `immutable_snapshot`: seller snapshots the referenced post at approval and later source changes do not change the served creative. `mutable_requires_reapproval`: the source post may change and material changes require review before continued serving. `mutable_auto_recheck`: the source post may change and the seller continuously or periodically rechecks authorization/policy without requiring buyer resubmission. Omit when the format has no `published_post` slot.").optional(), "production_window_business_days": z.number().int().gte(0).describe("Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative).").optional() }).catchall(z.any()).describe("Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).").describe("**3.2-track canonical.** The structural shape (algorithmic composition + brand-context input + optional offering/landing_page) is captured here so adopters can declare against it in 3.1 catalogs, but the **mention-level tracking contract is intentionally underspecified for 3.1**: no normative macro vocabulary, no postback shape, no cross-surface dedup model. Adopters claiming `agent_placement` in 3.1 ship private tracking integrations and SHOULD leave `experimental: true` on the product declaration that references this canonical; buyer agents MUST treat agent_placement attribution as adapter-defined until the 3.2 tracking-macro spec lands. The canonical promotes to a normatively-buyer-callable surface in 3.2 (or later) once the tracking contract is specified.\n\nSponsored placement integrated into an AI-surface's response to a user. Buyer supplies a `BrandRef` (resolving brand.json for context), an optional `offering_ref` to focus the mention on a specific offering, and an optional `landing_page_url` the surface MAY attach as a citation. The surface (LLM, voice assistant, sponsored-search ranker) composes a natural-language mention, sponsored card, or audio snippet within its response to a user query. **Composition is algorithmic** — the agent chooses phrasing and presentation. Output asset_type varies by surface: `text` for chat UIs and sponsored search snippets; `audio` (synthesized) for voice assistants; `card` for structured AI-surface result cards. Tracking model: mention-level impression + attribution events; per-mention id keys back to brand and offering — but see the 3.2-track note above; the wire shape of these events is not yet specified. Distinct from `si_chat` (which is the user-converses-with-brand's-agent pattern — brand owns the conversational surface) and from `sponsored_placement` (retail-media catalog-driven). Parallels `sponsored_placement` structurally: both are surface-composed placements; agent_placement is for AI/agentic surfaces, sponsored_placement is for retail media.") }), z.object({ "format_kind": z.literal("custom"), "params": z.record(z.string(), z.any()).describe("Custom shape's params. Validated against the schema fetched from `format_schema.uri` at the cached `format_schema.digest`.") }).describe("Adopter-defined shape that doesn't fit the 12 canonicals. Requires `format_shape` (vocabulary-registered global pattern) and `format_schema` (URI+digest reference to a fetchable schema describing the actual params/slots). `params` shape is governed by the fetched schema rather than baked into AdCP — kept as `type: object` here with `additionalProperties: true` because the canonical schema validates dynamically post-fetch.")])).and(z.intersection(z.intersection(z.any(), z.union([z.union([z.any(), z.any()]), z.any().refine((value) => !z.union([z.any(), z.any()]).safeParse(value).success, "Invalid input: Should NOT be valid against schema")]).superRefine((value,ctx) => { +export const AdagentsJsonSchema = z.union([z.object({ "$schema": z.string().describe("JSON Schema identifier for this adagents.json file").optional(), "authoritative_location": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL of the authoritative adagents.json file. When present, this file is a reference and the authoritative location contains the actual agent authorization data. Because one deploy can change authorization across every publisher in the network, validators MUST cap response size, refuse redirects on the fetch, enforce short timeouts, and serve the previously cached file on transient 5xx. Two-tier size cap: pointer files served at `/.well-known/adagents.json` use the general 5 MB SSRF cap; dereferenced authoritative files (this URL's response, after the indirection) use a recommended 20 MB cap because the origin has explicitly opted in to fanning out across a publisher network. See docs/governance/property/managed-networks#security-considerations."), "last_updated": z.string().datetime().describe("ISO 8601 timestamp indicating when this reference was last updated").optional() }).catchall(z.any()).describe("URL reference variant - points to the authoritative location of the adagents.json file"), z.object({ "$schema": z.string().describe("JSON Schema identifier for this adagents.json file").optional(), "contact": z.object({ "name": z.string().min(1).max(255).describe("Name of the entity managing this file (e.g., 'Meta Advertising Operations', 'Clear Channel Digital')"), "email": z.string().email().min(1).max(255).describe("Contact email for questions or issues with this authorization file").optional(), "domain": z.string().regex(new RegExp("^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$")).describe("Primary domain of the entity managing this file").optional(), "seller_id": z.string().min(1).max(255).describe("Seller ID from IAB Tech Lab sellers.json (if applicable)").optional(), "tag_id": z.string().min(1).max(100).describe("TAG Certified Against Fraud ID for verification (if applicable)").optional(), "privacy_policy_url": z.string().url().describe("URL to the entity's privacy policy. Used for consumer consent flows when interacting with this sales agent.").optional() }).catchall(z.any()).describe("Contact information for the entity managing this adagents.json file (may be publisher or third-party operator)").optional(), "catalog_etag": z.string().min(1).max(255).describe("Opaque publisher-controlled cache validator for the public catalog portions of this file (`properties[]`, `collections[]`, `placements[]`, `formats[]`, `signals[]`, and tag metadata). Publishers SHOULD change this value whenever any catalog entry or catalog-scoped authorization changes, even when the hosting URL and HTTP validators stay the same. Buyer SDKs SHOULD cache resolved catalog lookups by URL plus `catalog_etag` (falling back to HTTP ETag/Last-Modified, then bounded TTL when absent) and re-resolve placement, format, collection, property, and signal references when it changes. This value is not a cryptographic digest; it is a compact version token such as a deployment hash, revision ID, or ISO timestamp.").optional(), "catalog_role": z.literal("community_format_registry").describe("Marks a catalog payload intended for the fixed AgenticAdvertising.org community-format registry. This self-description is not provenance: consumers accept reference_renderer only when the document was obtained from the configured, reviewed community-registry origin.").optional(), "properties": z.array(z.object({ "property_id": z.string().regex(new RegExp("^[a-z0-9_]+$")).describe("Unique identifier for this property (optional). Enables referencing properties by ID instead of repeating full objects.").optional(), "property_type": z.enum(["website","mobile_app","ctv_app","desktop_app","dooh","podcast","radio","linear_tv","streaming_audio","ai_assistant"]).describe("Type of advertising property"), "name": z.string().describe("Human-readable property name"), "identifiers": z.array(z.object({ "type": z.enum(["domain","subdomain","network_id","ios_bundle","android_package","apple_app_store_id","google_play_id","roku_store_id","fire_tv_asin","samsung_app_id","apple_tv_bundle","bundle_id","venue_id","screen_id","openooh_venue_type","rss_url","apple_podcast_id","spotify_collection_id","podcast_guid","station_id","facility_id"]).describe("Type of identifier for this property"), "value": z.string().describe("The identifier value. For domain type: 'example.com' matches base domain plus www and m subdomains; 'edition.example.com' matches that specific subdomain; '*.example.com' matches ALL subdomains but NOT base domain") }).catchall(z.any())).describe("Array of identifiers for this property"), "tags": z.array(z.string().regex(new RegExp("^[a-z0-9_]+$")).describe("Tag for categorizing publisher properties. Must be lowercase alphanumeric with underscores only.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Tags for categorization and grouping (e.g., network membership, content categories)").optional(), "supported_channels": z.array(z.enum(["display","olv","social","search","ctv","linear_tv","radio","streaming_audio","podcast","dooh","ooh","print","cinema","email","gaming","retail_media","influencer","affiliate","product_placement","sponsored_intelligence"]).describe("Standardized advertising media channels describing how buyers allocate budget. Channels are planning abstractions, not technical substrates. See the Media Channel Taxonomy specification for detailed definitions.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Advertising channels this property supports (e.g., ['display', 'olv', 'social']). Publishers declare which channels their inventory aligns with. Properties may support multiple channels. See the Media Channel Taxonomy for definitions.").optional(), "publisher_domain": z.string().describe("Domain where adagents.json should be checked for authorization validation. Optional in adagents.json (file location implies domain).").optional() }).catchall(z.any()).describe("An advertising property that can be validated via adagents.json")).describe("Array of all properties covered by this adagents.json file. Defines the canonical property list that authorized agents reference.").optional(), "revoked_publisher_domains": z.array(z.object({ "publisher_domain": z.string().regex(new RegExp("^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$")).describe("Publisher domain being revoked. Matches against the same canonicalized form used in `publisher_properties[].publisher_domain`."), "revoked_at": z.string().datetime().describe("ISO 8601 timestamp when this publisher was revoked. Validators MAY use this to order revocations against their own cached state."), "reason": z.enum(["relationship_ended","compliance_violation","publisher_request","other"]).describe("Reason for revocation. **Operator-internal self-classification for review routing — not a public accusation.** `relationship_ended` is the routine commercial case. `compliance_violation` SHOULD be used only when the network has itself determined the publisher is out of policy; for un-adjudicated third-party allegations (regulator inquiries, advertiser complaints, ongoing investigations), use `other` to avoid making a discoverable adverse statement. `publisher_request` is for publisher-initiated exits. Compare to sellers.json, which deliberately carries no reason field for the same exposure concern.").optional() }).catchall(z.any())).describe("Publisher domains explicitly removed from this managed network. Validators MUST treat any publisher domain listed here as no-longer-authorized, taking precedence over any appearance of the same domain in `authorized_agents[].publisher_properties[].publisher_domain` / `.publisher_domains[]`, in `authorized_agents[].properties[].publisher_domain` (`inline_properties` authorization type), or in top-level `properties[].publisher_domain`. Lets a network propagate per-publisher revocations on the next refresh instead of waiting for the file-level 7-day cache cap. Validators MUST hold previously-observed `(publisher_domain, revoked_at)` tuples for 7 days from the validator's first observation, even if the entry vanishes from a subsequent fetch — this closes the rollback gap where an attacker re-serves a stale file with the revocation removed. Networks SHOULD retain entries for at least 7 days after `revoked_at` so validators that didn't observe the original entry still pick it up on refresh.").optional(), "collections": z.array(z.object({ "collection_id": z.string().describe("Publisher-assigned identifier for this collection. Declared in the publisher's adagents.json collections array. Products reference collections via collection selectors with publisher_domain and collection_ids. Use distribution identifiers for cross-seller matching across publishers."), "name": z.string().describe("Human-readable collection name"), "kind": z.enum(["series","publication","event_series","rotation"]).describe("What kind of content program this is. Helps agents interpret installments correctly. Defaults to 'series' when absent.").optional(), "description": z.string().describe("What the collection is about").optional(), "genre": z.array(z.string()).describe("Genre tags. When genre_taxonomy is present, values are taxonomy IDs (e.g., IAB Content Taxonomy 3.0 codes). Otherwise free-form.").optional(), "genre_taxonomy": z.string().describe("Taxonomy system for genre values (e.g., 'iab_content_3.0'). When present, genre values should be valid taxonomy IDs. Recommended for machine-readable brand safety evaluation.").optional(), "language": z.string().describe("Primary language (BCP 47 tag, e.g., 'en', 'es-MX')").optional(), "content_rating": z.object({ "system": z.enum(["tv_parental","mpaa","podcast","esrb","bbfc","fsk","acb","chvrs","csa","pegi","custom"]).describe("Rating system used"), "rating": z.string().describe("Rating value within the system (e.g., 'TV-PG', 'R', 'explicit')") }).catchall(z.any()).describe("Baseline content rating for the collection. Individual installments may override this.").optional(), "cadence": z.enum(["daily","weekly","monthly","seasonal","event","irregular"]).describe("How frequently the collection releases new installments").optional(), "season": z.string().describe("Current or most recent season identifier (e.g., '3', '2026', 'spring_2026'). A lightweight label — not a full season object.").optional(), "status": z.enum(["active","hiatus","ended","upcoming"]).describe("Lifecycle status of the collection").optional(), "production_quality": z.enum(["professional","prosumer","ugc"]).describe("Production quality tier. Seller-declared. Maps to OpenRTB content.prodq (professional=1, prosumer=2, ugc=3).").optional(), "talent": z.array(z.object({ "role": z.enum(["host","guest","creator","cast","narrator","producer","correspondent","commentator","analyst"]).describe("Role of this person on the collection or installment"), "name": z.string().describe("Person's name as credited on the collection"), "brand_url": z.string().url().describe("URL to this person's brand.json entry. Enables buyer agents to evaluate the talent's brand identity and associations.").optional() }).catchall(z.any()).describe("A person associated with a collection or installment, with an optional link to their brand.json identity")).describe("Hosts, recurring cast, creators associated with the collection. Each talent entry may include a brand_url linking to their brand.json identity.").optional(), "special": z.object({ "name": z.string().describe("Name of the event (e.g., 'Olympics 2028', 'Super Bowl LXI')"), "category": z.enum(["awards","championship","concert","conference","election","festival","gala","holiday","premiere","product_launch","reunion","tribute"]).describe("Category of the event").optional(), "starts": z.string().datetime().describe("When the event starts (ISO 8601)").optional(), "ends": z.string().datetime().describe("When the event ends (ISO 8601). Omit for single-day events.").optional() }).catchall(z.any()).describe("When present, this collection is a special — content anchored to a real-world event or occasion. Individual installments may override with their own event context.").optional(), "limited_series": z.object({ "total_installments": z.number().int().gte(1).describe("Planned number of installments in the series"), "starts": z.string().datetime().describe("When the series begins (ISO 8601)").optional(), "ends": z.string().datetime().describe("When the series ends (ISO 8601)").optional() }).catchall(z.any()).describe("When present, this collection is a limited series — a bounded run with a defined arc, installment count, and end date.").optional(), "distribution": z.array(z.object({ "publisher_domain": z.string().describe("Domain of the publisher platform where the collection is distributed (e.g., 'youtube.com', 'spotify.com')"), "identifiers": z.array(z.object({ "type": z.enum(["apple_podcast_id","spotify_collection_id","rss_url","podcast_guid","amazon_music_id","iheart_id","podcast_index_id","youtube_channel_id","youtube_channel_handle","youtube_channel_url","youtube_playlist_id","amazon_title_id","roku_channel_id","pluto_channel_id","tubi_id","peacock_id","tiktok_id","twitch_channel","imdb_id","gracenote_id","eidr_id","domain","substack_id"]).describe("Type of distribution identifier"), "value": z.string().describe("The identifier value") }).strict()).describe("Platform-specific identifiers for the collection on this publisher") }).catchall(z.any()).describe("A collection's presence on a specific publisher platform, identified by platform-specific identifiers. Enables cross-seller matching when the same collection is sold by different agents.")).describe("Where this collection is distributed. Each entry maps the collection to a publisher platform with platform-specific identifiers. Collections SHOULD include at least one platform-independent identifier (imdb_id, gracenote_id, eidr_id) when available.").optional(), "deadline_policy": z.object({ "booking_lead_days": z.number().int().gte(0).describe("Days before scheduled_at by which the placement must be booked").optional(), "cancellation_lead_days": z.number().int().gte(0).describe("Days before scheduled_at by which cancellation is penalty-free").optional(), "material_stages": z.array(z.object({ "stage": z.string().describe("Stage identifier. Standard values: 'draft' (needs seller processing), 'final' (production-ready)."), "lead_days": z.number().int().gte(0).describe("Days before scheduled_at this stage is due"), "label": z.string().describe("What the seller needs at this stage").optional() }).catchall(z.any())).describe("Default material submission stages. Items MUST be in chronological order (earliest due first). Agents compute due_at as: installment.scheduled_at minus lead_days.").optional(), "business_days_only": z.boolean().describe("When true, lead_days counts business days (Mon-Fri) rather than calendar days. Defaults to false.").default(false) }).catchall(z.any()).describe("Default deadline rules for installments of this collection. Agents compute absolute deadlines from each installment's scheduled_at and these lead times. Installments with explicit deadlines override this policy.").optional(), "related_collections": z.array(z.object({ "collection_id": z.string().describe("The related collection's collection_id within this seller's response"), "relationship": z.enum(["spinoff","companion","sequel","prequel","crossover"]).describe("How the collections are related") }).strict()).describe("Relationships to other collections (spin-offs, companion collections, etc.). Each entry references another collection by collection_id within the same publisher's adagents.json.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension object for platform-specific, vendor-namespaced parameters. Extensions are always optional and must be namespaced under a vendor/platform key (e.g., ext.gam, ext.roku). Used for custom capabilities, partner-specific configuration, and features being proposed for standardization.").optional() }).catchall(z.any()).describe("A recurring inventory container — a named program, publication, event series, or rotation that produces bookable installments on a defined cadence. The kind field indicates how to interpret this collection: 'series' for TV/podcast programs, 'publication' for print/newsletter titles, 'event_series' for live events, 'rotation' for DOOH scheduling. Declared in the publisher's adagents.json and referenced by products via collection selectors.")).describe("Collections produced or distributed by this publisher. Declares the content programs whose inventory is sold through authorized agents. Products in get_products responses reference these collections by collection_id.").optional(), "placements": z.array(z.object({ "placement_id": z.string().describe("Stable placement identifier unique within this adagents.json file."), "name": z.string().describe("Human-readable placement name (e.g., 'Homepage Banner', 'Pre-roll', 'Sponsored Listing Slot 1')."), "description": z.string().describe("Description of where and how this placement appears.").optional(), "tags": z.array(z.string()).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Tags for grouping and querying placements across properties and products (e.g., 'homepage', 'native', 'premium', 'pre_roll').").optional(), "property_ids": z.array(z.string().regex(new RegExp("^[a-z0-9_]+$")).describe("Identifier for a publisher property. Must be lowercase alphanumeric with underscores only.")).describe("Property IDs in this adagents.json where this placement can appear.").optional(), "property_tags": z.array(z.string().regex(new RegExp("^[a-z0-9_]+$")).describe("Tag for categorizing publisher properties. Must be lowercase alphanumeric with underscores only.")).describe("Property tags in this adagents.json where this placement can appear. Useful for network-wide positions such as 'pre_roll' or 'homepage_native_feed'.").optional(), "collection_ids": z.array(z.string()).describe("Optional collection IDs in this adagents.json where this placement is valid. Use to narrow a placement to specific content programs carried on the selected properties.").optional(), "channels": z.array(z.enum(["display","olv","social","search","ctv","linear_tv","radio","streaming_audio","podcast","dooh","ooh","print","cinema","email","gaming","retail_media","influencer","affiliate","product_placement","sponsored_intelligence"]).describe("Standardized advertising media channels describing how buyers allocate budget. Channels are planning abstractions, not technical substrates. See the Media Channel Taxonomy specification for detailed definitions.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Advertising channels where this placement can run. Products that reference the placement may narrow this set but should not broaden it.").optional(), "presentation_ref": z.object({ "uri": z.string().url().regex(new RegExp("^https://")).describe("Publisher-controlled HTTPS URL for the presentation metadata. Consumers MUST apply the same SSRF, redirect, response-size, timeout, and DNS-rebinding protections used for format_schema fetches."), "digest": z.string().regex(new RegExp("^sha256:[a-f0-9]{64}$")).describe("SHA-256 content digest. Consumers cache by uri@digest and MUST fail closed on a digest mismatch."), "media_type": z.literal("application/vnd.adcp.placement-presentation+json").describe("Media type of the referenced declarative presentation document."), "schema_version": z.literal("1.0").describe("Version of /schemas/core/placement-presentation.json used to validate and compose the referenced document.") }).strict().describe("Optional publisher-specific declarative frame for representing this placement's real chrome offline. It composes around the selected creative rendering unless the publisher-delegated preview_provider route explicitly covers placement presentation. It MUST NOT be promoted to or copied onto a shared format entry.").optional(), "preview_provider": z.object({ "agent_url": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL of the delegated creative-agent endpoint. Buyers call get_adcp_capabilities and preview_creative on this endpoint. They MUST allow only public IPs, pin DNS resolution through connection, refuse redirects, cap time and response size, and attach provider credentials only after exact normalized-origin binding."), "authority": z.literal("publisher_designated").describe("Explicitly states that authority comes from the publisher-hosted placement declaration. The provider's rendering_origin metadata is informational and remains non-authoritative elsewhere."), "routes": z.array(z.object({ "format_option_id": z.string().min(1).describe("Format option in this adagents.json placement for which the delegation applies. It MUST resolve through the same-file top-level formats[] catalog or an inline placement format declaration."), "capability_id": z.string().regex(new RegExp("^[a-zA-Z0-9_-]+$")).describe("Agent-local preview capability advertised by the delegated provider. The provider's canonical format declaration MUST satisfy the resolved placement format option."), "covers_placement_presentation": z.boolean().describe("True only when the publisher delegates both creative rendering and the complete placement-specific frame to this route. When false or omitted, consumers compose any presentation_ref around the provider's creative render.").default(false) }).strict()) }).strict().describe("Optional publisher delegation to a callable AdCP preview provider for specific format options on this placement. This publisher-origin route is the only mechanism that grants preview authority inside the placement's scope; an agent's own rendering_origin description does not.").optional(), "format_options": z.array(z.union([z.object({ "format_option_id": z.string().describe("Matches a `format_option_id` in the file's top-level `formats[]`."), "locale_policy": z.object({ "accepted_language_ranges": z.array(z.string().regex(new RegExp("^(?:[a-z]{2,8}(?:-[A-Z][a-z]{3})?(?:-(?:[A-Z]{2}|[0-9]{3}))?(?:-(?:[a-z0-9]{5,8}|[0-9][a-z0-9]{3}))*(?:-[0-9a-wy-z](?:-[a-z0-9]{2,8})+)*(?:-x(?:-[a-z0-9]{1,8})+)?|x(?:-[a-z0-9]{1,8})+)$")).min(2).max(63).describe("A well-formed BCP 47 language tag used by AdCP only as language identity. Script and region may refine that identity; other valid BCP 47 subtags remain part of tag matching but do not make this a general locale-settings object. It does not determine currency, time zone, number/date formatting, market, or legal jurisdiction. The AdCP canonical wire profile requires lower-case language and variants, title-case script, and upper-case region (for example `en-US`, `zh-Hant-TW`, or `x-private`). RFC 5646 comparisons are case-insensitive and its case regularization is optional; AdCP intentionally requires this stricter single wire spelling and receivers MUST reject differently cased tags rather than silently normalizing them. The schema pattern enforces the AdCP casing profile and extension structure for commonly used tags; conforming receivers additionally validate the complete RFC 5646 grammar and registry rules. Every new AdCP field carrying BCP 47 language identity or a concrete language range MUST reference this schema instead of declaring independent string constraints.")).max(50).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Concrete canonical BCP 47 language ranges accepted by this format option. RFC 4647 Basic Filtering is directional: seller range fr accepts variant fr-CA, but seller range fr-CA does not accept variant fr or fr-FR. Use zxx explicitly for language-neutral creative; und means unknown and is not a wildcard.") }).catchall(z.any()).describe("Optional placement-local locale policy. When the resolved top-level format declares a policy, every placement range must be contained by one of its ranges; otherwise this introduces a narrowing of the unconstrained format. The resolved effective route remains canonical-only.").optional() }).catchall(z.any()).describe("Reference an entry in the file's top-level `formats[]` by `format_option_id`. Resolved at validation time. additionalProperties: true so placement-local fields (display_name, etc.) carry through without forcing a full inline declaration."), z.object({ "format_option_id": z.string().describe("Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'.").optional(), "publisher_domain": z.string().regex(new RegExp("^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$")).describe("Namespace for `format_option_id` when this declaration references or narrows a publisher-declared format option from that publisher's adagents.json top-level `formats[]`. Product-local options omit this field and are selected by `format_option_id` within the target product.").optional(), "display_name": z.string().describe("Optional seller-controlled human-readable label for this format declaration. Used by buyer dashboards, catalog UIs, and reporting surfaces to show a seller's own naming ('Homepage Takeover', 'Branded Canvas', 'Reels Premium Video') rather than the raw `format_kind` or `format_option_id`. Has no machine semantics — buyer agents route on `format_kind` and `format_option_id`; `display_name` is purely for human presentation. Freeform; no enumeration. Sellers SHOULD keep it stable once published to avoid dashboard churn.").optional(), "sample_render_url": z.string().url().regex(new RegExp("^https://")).describe("Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.").optional(), "applies_to_channels": z.array(z.enum(["display","olv","social","search","ctv","linear_tv","radio","streaming_audio","podcast","dooh","ooh","print","cinema","email","gaming","retail_media","influencer","affiliate","product_placement","sponsored_intelligence"]).describe("Standardized advertising media channels describing how buyers allocate budget. Channels are planning abstractions, not technical substrates. See the Media Channel Taxonomy specification for detailed definitions.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Optional subset of the parent product's `channels` to which this declaration applies. When omitted, the declaration applies to ALL channels declared on the product. Lets a multi-channel product (e.g., `channels: ['display', 'video']`) carry distinct format_options per channel — `format_options: [{format_kind: 'image', applies_to_channels: ['display']}, {format_kind: 'video_hosted', applies_to_channels: ['video']}]`. Buyers ship channel-appropriate manifests per `applies_to_channels`.").optional(), "seller_preference": z.enum(["preferred","accepted","discouraged"]).describe("Optional soft routing hint *within* a product's accepted set of formats — NOT an enforcement axis. `preferred` — seller actively recommends this format (often because of measurement, viewability, or render-quality differences); `accepted` — supported on equal footing with other format_options (default when omitted); `discouraged` — supported but suboptimal (e.g., legacy 3p-tag where the seller would prefer html5 for OM-SDK coverage). Buyer agents picking between format_options SHOULD respect seller preferences when their own constraints don't override.\n\n**Not an enforcement axis (normative).** `seller_preference` does NOT carry the meaning of 'this format won't work / required-only'. That case is structural: `format_options[]` IS the closed set of accepted formats; anything outside the list is rejected at `create_media_buy` regardless of preference. A seller that accepts only one format lists exactly that one entry — the structural fact does the enforcement work, no enum value needed. There is intentionally no `required` value; preference is bounded to *ranking within the already-accepted set*, not gating into it.").optional(), "locale_policy": z.object({ "accepted_language_ranges": z.array(z.string().regex(new RegExp("^(?:[a-z]{2,8}(?:-[A-Z][a-z]{3})?(?:-(?:[A-Z]{2}|[0-9]{3}))?(?:-(?:[a-z0-9]{5,8}|[0-9][a-z0-9]{3}))*(?:-[0-9a-wy-z](?:-[a-z0-9]{2,8})+)*(?:-x(?:-[a-z0-9]{1,8})+)?|x(?:-[a-z0-9]{1,8})+)$")).min(2).max(63).describe("A well-formed BCP 47 language tag used by AdCP only as language identity. Script and region may refine that identity; other valid BCP 47 subtags remain part of tag matching but do not make this a general locale-settings object. It does not determine currency, time zone, number/date formatting, market, or legal jurisdiction. The AdCP canonical wire profile requires lower-case language and variants, title-case script, and upper-case region (for example `en-US`, `zh-Hant-TW`, or `x-private`). RFC 5646 comparisons are case-insensitive and its case regularization is optional; AdCP intentionally requires this stricter single wire spelling and receivers MUST reject differently cased tags rather than silently normalizing them. The schema pattern enforces the AdCP casing profile and extension structure for commonly used tags; conforming receivers additionally validate the complete RFC 5646 grammar and registry rules. Every new AdCP field carrying BCP 47 language identity or a concrete language range MUST reference this schema instead of declaring independent string constraints.")).max(50).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Concrete canonical BCP 47 language ranges accepted by this format option. RFC 4647 Basic Filtering is directional: seller range fr accepts variant fr-CA, but seller range fr-CA does not accept variant fr or fr-FR. Use zxx explicitly for language-neutral creative; und means unknown and is not a wildcard.") }).catchall(z.any()).describe("Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.").optional(), "canonical_formats_only": z.boolean().describe("When true, this format declaration has no clean v1 projection and SDKs MUST NOT synthesize a v1 `format_id` for it. Buyers reading the product on the v1 wire path see this declaration absent from `format_ids`; only v2-aware buyers (reading `format_options`) discover it. Set explicitly for `format_kind: \"custom\"` declarations (no canonical exists in v1 to project onto) and for declarations whose canonical/parameter shape cannot round-trip through a v1 named format without semantic loss. The protocol does NOT mint synthetic v1 format_ids for unmappable declarations — the alternative (an `aao-synth/*` namespace populated automatically) was considered and rejected because adopters would index on synthetic IDs that have no stable identity. Producers SHOULD set `canonical_formats_only: true` rather than omit the declaration from `format_options` — explicit v2-only is more useful than silent absence.").default(false), "experimental": z.boolean().describe("When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum.").default(false), "format_shape": z.string().describe("REQUIRED when `format_kind: \"custom\"`; otherwise MUST be absent. Recognized global pattern this custom shape is an instance of, drawn from the [format-shape vocabulary registry](/schemas/core/format-shape-vocabulary.json) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are valid (validators MAY soft-warn) — adopters CAN ship a shape that isn't yet in the registry. Adding entries is a vocabulary PR. Once a `format_shape` entry sees 2+ adopters with substantively similar `format_schema` content for 90+ days, the working group may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.").optional(), "v1_format_ref": z.array(z.object({ "agent_url": z.string().url().describe("URL of the agent that defines this format (e.g., 'https://creative.adcontextprotocol.org' for standard formats, or 'https://publisher.com/.well-known/adcp/sales' for custom formats). Callers comparing two `format-id` values MUST canonicalize `agent_url` per the AdCP URL canonicalization rules before treating two formats as the same. See docs/reference/url-canonicalization."), "id": z.string().regex(new RegExp("^[a-zA-Z0-9_-]+$")).describe("Format identifier within the agent's namespace (e.g., 'display_static', 'video_hosted', 'audio_standard'). When used alone, references a template format. When combined with dimension/duration fields, creates a parameterized format ID for a specific variant."), "width": z.number().int().gte(1).describe("Width in pixels for visual formats. When specified, height must also be specified. Both fields together create a parameterized format ID for dimension-specific variants.").optional(), "height": z.number().int().gte(1).describe("Height in pixels for visual formats. When specified, width must also be specified. Both fields together create a parameterized format ID for dimension-specific variants.").optional(), "duration_ms": z.number().gte(1).describe("Duration in milliseconds for time-based formats (video, audio). When specified, creates a parameterized format ID. Omit to reference a template format without parameters.").optional(), "pixel_ratio": z.number().gt(0).describe("Required intrinsic-pixel density for a parameterized visual format, expressed as intrinsic pixels per logical pixel. Requires `width` and `height`. Example: `{id: \"display_image\", width: 300, height: 250, pixel_ratio: 2}` identifies a 300×250 logical render supplied by a 600×500 image. Omit for the backward-compatible 1x variant.").optional() }).catchall(z.any()).describe("**DEPRECATED in 3.2.** Legacy named-format reference retained for 3.x compatibility. This JSON object — never a plain string — identifies a format by its declaring agent and local slug, and may carry legacy logical dimensions, pixel ratio, or duration parameters. New products and manifests use canonical `format_options`, `format_kind`, and `format_option_ref`; creative-agent build routing uses `creative.supported_formats[].capability_id`.")).describe("Authoritative v2 → v1 link, expressed as an array of one or more v1 `format_id` ({agent_url, id}) values. Each entry asserts that this canonical-formats declaration IS the same underlying format as the referenced v1 named format. Always an array (single-ref is `[{...}]`) so the multi-size case below has a clean wire shape — adopters surveyed in the SDK implementor review pushed for this over the lossy single-ref form.\n\nThe v2 declaration's `params` MUST narrow (be compatible with) each referenced v1 format's `requirements` — see the 'Narrows — formal definition' section in canonical-formats.mdx. SDKs comparing dual-emitted shapes (`Product.format_ids[]` ⊇ entries from `v1_format_ref` AND `Product.format_options[]` carrying this declaration) treat the link as the authoritative pairing and run the narrowing check between this declaration and EACH referenced v1 format file's `requirements`.\n\n**Multi-size fan-out (normative).** When the declaration carries `params.sizes: [{w,h}, ...]` (multi-size flexible slot), sellers SHOULD carry one `v1_format_ref[]` entry per size, each pointing at the per-size v1 named format in the AAO catalog. Example: a multi-size image declaration with `sizes: [300x250, 728x90, 970x250]` SHOULD carry `v1_format_ref: [{aao, display_300x250_image}, {aao, display_728x90_image}, {aao, display_970x250_image}]`. v1-only buyers then see the product on all three sizes via the `format_ids[]` dual-emission. When `v1_format_ref[]` count < `sizes[]` count, SDKs MUST emit `FORMAT_DECLARATION_V1_LOSSY_MULTI_SIZE` on the response `errors[]` (advisory, alongside the partial-coverage v1 emit — NOT in place of it). SDKs MAY (non-normative) fan out automatically by catalog lookup when `v1_format_ref[]` has length 1 and `sizes[]` has length N — opt-in, requires catalog access; sellers asserting refs is the source of truth.\n\nMutually exclusive with `canonical_formats_only: true` — a declaration can EITHER assert no v1 projection (`canonical_formats_only: true`) OR link to v1 named formats (`v1_format_ref[]`), never both. When neither is present, SDKs fall back to the resolution order in `v1-canonical-mapping.json` (seller's explicit `canonical` field on the v1 file → registry glob → structural match → fail-closed).\n\nThis is the v2-side authoritative replacement for the v1-side `canonical_parameters` field on `format.json` (which is deprecated for 3.1, removed at 4.0). Sellers SHOULD prefer authoring v2 declarations with `v1_format_ref[]` over mirroring the v2 shape onto v1 files via `canonical_parameters`; the directional link (v2 declaration → v1 identifiers) is the same fact without the parallel-shape drift surface.\n\n**AAO-hosted convention (normative).** For IAB-standard formats (image dimensions, VAST/DAAST tags, standard third-party tags, HTML5 banner bundles), sellers SHOULD point each `v1_format_ref[].agent_url` at the AAO-hosted canonical agent URL `https://creative.adcontextprotocol.org` and use the registry-published id (e.g., `display_300x250_image`, `video_vast_30s`, `audio_standard_30s`, `display_300x250_html`, `display_js`). This converges the v1-wire namespace: every seller's IAB MREC points at the same `{agent_url, id}` pair, so v1-only buyers' allowlists work uniformly. Without this convention, every publisher's 300x250 ships with a different `v1_format_ref` (theirs vs nytimes.example vs cnn.example vs …) and the v1 wire fragments into per-publisher namespaces — exactly what canonical-formats was designed to eliminate.\n\nFor platform-specific formats (Meta Reels, TikTok Spark, Snap Spotlight, etc.), each `v1_format_ref[].agent_url` SHOULD point at the platform's own agent_url when the platform has adopted AdCP and publishes its own `adagents.json` with `formats[]`. When the platform has NOT adopted AdCP, sellers SHOULD point at the AAO community-registry mirror — `https://creative.adcontextprotocol.org/translated/` + `id: ` (e.g., `https://creative.adcontextprotocol.org/translated/meta` + `id: meta_reels`). This keeps the v1 namespace converged across all sellers selling that platform's inventory until the platform owns its own adagents.json.\n\n**Platform-adoption cutover (normative).** When a platform adopts AdCP and publishes its own adagents.json, sellers MUST update `v1_format_ref[].agent_url` to the platform's adopted agent_url in the same minor release as the AAO mirror entry's `superseded_by` field goes live (see `static/schemas/source/adagents.json#superseded_by`). The AAO mirror entry SHOULD continue serving for ≥1 minor release after `superseded_by` is set, returning an advisory 'superseded' marker so v1 buyer allowlists keyed on the mirror URL get an explicit signal rather than a silent break. **Identity-confusion note**: the mirror URL is *format-shape namespace*, NOT seller identity. Inventory authorization always flows from `authorized_agents[]` + publisher signing keys; a buyer matching `v1_format_ref[].agent_url` against an allowlist is matching format-shape provenance, not seller identity.\n\n**Mirror domain migration (3.1).** Earlier drafts used `https://mirror.adcontextprotocol.org/translated/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — sibling content under the AAO catalog domain we already host. Adopters who hardcoded the earlier mirror URL MUST migrate to the new path; the canonical-formats.mdx migration section documents the move. No transitional redirect is currently published (the earlier subdomain was never provisioned).\n\nFor seller-bespoke formats (a publisher's `acme_homepage_takeover` that doesn't fit IAB conventions), each `v1_format_ref[].agent_url` is the seller's own agent_url and the id is seller-namespaced. These won't appear in `v1-canonical-mapping.json`'s registry; they're seller-asserted only.").optional(), "format_schema": z.object({ "uri": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL identifying the extension. `https://` is mandatory — `http://`, `file://`, `data:`, and other schemes are rejected at the schema layer (defense-in-depth on top of the fetch-contract normative rules). The URI base is the owning agent's URL; the path identifies the extension within that agent. Example: 'https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel'. The full fetch contract — SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds — is documented on `product-format-declaration.json#format_schema` and applies to ALL fetches of this reference shape regardless of whether the field is named `format_schema` (load-bearing for validation) or `platform_extensions` (informational); the *transport* rules are identical, only the *consumption* semantics differ."), "digest": z.string().regex(new RegExp("^sha256:[a-f0-9]{64}$")).describe("SHA-256 content digest of the extension definition (sha256:). Used to detect drift — if the agent revises the extension, the digest changes and cached definitions become invalid.") }).catchall(z.any()).describe("REQUIRED when `format_kind: \"custom\"`; otherwise MUST be absent. URI+digest reference to a fetchable schema describing this custom shape's actual `params` and `slots`. Same hosting model as `platform_extensions`: open-ecosystem publishers host the artifact at the canonical URI on their subdomain; closed-platform / walled-garden shapes resolve through the AAO mirror at `https://creative.adcontextprotocol.org/translated/...`. Buyer agents fetch by `uri@digest` (immutable per digest, aggressive caching, `Cache-Control: public, max-age=31536000, immutable`), validate `params` and `slots` against the fetched schema, and reason about manifests structurally — same mechanic as platform_extensions but at the format-structure level. Without `format_schema`, custom shapes would be opaque to buyer agents and the protocol would regress to per-seller integration code; that's why the schema is required, not optional.\n\n**Fetch contract (normative)** — `format_schema` is load-bearing for validation (unlike `platform_extensions`, which is informational on the *consumption* side). The *transport* rules below apply identically to BOTH fields — any SDK fetching a `platform-extension-ref.json` URI MUST apply this contract regardless of whether the field name is `format_schema` or `platform_extensions`. A shared SDK fetch path that drops to the weakest bar undermines `format_schema`'s hardening. The consumption distinction (load-bearing vs informational) is about *what the body means*; the transport distinction is `https`-and-allowlisted regardless.\n\n- **Transport**: `https` only. Buyers MUST reject `http://`, `file://`, `data:`, and any non-`https` scheme. The URI MUST resolve to a JSON document that is itself a valid JSON Schema (Draft 07 or 2020-12; producers MUST declare `$schema`).\n- **SSRF protection**: buyers MUST resolve the URI hostname and reject if any resolved address is in RFC 1918 private space (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`), loopback (`127.0.0.0/8`, `::1`), link-local (`169.254.0.0/16`, `fe80::/10`), CGNAT (`100.64.0.0/10`), or any RFC 6761 special-use name (`.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. Buyers MUST pin the connection to the resolved IP (or re-resolve and re-validate the allowlist per request) to defeat DNS rebinding.\n- **HTTP redirects**: MUST be disabled. If a follow is implemented at all, the redirect target MUST pass the same scheme + SSRF + allowlist checks; otherwise the fetch hard-fails. Open redirects on same-origin paths are otherwise a free SSRF primitive.\n- **Response size cap**: response body MUST be capped at 1 MiB. Enforce during streaming, not after full buffering. Over-cap hard-fails identically to digest mismatch.\n- **Timeout**: SDKs SHOULD apply a fetch timeout ≤5 seconds. Timeout SHOULD be treated identically to an HTTP 5xx response (transient — retry policy at the SDK's discretion; on persistent failure surface as unresolved and skip the declaration for this session).\n- **Digest verification**: SHA-256 of the response body MUST equal `digest`. **Digest mismatch is a hard fail** — the buyer MUST treat the format declaration as unresolvable and MUST NOT validate manifests against the mismatched body. A divergent digest is either a malicious substitution or producer error; either way, falling back to the un-verified body breaks the trust model. Digest format: `sha256:` prefix + 64 lowercase hex characters. Cache key is `uri@digest`; digest mismatch MUST NOT be cached as a negative result keyed on `uri` alone (defeats CDN-flap recovery), and MUST be distinguishable in telemetry from network 5xx / 404 (sustained mismatch is a substitution-attack signal, not a flap).\n- **Sandboxing of `$ref`**: fetched schemas MAY use `$ref`. Buyers MUST resolve `$ref` only to URIs that are (a) same-origin as the parent `format_schema.uri` after RFC 3986 §6 normalization (lowercase scheme + host, strip default port, normalize path dot-segments, no userinfo component), OR (b) hosted under the AAO catalog domain (`https://creative.adcontextprotocol.org/...`), OR (c) intra-document JSON Pointer refs (`#/...`) bounded to the parent document's parsed tree. Cross-origin `$ref` to arbitrary URIs MUST be rejected. `$ref: file://...` MUST be rejected unconditionally. Transitive `$ref` chains MUST be bounded at depth ≤8 AND `$ref` count ≤256 across the resolved tree (depth 8 with breadth 100 per level is 10^16 nodes — depth alone is not enough). Publishers SHOULD inline rather than $ref where possible.\n- **Schema-compile bounds (DoS protection)**: validators MUST bound CPU/memory on fetched schemas. Recommended: compiled-schema keyword count ≤10 000, `pattern` regexes evaluated with a non-backtracking engine (re2) OR under a per-pattern timeout, per-manifest validation budget ≤250 ms (exceeded budget → treat manifest as invalid, surface telemetry signal). Without these, a 'valid' schema with catastrophic regex backtracking or exponential `allOf`/`anyOf` expansion pins a CPU forever.\n- **Cache**: buyers cache fetched schemas by `uri@digest` and treat them as immutable (the same hosting contract as `platform_extensions`). On `404`, network partition, or persistent fetch failure, buyers SHOULD degrade gracefully (treat the declaration as unresolved, skip it for the current `get_products` response, surface via `errors[]` with the relevant code) rather than failing the entire session.\n- **Schema-not-valid handling**: if the fetched body parses as JSON but is not a valid JSON Schema, the buyer MUST treat the declaration as unresolvable (same as digest mismatch) and surface via `errors[]`. Validators MUST NOT attempt partial validation against an invalid schema.\n- **AAO catalog trust**: `https://creative.adcontextprotocol.org/*` is a single trust anchor in the same-origin allowlist; compromise of the catalog domain or its CA compromises every buyer agent. Catalog-served bodies MUST be digest-pinned identically to origin fetches (the digest is on the *parent* `format_schema.uri@digest`, not on the catalog response). Future hardening (signed bodies, transparency log) is tracked separately.").optional() }).and(z.union([z.object({ "format_kind": z.literal("image"), "params": z.intersection(z.intersection(z.object({ "experimental": z.boolean().describe("When true, this canonical or seller narrowing may not work as declared. Adopters SHOULD preflight it with validate_input or in a sandbox and SHOULD NOT route production budget without testing; experimental status never makes the deprecated v1 path preferable. Drivers include unsettled spec shape, an adopter runtime gap, and custom shapes awaiting promotion. This replaces the earlier status plus runtime_status axes. Sellers SHOULD set experimental whenever a canonical or declaration is not production-ready.").default(false), "deprecated": z.boolean().describe("When true, this canonical (or a seller's specific narrowing of it) is going away. Existing adopters are supported through the deprecation cycle; new adoption is discouraged. Pair with `migration_target_version` to indicate when the canonical is expected to be removed. Distinct from `experimental`: an experimental canonical may stabilize and stop being experimental; a deprecated canonical is on a sunset path.").default(false), "v1_translatable": z.boolean().describe("Whether this canonical has any v1 named-format equivalent. `true` (default) — the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1→v2 projection via `v1-canonical-mapping.json` is meaningful. `false` — the canonical is inherently new in v2 and has no v1 form; v1's `list_creative_formats` couldn't express it because the underlying concept (algorithmic surface composition, AI-surface mentions, retail-media catalog placements, multi-card carousels) didn't exist as a v1 named-format archetype.\n\nLets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The six inherently-v2 canonicals in 3.2 are `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`, `seller_rendered_stateful_display`, and `coordinated_placements`.").default(true), "since_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version that introduced this canonical (e.g., '3.1', '3.2'). Lets adopters reason about minimum protocol version requirements when consuming a format declaration. Patch precision is intentionally rejected — canonicals are introduced at minor-version boundaries.").optional(), "migration_target_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version by which the working group expects this canonical to stabilize, surface a breaking revision, or (when `deprecated: true`) be removed. Patch precision is intentionally rejected — canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown').").optional(), "composition_model": z.enum(["deterministic","algorithmic"]).describe("Whether the surface composes deterministically (buyer can predict per-slot rendering — sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing — responsive_creative, agent_placement).").optional(), "provenance_required": z.boolean().describe("When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent.").optional(), "platform_extensions": z.array(z.object({ "uri": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL identifying the extension. `https://` is mandatory — `http://`, `file://`, `data:`, and other schemes are rejected at the schema layer (defense-in-depth on top of the fetch-contract normative rules). The URI base is the owning agent's URL; the path identifies the extension within that agent. Example: 'https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel'. The full fetch contract — SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds — is documented on `product-format-declaration.json#format_schema` and applies to ALL fetches of this reference shape regardless of whether the field is named `format_schema` (load-bearing for validation) or `platform_extensions` (informational); the *transport* rules are identical, only the *consumption* semantics differ."), "digest": z.string().regex(new RegExp("^sha256:[a-f0-9]{64}$")).describe("SHA-256 content digest of the extension definition (sha256:). Used to detect drift — if the agent revises the extension, the digest changes and cached definitions become invalid.") }).catchall(z.any()).describe("Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` — divergent digests in one response indicate producer-side error (e.g., concurrent extension revision mid-render). Buyers encountering divergent digests for the same URI MUST fail closed: treat all references to that URI as unresolved and surface a validation error rather than picking one branch silently. **Across responses**, digest divergence is normal — extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.")).describe("Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\n\n**Collision precedence (normative).** When two or more `platform_extensions[]` entries on the same declaration extend the same target (e.g., both extend `tracking`) with overlapping field names, **array order is authoritative — later entries override earlier ones on a per-field basis** (last-in-array-wins). SDKs MUST surface the overlap via the `errors[]` array on the `get_products` response with a structured code (`FORMAT_DECLARATION_DIVERGENT` is appropriate when the overlap appears across dual-emitted shapes; a producer-self-emitted overlap on a single declaration SHOULD use the same code with `error.details: { collision_kind: \"platform_extension_field\", target, overlapping_fields, winning_extension_uri }`). Producers SHOULD avoid the collision by emitting one extension per target or by partitioning fields across extensions; the deterministic precedence is for last-resort consistency across SDK implementations, not a sanctioned merging strategy.").optional(), "synthesis_nondeterministic": z.boolean().describe("When true, the format's production pipeline is genuinely nondeterministic — the platform cannot guarantee that synthesis from a given input set produces in-spec output. Veo / Sora / Runway-class generative video, and other AI-synthesis flows where output dimensions, duration, or quality vary per run. Implies a different validation contract: predictive `validate_input` is impossible; the platform's own post-synthesis QA loop applies; if the QA loop exhausts without producing a valid artifact, `build_creative` returns task_failed with a synthesis_failed reason. Distinct from `composition_model` (which describes how the surface composes per-slot rendering, not whether synthesis is deterministic). When false or absent, the format's production is predictable enough that `validate_input` can predict output properties from input properties.\n\n**Compatibility with `asset_source` / `item_production_model`**: `synthesis_nondeterministic: true` MAY pair with any of `seller_pre_rendered_from_brief`, `seller_human_designed`, or `agent_synthesized` (the QA loop is concept-level, not source-specific — 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.").default(false), "slots": z.array(z.object({ "asset_group_id": z.string().describe("Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings."), "asset_type": z.enum(["image","video","audio","text","markdown","url","html","css","javascript","vast","daast","webhook","brief","catalog","published_post","zip","card","object","pixel_tracker","vast_tracker","daast_tracker"]).describe("Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `published_post` is an existing-post reference asset, not uploaded media bytes and not a catalog row. `card` is the multi-card carousel element type (see card-asset.json). `pixel_tracker` / `vast_tracker` / `daast_tracker` are the renderer-fired measurement-tracker primitives — see `/schemas/core/assets/pixel-tracker-asset.json` and the VAST / DAAST tracker schemas. `object` is a last-resort fallback for structured non-asset inputs that don't fit any primitive asset_type — prefer specific types whenever possible."), "required": z.boolean().describe("Whether this slot is required for a valid manifest.").default(false), "min": z.number().int().gte(0).describe("Minimum count for repeatable / pool slots.").optional(), "max": z.number().int().gte(1).describe("Maximum count for repeatable / pool slots.").optional(), "max_chars": z.number().int().gte(1).describe("Per-slot character limit. Valid only when `asset_type` is `text`, `markdown`, or `brief`. Mutually exclusive with `max_size_kb` (which applies to binary asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "max_size_kb": z.number().int().gte(1).describe("Per-slot file size limit in kilobytes. Valid only when `asset_type` is `image`, `video`, `audio`, or `zip`. Mutually exclusive with `max_chars` (which applies to text asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Accepted intrinsic-pixel densities for this image-bearing slot. Valid when `asset_type` is `image`, and on a `card` slot where it constrains each card's image media (video media is unaffected). This makes density available to every canonical carrying image assets (native, carousel, responsive, companion images, and image itself), not only `format_kind: image`. When the image canonical also declares top-level `params.pixel_ratios`, the effective set is the intersection; an empty intersection is invalid. One matching asset satisfies the slot unless `required_pixel_ratios` requires rendition coverage.").optional(), "required_pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Required density coverage for an image rendition set. Valid only when `asset_type` is `image` and `pixel_ratios` is also declared. Every value MUST appear in the effective accepted set after intersecting any top-level image `params.pixel_ratios`, and the manifest slot value MUST be an array containing exactly one matching image rendition for each required ratio. Other accepted ratios remain optional. For example, `pixel_ratios: [1, 1.5, 2]` with `required_pixel_ratios: [1, 2]` requires the 1x and 2x renditions while making 1.5x optional. SDKs enforce intersection, subset, coverage, and duplicate-ratio rules because JSON Schema draft-07 cannot express them generically.").optional(), "logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("When `asset_group_id` is `logo`, renderer-facing brand.json logo slots acceptable for this format slot. Producers selecting from brand.json SHOULD prefer `logos[]` entries whose `slots[]` intersects this list, then apply `visual_guidelines.logo_usage_rules[]`.").optional(), "required_logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Subset of `logo_slots` for which this format expects explicit logo coverage. A manifest or brand-derived logo pool SHOULD include at least one usable logo for each required slot; if coverage is missing, builders SHOULD surface a validation warning or approval mapping instead of guessing from prose.").optional(), "description": z.string().describe("Human-readable description of what the slot expects from the buyer.").optional(), "consumed_for_production": z.boolean().describe("Dispatch hint for `build_creative` and v1↔v2 wire translators: when `true`, the slot's value is consumed as INPUT to a production step (host-read script, brief copy fed to generative synthesis, catalog feed driving per-SKU rendering) and is not rendered verbatim. When `false` (default), the slot's value is rendered verbatim on the placement (image bytes, video file, display tag).\n\nMotivates the v1↔v2 dispatch table: pre-v2 buyers shipped production-consumed inputs separately in a `inputs` map on the build_creative request; v2 collapses inputs and rendered assets into a single `assets` map keyed by `asset_group_id`. SDK translators between v1 and v2 use this flag per canonical to know which assets in the v2 manifest map back to v1 `inputs` vs v1 `assets`. Without the per-slot flag the dispatch table lives in adopter code and every SDK gets it slightly different.\n\nProducers SHOULD set this explicitly on slots whose consumption pattern isn't obvious (host-read scripts on `audio_hosted`, briefs on generative `video_hosted`, catalog feeds on `sponsored_placement`). For canonicals where every slot is render-verbatim (`image`, `display_tag`, `video_vast`), the default `false` is sufficient and the flag MAY be omitted.").default(false) }).catchall(z.any()).and(z.intersection(z.intersection(z.any(), z.intersection(z.any(), z.any())), z.intersection(z.any(), z.intersection(z.any(), z.any()))))).describe("Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.").optional(), "required_connections": z.array(z.object({ "provider": z.string().describe("Stable provider or platform namespace, preferably lowercase. Examples: `social.example`, `shortvideo.example`, or a seller-defined namespace. Omit only when the requirement is provider-agnostic, or when an `authorization_url` fully routes the human to the correct provider-specific connection flow.").optional(), "connection_type": z.enum(["advertiser_account","publisher_identity","post_authorization"]).describe("Kind of downstream connection required. `advertiser_account` is the platform account used to buy/manage ads. `publisher_identity` is the creator, page, channel, organization, or profile that owns source posts. `post_authorization` is a post-scoped grant when the platform authorizes individual posts instead of, or in addition to, the owning identity."), "required_for": z.array(z.string().min(1)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Concrete AdCP protocol operation names that require this downstream connection. Sellers SHOULD include this in product declarations when the requirement is known ahead of time, and in AUTHORIZATION_REQUIRED details when it explains the failed operation. Prefer specific operation names such as `list_creatives`, `sync_creatives`, `create_media_buy`, `get_media_buy_delivery`, or `get_creative_delivery` over broad category labels such as `reporting`.").optional(), "scope": z.enum(["account","identity","post","unknown"]).describe("Granularity of the downstream grant.").optional(), "status": z.enum(["connected","missing","pending","expired","revoked","not_required","unknown"]).describe("Current seller-observed state for this downstream connection when known. Product declarations MAY omit status or use `unknown`; AUTHORIZATION_REQUIRED details SHOULD use `missing`, `expired`, or `revoked` for the connection that blocked the call.").optional(), "connection_id": z.string().describe("Seller-defined identifier for an already-created downstream connection. Omit when no connection exists yet or when exposing it would leak platform/account state.").optional(), "resource_ref": z.object({ "platform_account_id": z.string().describe("Provider-native advertiser or business account id, when safe to disclose.").optional(), "identity_id": z.string().describe("Provider-native creator, page, channel, organization, or profile id, when safe to disclose.").optional(), "handle": z.string().describe("Provider-native public handle for the owning identity, when available.").optional(), "profile_url": z.string().url().describe("Public URL for the owning identity, when available.").optional(), "post_id": z.string().describe("Provider-native post id, when the grant is post-scoped or the failed request referenced a specific post.").optional(), "post_url": z.string().url().describe("Public URL for the referenced post, when available.").optional() }).catchall(z.any()).describe("Optional opaque provider-native resource hint, such as a platform account id, profile URL, handle, channel id, post id, or post URL. This is a hint for routing authorization, not proof that authorization exists.").optional(), "authorization_url": z.string().url().describe("Seller-hosted or provider-hosted URL where a human can complete or restore this downstream connection.").optional(), "authorization_instructions": z.string().describe("Human-readable instructions for completing or restoring this downstream connection.").optional(), "expires_at": z.string().datetime().describe("Expiration time for the downstream grant, when known.").optional() }).catchall(z.any()).and(z.any()).describe("A seller/platform-side connection or grant required by a product, format, or request. This is not the AdCP caller credential: the AdCP request is still authenticated once, and the seller uses these stored downstream connections to call a platform or service on the buyer's behalf. Use this shape for platforms that require more than one downstream grant, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.")).describe("Downstream platform connections or grants required to use this format declaration. These are in addition to the single AdCP caller credential. Use this when a platform product requires multiple downstream grants, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.").optional(), "reference_mutability": z.enum(["immutable_snapshot","mutable_requires_reapproval","mutable_auto_recheck"]).describe("Policy for formats whose `slots` accept a `published_post` reference. `immutable_snapshot`: seller snapshots the referenced post at approval and later source changes do not change the served creative. `mutable_requires_reapproval`: the source post may change and material changes require review before continued serving. `mutable_auto_recheck`: the source post may change and the seller continuously or periodically rechecks authorization/policy without requiring buyer resubmission. Omit when the format has no `published_post` slot.").optional(), "production_window_business_days": z.number().int().gte(0).describe("Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative).").optional() }).catchall(z.any()).describe("Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints)."), z.union([z.any().refine((value) => !z.union([z.any(), z.any(), z.any(), z.any(), z.any()]).safeParse(value).success, "Invalid input: Should NOT be valid against schema"), z.any().refine((value) => !z.union([z.any(), z.any(), z.any(), z.any(), z.any(), z.any()]).safeParse(value).success, "Invalid input: Should NOT be valid against schema"), z.union([z.any(), z.any(), z.any(), z.any()]), z.any().refine((value) => !z.union([z.any(), z.any(), z.any(), z.any(), z.any(), z.any(), z.any()]).safeParse(value).success, "Invalid input: Should NOT be valid against schema")]).describe("Exactly one of: (a) fixed (`width` + `height` both set), (b) multi-size (`sizes` set), (c) responsive (any of `min_width`/`max_width`/`min_height`/`max_height` set), (d) none (no size constraint declared — accepts any dimensions). Combining modes (e.g., `width` + `sizes`) is rejected at schema layer; same rule on `html5` and `display_tag` canonicals.")), z.intersection(z.any(), z.any())).describe("Static image creative format. Slots: `image_main` (image asset, file or hosted URL), optional `headline` (text), `body_text` (text), `cta` (text/enum), `landing_page_url` (url). Tracking model: impression pixel + click URL via universal_macros, with optional viewability pixel. Distinct from `html5` (interactive bundles) and `display_tag` (third-party served). AR/dimensions narrow to specific sizes via product parameters — covers IAB display sizes (300x250, 728x90, 970x250, etc.) without a separate iab_size enum.") }), z.object({ "format_kind": z.literal("html5"), "params": z.intersection(z.object({ "experimental": z.boolean().describe("When true, this canonical or seller narrowing may not work as declared. Adopters SHOULD preflight it with validate_input or in a sandbox and SHOULD NOT route production budget without testing; experimental status never makes the deprecated v1 path preferable. Drivers include unsettled spec shape, an adopter runtime gap, and custom shapes awaiting promotion. This replaces the earlier status plus runtime_status axes. Sellers SHOULD set experimental whenever a canonical or declaration is not production-ready.").default(false), "deprecated": z.boolean().describe("When true, this canonical (or a seller's specific narrowing of it) is going away. Existing adopters are supported through the deprecation cycle; new adoption is discouraged. Pair with `migration_target_version` to indicate when the canonical is expected to be removed. Distinct from `experimental`: an experimental canonical may stabilize and stop being experimental; a deprecated canonical is on a sunset path.").default(false), "v1_translatable": z.boolean().describe("Whether this canonical has any v1 named-format equivalent. `true` (default) — the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1→v2 projection via `v1-canonical-mapping.json` is meaningful. `false` — the canonical is inherently new in v2 and has no v1 form; v1's `list_creative_formats` couldn't express it because the underlying concept (algorithmic surface composition, AI-surface mentions, retail-media catalog placements, multi-card carousels) didn't exist as a v1 named-format archetype.\n\nLets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The six inherently-v2 canonicals in 3.2 are `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`, `seller_rendered_stateful_display`, and `coordinated_placements`.").default(true), "since_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version that introduced this canonical (e.g., '3.1', '3.2'). Lets adopters reason about minimum protocol version requirements when consuming a format declaration. Patch precision is intentionally rejected — canonicals are introduced at minor-version boundaries.").optional(), "migration_target_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version by which the working group expects this canonical to stabilize, surface a breaking revision, or (when `deprecated: true`) be removed. Patch precision is intentionally rejected — canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown').").optional(), "composition_model": z.enum(["deterministic","algorithmic"]).describe("Whether the surface composes deterministically (buyer can predict per-slot rendering — sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing — responsive_creative, agent_placement).").optional(), "provenance_required": z.boolean().describe("When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent.").optional(), "platform_extensions": z.array(z.object({ "uri": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL identifying the extension. `https://` is mandatory — `http://`, `file://`, `data:`, and other schemes are rejected at the schema layer (defense-in-depth on top of the fetch-contract normative rules). The URI base is the owning agent's URL; the path identifies the extension within that agent. Example: 'https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel'. The full fetch contract — SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds — is documented on `product-format-declaration.json#format_schema` and applies to ALL fetches of this reference shape regardless of whether the field is named `format_schema` (load-bearing for validation) or `platform_extensions` (informational); the *transport* rules are identical, only the *consumption* semantics differ."), "digest": z.string().regex(new RegExp("^sha256:[a-f0-9]{64}$")).describe("SHA-256 content digest of the extension definition (sha256:). Used to detect drift — if the agent revises the extension, the digest changes and cached definitions become invalid.") }).catchall(z.any()).describe("Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` — divergent digests in one response indicate producer-side error (e.g., concurrent extension revision mid-render). Buyers encountering divergent digests for the same URI MUST fail closed: treat all references to that URI as unresolved and surface a validation error rather than picking one branch silently. **Across responses**, digest divergence is normal — extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.")).describe("Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\n\n**Collision precedence (normative).** When two or more `platform_extensions[]` entries on the same declaration extend the same target (e.g., both extend `tracking`) with overlapping field names, **array order is authoritative — later entries override earlier ones on a per-field basis** (last-in-array-wins). SDKs MUST surface the overlap via the `errors[]` array on the `get_products` response with a structured code (`FORMAT_DECLARATION_DIVERGENT` is appropriate when the overlap appears across dual-emitted shapes; a producer-self-emitted overlap on a single declaration SHOULD use the same code with `error.details: { collision_kind: \"platform_extension_field\", target, overlapping_fields, winning_extension_uri }`). Producers SHOULD avoid the collision by emitting one extension per target or by partitioning fields across extensions; the deterministic precedence is for last-resort consistency across SDK implementations, not a sanctioned merging strategy.").optional(), "synthesis_nondeterministic": z.boolean().describe("When true, the format's production pipeline is genuinely nondeterministic — the platform cannot guarantee that synthesis from a given input set produces in-spec output. Veo / Sora / Runway-class generative video, and other AI-synthesis flows where output dimensions, duration, or quality vary per run. Implies a different validation contract: predictive `validate_input` is impossible; the platform's own post-synthesis QA loop applies; if the QA loop exhausts without producing a valid artifact, `build_creative` returns task_failed with a synthesis_failed reason. Distinct from `composition_model` (which describes how the surface composes per-slot rendering, not whether synthesis is deterministic). When false or absent, the format's production is predictable enough that `validate_input` can predict output properties from input properties.\n\n**Compatibility with `asset_source` / `item_production_model`**: `synthesis_nondeterministic: true` MAY pair with any of `seller_pre_rendered_from_brief`, `seller_human_designed`, or `agent_synthesized` (the QA loop is concept-level, not source-specific — 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.").default(false), "slots": z.array(z.object({ "asset_group_id": z.string().describe("Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings."), "asset_type": z.enum(["image","video","audio","text","markdown","url","html","css","javascript","vast","daast","webhook","brief","catalog","published_post","zip","card","object","pixel_tracker","vast_tracker","daast_tracker"]).describe("Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `published_post` is an existing-post reference asset, not uploaded media bytes and not a catalog row. `card` is the multi-card carousel element type (see card-asset.json). `pixel_tracker` / `vast_tracker` / `daast_tracker` are the renderer-fired measurement-tracker primitives — see `/schemas/core/assets/pixel-tracker-asset.json` and the VAST / DAAST tracker schemas. `object` is a last-resort fallback for structured non-asset inputs that don't fit any primitive asset_type — prefer specific types whenever possible."), "required": z.boolean().describe("Whether this slot is required for a valid manifest.").default(false), "min": z.number().int().gte(0).describe("Minimum count for repeatable / pool slots.").optional(), "max": z.number().int().gte(1).describe("Maximum count for repeatable / pool slots.").optional(), "max_chars": z.number().int().gte(1).describe("Per-slot character limit. Valid only when `asset_type` is `text`, `markdown`, or `brief`. Mutually exclusive with `max_size_kb` (which applies to binary asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "max_size_kb": z.number().int().gte(1).describe("Per-slot file size limit in kilobytes. Valid only when `asset_type` is `image`, `video`, `audio`, or `zip`. Mutually exclusive with `max_chars` (which applies to text asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Accepted intrinsic-pixel densities for this image-bearing slot. Valid when `asset_type` is `image`, and on a `card` slot where it constrains each card's image media (video media is unaffected). This makes density available to every canonical carrying image assets (native, carousel, responsive, companion images, and image itself), not only `format_kind: image`. When the image canonical also declares top-level `params.pixel_ratios`, the effective set is the intersection; an empty intersection is invalid. One matching asset satisfies the slot unless `required_pixel_ratios` requires rendition coverage.").optional(), "required_pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Required density coverage for an image rendition set. Valid only when `asset_type` is `image` and `pixel_ratios` is also declared. Every value MUST appear in the effective accepted set after intersecting any top-level image `params.pixel_ratios`, and the manifest slot value MUST be an array containing exactly one matching image rendition for each required ratio. Other accepted ratios remain optional. For example, `pixel_ratios: [1, 1.5, 2]` with `required_pixel_ratios: [1, 2]` requires the 1x and 2x renditions while making 1.5x optional. SDKs enforce intersection, subset, coverage, and duplicate-ratio rules because JSON Schema draft-07 cannot express them generically.").optional(), "logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("When `asset_group_id` is `logo`, renderer-facing brand.json logo slots acceptable for this format slot. Producers selecting from brand.json SHOULD prefer `logos[]` entries whose `slots[]` intersects this list, then apply `visual_guidelines.logo_usage_rules[]`.").optional(), "required_logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Subset of `logo_slots` for which this format expects explicit logo coverage. A manifest or brand-derived logo pool SHOULD include at least one usable logo for each required slot; if coverage is missing, builders SHOULD surface a validation warning or approval mapping instead of guessing from prose.").optional(), "description": z.string().describe("Human-readable description of what the slot expects from the buyer.").optional(), "consumed_for_production": z.boolean().describe("Dispatch hint for `build_creative` and v1↔v2 wire translators: when `true`, the slot's value is consumed as INPUT to a production step (host-read script, brief copy fed to generative synthesis, catalog feed driving per-SKU rendering) and is not rendered verbatim. When `false` (default), the slot's value is rendered verbatim on the placement (image bytes, video file, display tag).\n\nMotivates the v1↔v2 dispatch table: pre-v2 buyers shipped production-consumed inputs separately in a `inputs` map on the build_creative request; v2 collapses inputs and rendered assets into a single `assets` map keyed by `asset_group_id`. SDK translators between v1 and v2 use this flag per canonical to know which assets in the v2 manifest map back to v1 `inputs` vs v1 `assets`. Without the per-slot flag the dispatch table lives in adopter code and every SDK gets it slightly different.\n\nProducers SHOULD set this explicitly on slots whose consumption pattern isn't obvious (host-read scripts on `audio_hosted`, briefs on generative `video_hosted`, catalog feeds on `sponsored_placement`). For canonicals where every slot is render-verbatim (`image`, `display_tag`, `video_vast`), the default `false` is sufficient and the flag MAY be omitted.").default(false) }).catchall(z.any()).and(z.intersection(z.intersection(z.any(), z.intersection(z.any(), z.any())), z.intersection(z.any(), z.intersection(z.any(), z.any()))))).describe("Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.").optional(), "required_connections": z.array(z.object({ "provider": z.string().describe("Stable provider or platform namespace, preferably lowercase. Examples: `social.example`, `shortvideo.example`, or a seller-defined namespace. Omit only when the requirement is provider-agnostic, or when an `authorization_url` fully routes the human to the correct provider-specific connection flow.").optional(), "connection_type": z.enum(["advertiser_account","publisher_identity","post_authorization"]).describe("Kind of downstream connection required. `advertiser_account` is the platform account used to buy/manage ads. `publisher_identity` is the creator, page, channel, organization, or profile that owns source posts. `post_authorization` is a post-scoped grant when the platform authorizes individual posts instead of, or in addition to, the owning identity."), "required_for": z.array(z.string().min(1)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Concrete AdCP protocol operation names that require this downstream connection. Sellers SHOULD include this in product declarations when the requirement is known ahead of time, and in AUTHORIZATION_REQUIRED details when it explains the failed operation. Prefer specific operation names such as `list_creatives`, `sync_creatives`, `create_media_buy`, `get_media_buy_delivery`, or `get_creative_delivery` over broad category labels such as `reporting`.").optional(), "scope": z.enum(["account","identity","post","unknown"]).describe("Granularity of the downstream grant.").optional(), "status": z.enum(["connected","missing","pending","expired","revoked","not_required","unknown"]).describe("Current seller-observed state for this downstream connection when known. Product declarations MAY omit status or use `unknown`; AUTHORIZATION_REQUIRED details SHOULD use `missing`, `expired`, or `revoked` for the connection that blocked the call.").optional(), "connection_id": z.string().describe("Seller-defined identifier for an already-created downstream connection. Omit when no connection exists yet or when exposing it would leak platform/account state.").optional(), "resource_ref": z.object({ "platform_account_id": z.string().describe("Provider-native advertiser or business account id, when safe to disclose.").optional(), "identity_id": z.string().describe("Provider-native creator, page, channel, organization, or profile id, when safe to disclose.").optional(), "handle": z.string().describe("Provider-native public handle for the owning identity, when available.").optional(), "profile_url": z.string().url().describe("Public URL for the owning identity, when available.").optional(), "post_id": z.string().describe("Provider-native post id, when the grant is post-scoped or the failed request referenced a specific post.").optional(), "post_url": z.string().url().describe("Public URL for the referenced post, when available.").optional() }).catchall(z.any()).describe("Optional opaque provider-native resource hint, such as a platform account id, profile URL, handle, channel id, post id, or post URL. This is a hint for routing authorization, not proof that authorization exists.").optional(), "authorization_url": z.string().url().describe("Seller-hosted or provider-hosted URL where a human can complete or restore this downstream connection.").optional(), "authorization_instructions": z.string().describe("Human-readable instructions for completing or restoring this downstream connection.").optional(), "expires_at": z.string().datetime().describe("Expiration time for the downstream grant, when known.").optional() }).catchall(z.any()).and(z.any()).describe("A seller/platform-side connection or grant required by a product, format, or request. This is not the AdCP caller credential: the AdCP request is still authenticated once, and the seller uses these stored downstream connections to call a platform or service on the buyer's behalf. Use this shape for platforms that require more than one downstream grant, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.")).describe("Downstream platform connections or grants required to use this format declaration. These are in addition to the single AdCP caller credential. Use this when a platform product requires multiple downstream grants, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.").optional(), "reference_mutability": z.enum(["immutable_snapshot","mutable_requires_reapproval","mutable_auto_recheck"]).describe("Policy for formats whose `slots` accept a `published_post` reference. `immutable_snapshot`: seller snapshots the referenced post at approval and later source changes do not change the served creative. `mutable_requires_reapproval`: the source post may change and material changes require review before continued serving. `mutable_auto_recheck`: the source post may change and the seller continuously or periodically rechecks authorization/policy without requiring buyer resubmission. Omit when the format has no `published_post` slot.").optional(), "production_window_business_days": z.number().int().gte(0).describe("Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative).").optional() }).catchall(z.any()).describe("Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints)."), z.union([z.any().refine((value) => !z.union([z.any(), z.any(), z.any(), z.any(), z.any()]).safeParse(value).success, "Invalid input: Should NOT be valid against schema"), z.any().refine((value) => !z.union([z.any(), z.any(), z.any(), z.any(), z.any(), z.any()]).safeParse(value).success, "Invalid input: Should NOT be valid against schema"), z.union([z.any(), z.any(), z.any(), z.any()]), z.any().refine((value) => !z.union([z.any(), z.any(), z.any(), z.any(), z.any(), z.any(), z.any()]).safeParse(value).success, "Invalid input: Should NOT be valid against schema")]).describe("Exactly one of: (a) fixed (`width` + `height` both set), (b) multi-size (`sizes` set), (c) responsive (any of `min_width`/`max_width`/`min_height`/`max_height` set), (d) none (no size constraint declared — accepts any dimensions). Combining modes is rejected at schema layer.")).describe("Interactive HTML5 banner delivered as a zip archive. Slot: `html5_bundle` (zip asset). Tracking model: MRAID + IAB Open Measurement (OM-SDK) + click-tag macro substitution + backup image fallback. Receivers unpack the zip, validate internal structure, and serve from CDN. Distinct from `image` (static, non-interactive) and `display_tag` (third-party served). The zip's entry point is typically `index.html`; click handling uses `clickTag` (or `clickTAG`) macro substitution.") }), z.object({ "format_kind": z.literal("display_tag"), "params": z.intersection(z.object({ "experimental": z.boolean().describe("When true, this canonical or seller narrowing may not work as declared. Adopters SHOULD preflight it with validate_input or in a sandbox and SHOULD NOT route production budget without testing; experimental status never makes the deprecated v1 path preferable. Drivers include unsettled spec shape, an adopter runtime gap, and custom shapes awaiting promotion. This replaces the earlier status plus runtime_status axes. Sellers SHOULD set experimental whenever a canonical or declaration is not production-ready.").default(false), "deprecated": z.boolean().describe("When true, this canonical (or a seller's specific narrowing of it) is going away. Existing adopters are supported through the deprecation cycle; new adoption is discouraged. Pair with `migration_target_version` to indicate when the canonical is expected to be removed. Distinct from `experimental`: an experimental canonical may stabilize and stop being experimental; a deprecated canonical is on a sunset path.").default(false), "v1_translatable": z.boolean().describe("Whether this canonical has any v1 named-format equivalent. `true` (default) — the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1→v2 projection via `v1-canonical-mapping.json` is meaningful. `false` — the canonical is inherently new in v2 and has no v1 form; v1's `list_creative_formats` couldn't express it because the underlying concept (algorithmic surface composition, AI-surface mentions, retail-media catalog placements, multi-card carousels) didn't exist as a v1 named-format archetype.\n\nLets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The six inherently-v2 canonicals in 3.2 are `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`, `seller_rendered_stateful_display`, and `coordinated_placements`.").default(true), "since_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version that introduced this canonical (e.g., '3.1', '3.2'). Lets adopters reason about minimum protocol version requirements when consuming a format declaration. Patch precision is intentionally rejected — canonicals are introduced at minor-version boundaries.").optional(), "migration_target_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version by which the working group expects this canonical to stabilize, surface a breaking revision, or (when `deprecated: true`) be removed. Patch precision is intentionally rejected — canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown').").optional(), "composition_model": z.enum(["deterministic","algorithmic"]).describe("Whether the surface composes deterministically (buyer can predict per-slot rendering — sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing — responsive_creative, agent_placement).").optional(), "provenance_required": z.boolean().describe("When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent.").optional(), "platform_extensions": z.array(z.object({ "uri": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL identifying the extension. `https://` is mandatory — `http://`, `file://`, `data:`, and other schemes are rejected at the schema layer (defense-in-depth on top of the fetch-contract normative rules). The URI base is the owning agent's URL; the path identifies the extension within that agent. Example: 'https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel'. The full fetch contract — SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds — is documented on `product-format-declaration.json#format_schema` and applies to ALL fetches of this reference shape regardless of whether the field is named `format_schema` (load-bearing for validation) or `platform_extensions` (informational); the *transport* rules are identical, only the *consumption* semantics differ."), "digest": z.string().regex(new RegExp("^sha256:[a-f0-9]{64}$")).describe("SHA-256 content digest of the extension definition (sha256:). Used to detect drift — if the agent revises the extension, the digest changes and cached definitions become invalid.") }).catchall(z.any()).describe("Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` — divergent digests in one response indicate producer-side error (e.g., concurrent extension revision mid-render). Buyers encountering divergent digests for the same URI MUST fail closed: treat all references to that URI as unresolved and surface a validation error rather than picking one branch silently. **Across responses**, digest divergence is normal — extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.")).describe("Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\n\n**Collision precedence (normative).** When two or more `platform_extensions[]` entries on the same declaration extend the same target (e.g., both extend `tracking`) with overlapping field names, **array order is authoritative — later entries override earlier ones on a per-field basis** (last-in-array-wins). SDKs MUST surface the overlap via the `errors[]` array on the `get_products` response with a structured code (`FORMAT_DECLARATION_DIVERGENT` is appropriate when the overlap appears across dual-emitted shapes; a producer-self-emitted overlap on a single declaration SHOULD use the same code with `error.details: { collision_kind: \"platform_extension_field\", target, overlapping_fields, winning_extension_uri }`). Producers SHOULD avoid the collision by emitting one extension per target or by partitioning fields across extensions; the deterministic precedence is for last-resort consistency across SDK implementations, not a sanctioned merging strategy.").optional(), "synthesis_nondeterministic": z.boolean().describe("When true, the format's production pipeline is genuinely nondeterministic — the platform cannot guarantee that synthesis from a given input set produces in-spec output. Veo / Sora / Runway-class generative video, and other AI-synthesis flows where output dimensions, duration, or quality vary per run. Implies a different validation contract: predictive `validate_input` is impossible; the platform's own post-synthesis QA loop applies; if the QA loop exhausts without producing a valid artifact, `build_creative` returns task_failed with a synthesis_failed reason. Distinct from `composition_model` (which describes how the surface composes per-slot rendering, not whether synthesis is deterministic). When false or absent, the format's production is predictable enough that `validate_input` can predict output properties from input properties.\n\n**Compatibility with `asset_source` / `item_production_model`**: `synthesis_nondeterministic: true` MAY pair with any of `seller_pre_rendered_from_brief`, `seller_human_designed`, or `agent_synthesized` (the QA loop is concept-level, not source-specific — 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.").default(false), "slots": z.array(z.object({ "asset_group_id": z.string().describe("Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings."), "asset_type": z.enum(["image","video","audio","text","markdown","url","html","css","javascript","vast","daast","webhook","brief","catalog","published_post","zip","card","object","pixel_tracker","vast_tracker","daast_tracker"]).describe("Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `published_post` is an existing-post reference asset, not uploaded media bytes and not a catalog row. `card` is the multi-card carousel element type (see card-asset.json). `pixel_tracker` / `vast_tracker` / `daast_tracker` are the renderer-fired measurement-tracker primitives — see `/schemas/core/assets/pixel-tracker-asset.json` and the VAST / DAAST tracker schemas. `object` is a last-resort fallback for structured non-asset inputs that don't fit any primitive asset_type — prefer specific types whenever possible."), "required": z.boolean().describe("Whether this slot is required for a valid manifest.").default(false), "min": z.number().int().gte(0).describe("Minimum count for repeatable / pool slots.").optional(), "max": z.number().int().gte(1).describe("Maximum count for repeatable / pool slots.").optional(), "max_chars": z.number().int().gte(1).describe("Per-slot character limit. Valid only when `asset_type` is `text`, `markdown`, or `brief`. Mutually exclusive with `max_size_kb` (which applies to binary asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "max_size_kb": z.number().int().gte(1).describe("Per-slot file size limit in kilobytes. Valid only when `asset_type` is `image`, `video`, `audio`, or `zip`. Mutually exclusive with `max_chars` (which applies to text asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Accepted intrinsic-pixel densities for this image-bearing slot. Valid when `asset_type` is `image`, and on a `card` slot where it constrains each card's image media (video media is unaffected). This makes density available to every canonical carrying image assets (native, carousel, responsive, companion images, and image itself), not only `format_kind: image`. When the image canonical also declares top-level `params.pixel_ratios`, the effective set is the intersection; an empty intersection is invalid. One matching asset satisfies the slot unless `required_pixel_ratios` requires rendition coverage.").optional(), "required_pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Required density coverage for an image rendition set. Valid only when `asset_type` is `image` and `pixel_ratios` is also declared. Every value MUST appear in the effective accepted set after intersecting any top-level image `params.pixel_ratios`, and the manifest slot value MUST be an array containing exactly one matching image rendition for each required ratio. Other accepted ratios remain optional. For example, `pixel_ratios: [1, 1.5, 2]` with `required_pixel_ratios: [1, 2]` requires the 1x and 2x renditions while making 1.5x optional. SDKs enforce intersection, subset, coverage, and duplicate-ratio rules because JSON Schema draft-07 cannot express them generically.").optional(), "logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("When `asset_group_id` is `logo`, renderer-facing brand.json logo slots acceptable for this format slot. Producers selecting from brand.json SHOULD prefer `logos[]` entries whose `slots[]` intersects this list, then apply `visual_guidelines.logo_usage_rules[]`.").optional(), "required_logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Subset of `logo_slots` for which this format expects explicit logo coverage. A manifest or brand-derived logo pool SHOULD include at least one usable logo for each required slot; if coverage is missing, builders SHOULD surface a validation warning or approval mapping instead of guessing from prose.").optional(), "description": z.string().describe("Human-readable description of what the slot expects from the buyer.").optional(), "consumed_for_production": z.boolean().describe("Dispatch hint for `build_creative` and v1↔v2 wire translators: when `true`, the slot's value is consumed as INPUT to a production step (host-read script, brief copy fed to generative synthesis, catalog feed driving per-SKU rendering) and is not rendered verbatim. When `false` (default), the slot's value is rendered verbatim on the placement (image bytes, video file, display tag).\n\nMotivates the v1↔v2 dispatch table: pre-v2 buyers shipped production-consumed inputs separately in a `inputs` map on the build_creative request; v2 collapses inputs and rendered assets into a single `assets` map keyed by `asset_group_id`. SDK translators between v1 and v2 use this flag per canonical to know which assets in the v2 manifest map back to v1 `inputs` vs v1 `assets`. Without the per-slot flag the dispatch table lives in adopter code and every SDK gets it slightly different.\n\nProducers SHOULD set this explicitly on slots whose consumption pattern isn't obvious (host-read scripts on `audio_hosted`, briefs on generative `video_hosted`, catalog feeds on `sponsored_placement`). For canonicals where every slot is render-verbatim (`image`, `display_tag`, `video_vast`), the default `false` is sufficient and the flag MAY be omitted.").default(false) }).catchall(z.any()).and(z.intersection(z.intersection(z.any(), z.intersection(z.any(), z.any())), z.intersection(z.any(), z.intersection(z.any(), z.any()))))).describe("Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.").optional(), "required_connections": z.array(z.object({ "provider": z.string().describe("Stable provider or platform namespace, preferably lowercase. Examples: `social.example`, `shortvideo.example`, or a seller-defined namespace. Omit only when the requirement is provider-agnostic, or when an `authorization_url` fully routes the human to the correct provider-specific connection flow.").optional(), "connection_type": z.enum(["advertiser_account","publisher_identity","post_authorization"]).describe("Kind of downstream connection required. `advertiser_account` is the platform account used to buy/manage ads. `publisher_identity` is the creator, page, channel, organization, or profile that owns source posts. `post_authorization` is a post-scoped grant when the platform authorizes individual posts instead of, or in addition to, the owning identity."), "required_for": z.array(z.string().min(1)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Concrete AdCP protocol operation names that require this downstream connection. Sellers SHOULD include this in product declarations when the requirement is known ahead of time, and in AUTHORIZATION_REQUIRED details when it explains the failed operation. Prefer specific operation names such as `list_creatives`, `sync_creatives`, `create_media_buy`, `get_media_buy_delivery`, or `get_creative_delivery` over broad category labels such as `reporting`.").optional(), "scope": z.enum(["account","identity","post","unknown"]).describe("Granularity of the downstream grant.").optional(), "status": z.enum(["connected","missing","pending","expired","revoked","not_required","unknown"]).describe("Current seller-observed state for this downstream connection when known. Product declarations MAY omit status or use `unknown`; AUTHORIZATION_REQUIRED details SHOULD use `missing`, `expired`, or `revoked` for the connection that blocked the call.").optional(), "connection_id": z.string().describe("Seller-defined identifier for an already-created downstream connection. Omit when no connection exists yet or when exposing it would leak platform/account state.").optional(), "resource_ref": z.object({ "platform_account_id": z.string().describe("Provider-native advertiser or business account id, when safe to disclose.").optional(), "identity_id": z.string().describe("Provider-native creator, page, channel, organization, or profile id, when safe to disclose.").optional(), "handle": z.string().describe("Provider-native public handle for the owning identity, when available.").optional(), "profile_url": z.string().url().describe("Public URL for the owning identity, when available.").optional(), "post_id": z.string().describe("Provider-native post id, when the grant is post-scoped or the failed request referenced a specific post.").optional(), "post_url": z.string().url().describe("Public URL for the referenced post, when available.").optional() }).catchall(z.any()).describe("Optional opaque provider-native resource hint, such as a platform account id, profile URL, handle, channel id, post id, or post URL. This is a hint for routing authorization, not proof that authorization exists.").optional(), "authorization_url": z.string().url().describe("Seller-hosted or provider-hosted URL where a human can complete or restore this downstream connection.").optional(), "authorization_instructions": z.string().describe("Human-readable instructions for completing or restoring this downstream connection.").optional(), "expires_at": z.string().datetime().describe("Expiration time for the downstream grant, when known.").optional() }).catchall(z.any()).and(z.any()).describe("A seller/platform-side connection or grant required by a product, format, or request. This is not the AdCP caller credential: the AdCP request is still authenticated once, and the seller uses these stored downstream connections to call a platform or service on the buyer's behalf. Use this shape for platforms that require more than one downstream grant, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.")).describe("Downstream platform connections or grants required to use this format declaration. These are in addition to the single AdCP caller credential. Use this when a platform product requires multiple downstream grants, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.").optional(), "reference_mutability": z.enum(["immutable_snapshot","mutable_requires_reapproval","mutable_auto_recheck"]).describe("Policy for formats whose `slots` accept a `published_post` reference. `immutable_snapshot`: seller snapshots the referenced post at approval and later source changes do not change the served creative. `mutable_requires_reapproval`: the source post may change and material changes require review before continued serving. `mutable_auto_recheck`: the source post may change and the seller continuously or periodically rechecks authorization/policy without requiring buyer resubmission. Omit when the format has no `published_post` slot.").optional(), "production_window_business_days": z.number().int().gte(0).describe("Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative).").optional() }).catchall(z.any()).describe("Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints)."), z.union([z.any().refine((value) => !z.union([z.any(), z.any(), z.any(), z.any(), z.any()]).safeParse(value).success, "Invalid input: Should NOT be valid against schema"), z.any().refine((value) => !z.union([z.any(), z.any(), z.any(), z.any(), z.any(), z.any()]).safeParse(value).success, "Invalid input: Should NOT be valid against schema"), z.union([z.any(), z.any(), z.any(), z.any()]), z.any().refine((value) => !z.union([z.any(), z.any(), z.any(), z.any(), z.any(), z.any(), z.any()]).safeParse(value).success, "Invalid input: Should NOT be valid against schema")]).describe("Exactly one of: (a) fixed (`width` + `height` both set), (b) multi-size (`sizes` set), (c) responsive (any of `min_width`/`max_width`/`min_height`/`max_height` set), (d) none (no size constraint declared — accepts any dimensions). Combining modes is rejected at schema layer.")).describe("Third-party-served display tag (JS, iframe, or 1×1 redirect). The buyer's adserver hosts the creative; the seller calls the tag URL at impression time. Slot: `tag_url` (url asset with appropriate `url_type`). Tracking model: opaque to seller — third party serves and measures. Click tracking via redirect URL substitution using universal_macros. Distinct from `image` (static asset hosted by seller) and `html5` (zip bundle hosted by seller).") }), z.object({ "format_kind": z.literal("image_carousel"), "params": z.object({ "experimental": z.boolean().describe("When true, this canonical or seller narrowing may not work as declared. Adopters SHOULD preflight it with validate_input or in a sandbox and SHOULD NOT route production budget without testing; experimental status never makes the deprecated v1 path preferable. Drivers include unsettled spec shape, an adopter runtime gap, and custom shapes awaiting promotion. This replaces the earlier status plus runtime_status axes. Sellers SHOULD set experimental whenever a canonical or declaration is not production-ready.").default(false), "deprecated": z.boolean().describe("When true, this canonical (or a seller's specific narrowing of it) is going away. Existing adopters are supported through the deprecation cycle; new adoption is discouraged. Pair with `migration_target_version` to indicate when the canonical is expected to be removed. Distinct from `experimental`: an experimental canonical may stabilize and stop being experimental; a deprecated canonical is on a sunset path.").default(false), "v1_translatable": z.boolean().describe("Whether this canonical has any v1 named-format equivalent. `true` (default) — the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1→v2 projection via `v1-canonical-mapping.json` is meaningful. `false` — the canonical is inherently new in v2 and has no v1 form; v1's `list_creative_formats` couldn't express it because the underlying concept (algorithmic surface composition, AI-surface mentions, retail-media catalog placements, multi-card carousels) didn't exist as a v1 named-format archetype.\n\nLets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The six inherently-v2 canonicals in 3.2 are `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`, `seller_rendered_stateful_display`, and `coordinated_placements`.").default(true), "since_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version that introduced this canonical (e.g., '3.1', '3.2'). Lets adopters reason about minimum protocol version requirements when consuming a format declaration. Patch precision is intentionally rejected — canonicals are introduced at minor-version boundaries.").optional(), "migration_target_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version by which the working group expects this canonical to stabilize, surface a breaking revision, or (when `deprecated: true`) be removed. Patch precision is intentionally rejected — canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown').").optional(), "composition_model": z.enum(["deterministic","algorithmic"]).describe("Whether the surface composes deterministically (buyer can predict per-slot rendering — sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing — responsive_creative, agent_placement).").optional(), "provenance_required": z.boolean().describe("When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent.").optional(), "platform_extensions": z.array(z.object({ "uri": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL identifying the extension. `https://` is mandatory — `http://`, `file://`, `data:`, and other schemes are rejected at the schema layer (defense-in-depth on top of the fetch-contract normative rules). The URI base is the owning agent's URL; the path identifies the extension within that agent. Example: 'https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel'. The full fetch contract — SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds — is documented on `product-format-declaration.json#format_schema` and applies to ALL fetches of this reference shape regardless of whether the field is named `format_schema` (load-bearing for validation) or `platform_extensions` (informational); the *transport* rules are identical, only the *consumption* semantics differ."), "digest": z.string().regex(new RegExp("^sha256:[a-f0-9]{64}$")).describe("SHA-256 content digest of the extension definition (sha256:). Used to detect drift — if the agent revises the extension, the digest changes and cached definitions become invalid.") }).catchall(z.any()).describe("Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` — divergent digests in one response indicate producer-side error (e.g., concurrent extension revision mid-render). Buyers encountering divergent digests for the same URI MUST fail closed: treat all references to that URI as unresolved and surface a validation error rather than picking one branch silently. **Across responses**, digest divergence is normal — extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.")).describe("Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\n\n**Collision precedence (normative).** When two or more `platform_extensions[]` entries on the same declaration extend the same target (e.g., both extend `tracking`) with overlapping field names, **array order is authoritative — later entries override earlier ones on a per-field basis** (last-in-array-wins). SDKs MUST surface the overlap via the `errors[]` array on the `get_products` response with a structured code (`FORMAT_DECLARATION_DIVERGENT` is appropriate when the overlap appears across dual-emitted shapes; a producer-self-emitted overlap on a single declaration SHOULD use the same code with `error.details: { collision_kind: \"platform_extension_field\", target, overlapping_fields, winning_extension_uri }`). Producers SHOULD avoid the collision by emitting one extension per target or by partitioning fields across extensions; the deterministic precedence is for last-resort consistency across SDK implementations, not a sanctioned merging strategy.").optional(), "synthesis_nondeterministic": z.boolean().describe("When true, the format's production pipeline is genuinely nondeterministic — the platform cannot guarantee that synthesis from a given input set produces in-spec output. Veo / Sora / Runway-class generative video, and other AI-synthesis flows where output dimensions, duration, or quality vary per run. Implies a different validation contract: predictive `validate_input` is impossible; the platform's own post-synthesis QA loop applies; if the QA loop exhausts without producing a valid artifact, `build_creative` returns task_failed with a synthesis_failed reason. Distinct from `composition_model` (which describes how the surface composes per-slot rendering, not whether synthesis is deterministic). When false or absent, the format's production is predictable enough that `validate_input` can predict output properties from input properties.\n\n**Compatibility with `asset_source` / `item_production_model`**: `synthesis_nondeterministic: true` MAY pair with any of `seller_pre_rendered_from_brief`, `seller_human_designed`, or `agent_synthesized` (the QA loop is concept-level, not source-specific — 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.").default(false), "slots": z.array(z.object({ "asset_group_id": z.string().describe("Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings."), "asset_type": z.enum(["image","video","audio","text","markdown","url","html","css","javascript","vast","daast","webhook","brief","catalog","published_post","zip","card","object","pixel_tracker","vast_tracker","daast_tracker"]).describe("Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `published_post` is an existing-post reference asset, not uploaded media bytes and not a catalog row. `card` is the multi-card carousel element type (see card-asset.json). `pixel_tracker` / `vast_tracker` / `daast_tracker` are the renderer-fired measurement-tracker primitives — see `/schemas/core/assets/pixel-tracker-asset.json` and the VAST / DAAST tracker schemas. `object` is a last-resort fallback for structured non-asset inputs that don't fit any primitive asset_type — prefer specific types whenever possible."), "required": z.boolean().describe("Whether this slot is required for a valid manifest.").default(false), "min": z.number().int().gte(0).describe("Minimum count for repeatable / pool slots.").optional(), "max": z.number().int().gte(1).describe("Maximum count for repeatable / pool slots.").optional(), "max_chars": z.number().int().gte(1).describe("Per-slot character limit. Valid only when `asset_type` is `text`, `markdown`, or `brief`. Mutually exclusive with `max_size_kb` (which applies to binary asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "max_size_kb": z.number().int().gte(1).describe("Per-slot file size limit in kilobytes. Valid only when `asset_type` is `image`, `video`, `audio`, or `zip`. Mutually exclusive with `max_chars` (which applies to text asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Accepted intrinsic-pixel densities for this image-bearing slot. Valid when `asset_type` is `image`, and on a `card` slot where it constrains each card's image media (video media is unaffected). This makes density available to every canonical carrying image assets (native, carousel, responsive, companion images, and image itself), not only `format_kind: image`. When the image canonical also declares top-level `params.pixel_ratios`, the effective set is the intersection; an empty intersection is invalid. One matching asset satisfies the slot unless `required_pixel_ratios` requires rendition coverage.").optional(), "required_pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Required density coverage for an image rendition set. Valid only when `asset_type` is `image` and `pixel_ratios` is also declared. Every value MUST appear in the effective accepted set after intersecting any top-level image `params.pixel_ratios`, and the manifest slot value MUST be an array containing exactly one matching image rendition for each required ratio. Other accepted ratios remain optional. For example, `pixel_ratios: [1, 1.5, 2]` with `required_pixel_ratios: [1, 2]` requires the 1x and 2x renditions while making 1.5x optional. SDKs enforce intersection, subset, coverage, and duplicate-ratio rules because JSON Schema draft-07 cannot express them generically.").optional(), "logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("When `asset_group_id` is `logo`, renderer-facing brand.json logo slots acceptable for this format slot. Producers selecting from brand.json SHOULD prefer `logos[]` entries whose `slots[]` intersects this list, then apply `visual_guidelines.logo_usage_rules[]`.").optional(), "required_logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Subset of `logo_slots` for which this format expects explicit logo coverage. A manifest or brand-derived logo pool SHOULD include at least one usable logo for each required slot; if coverage is missing, builders SHOULD surface a validation warning or approval mapping instead of guessing from prose.").optional(), "description": z.string().describe("Human-readable description of what the slot expects from the buyer.").optional(), "consumed_for_production": z.boolean().describe("Dispatch hint for `build_creative` and v1↔v2 wire translators: when `true`, the slot's value is consumed as INPUT to a production step (host-read script, brief copy fed to generative synthesis, catalog feed driving per-SKU rendering) and is not rendered verbatim. When `false` (default), the slot's value is rendered verbatim on the placement (image bytes, video file, display tag).\n\nMotivates the v1↔v2 dispatch table: pre-v2 buyers shipped production-consumed inputs separately in a `inputs` map on the build_creative request; v2 collapses inputs and rendered assets into a single `assets` map keyed by `asset_group_id`. SDK translators between v1 and v2 use this flag per canonical to know which assets in the v2 manifest map back to v1 `inputs` vs v1 `assets`. Without the per-slot flag the dispatch table lives in adopter code and every SDK gets it slightly different.\n\nProducers SHOULD set this explicitly on slots whose consumption pattern isn't obvious (host-read scripts on `audio_hosted`, briefs on generative `video_hosted`, catalog feeds on `sponsored_placement`). For canonicals where every slot is render-verbatim (`image`, `display_tag`, `video_vast`), the default `false` is sufficient and the flag MAY be omitted.").default(false) }).catchall(z.any()).and(z.intersection(z.intersection(z.any(), z.intersection(z.any(), z.any())), z.intersection(z.any(), z.intersection(z.any(), z.any()))))).describe("Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.").optional(), "required_connections": z.array(z.object({ "provider": z.string().describe("Stable provider or platform namespace, preferably lowercase. Examples: `social.example`, `shortvideo.example`, or a seller-defined namespace. Omit only when the requirement is provider-agnostic, or when an `authorization_url` fully routes the human to the correct provider-specific connection flow.").optional(), "connection_type": z.enum(["advertiser_account","publisher_identity","post_authorization"]).describe("Kind of downstream connection required. `advertiser_account` is the platform account used to buy/manage ads. `publisher_identity` is the creator, page, channel, organization, or profile that owns source posts. `post_authorization` is a post-scoped grant when the platform authorizes individual posts instead of, or in addition to, the owning identity."), "required_for": z.array(z.string().min(1)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Concrete AdCP protocol operation names that require this downstream connection. Sellers SHOULD include this in product declarations when the requirement is known ahead of time, and in AUTHORIZATION_REQUIRED details when it explains the failed operation. Prefer specific operation names such as `list_creatives`, `sync_creatives`, `create_media_buy`, `get_media_buy_delivery`, or `get_creative_delivery` over broad category labels such as `reporting`.").optional(), "scope": z.enum(["account","identity","post","unknown"]).describe("Granularity of the downstream grant.").optional(), "status": z.enum(["connected","missing","pending","expired","revoked","not_required","unknown"]).describe("Current seller-observed state for this downstream connection when known. Product declarations MAY omit status or use `unknown`; AUTHORIZATION_REQUIRED details SHOULD use `missing`, `expired`, or `revoked` for the connection that blocked the call.").optional(), "connection_id": z.string().describe("Seller-defined identifier for an already-created downstream connection. Omit when no connection exists yet or when exposing it would leak platform/account state.").optional(), "resource_ref": z.object({ "platform_account_id": z.string().describe("Provider-native advertiser or business account id, when safe to disclose.").optional(), "identity_id": z.string().describe("Provider-native creator, page, channel, organization, or profile id, when safe to disclose.").optional(), "handle": z.string().describe("Provider-native public handle for the owning identity, when available.").optional(), "profile_url": z.string().url().describe("Public URL for the owning identity, when available.").optional(), "post_id": z.string().describe("Provider-native post id, when the grant is post-scoped or the failed request referenced a specific post.").optional(), "post_url": z.string().url().describe("Public URL for the referenced post, when available.").optional() }).catchall(z.any()).describe("Optional opaque provider-native resource hint, such as a platform account id, profile URL, handle, channel id, post id, or post URL. This is a hint for routing authorization, not proof that authorization exists.").optional(), "authorization_url": z.string().url().describe("Seller-hosted or provider-hosted URL where a human can complete or restore this downstream connection.").optional(), "authorization_instructions": z.string().describe("Human-readable instructions for completing or restoring this downstream connection.").optional(), "expires_at": z.string().datetime().describe("Expiration time for the downstream grant, when known.").optional() }).catchall(z.any()).and(z.any()).describe("A seller/platform-side connection or grant required by a product, format, or request. This is not the AdCP caller credential: the AdCP request is still authenticated once, and the seller uses these stored downstream connections to call a platform or service on the buyer's behalf. Use this shape for platforms that require more than one downstream grant, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.")).describe("Downstream platform connections or grants required to use this format declaration. These are in addition to the single AdCP caller credential. Use this when a platform product requires multiple downstream grants, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.").optional(), "reference_mutability": z.enum(["immutable_snapshot","mutable_requires_reapproval","mutable_auto_recheck"]).describe("Policy for formats whose `slots` accept a `published_post` reference. `immutable_snapshot`: seller snapshots the referenced post at approval and later source changes do not change the served creative. `mutable_requires_reapproval`: the source post may change and material changes require review before continued serving. `mutable_auto_recheck`: the source post may change and the seller continuously or periodically rechecks authorization/policy without requiring buyer resubmission. Omit when the format has no `published_post` slot.").optional(), "production_window_business_days": z.number().int().gte(0).describe("Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative).").optional() }).catchall(z.any()).describe("Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).").describe("Multi-card swipeable carousel. The buyer ships a `cards` slot whose value is an **array** of [card-asset](/schemas/core/assets/card-asset.json) objects (a single key with an array value — NOT one key per card, NOT dotted/bracketed paths). Each card-asset carries: `asset_type: \"card\"`, `media` (an image or video asset), optional `headline` (text), optional `landing_page_url` (url asset). Per-card structure is the same across all cards; mixed orientations not allowed within a single carousel. Tracking model: per-card impression and engagement pixels + carousel-level engagement (swipe, view-time). Allowed asset types for a card's `media` field: `image` and `video` (Meta-style mixed-media); platforms can narrow to image-only or video-only via `allowed_card_media_asset_types`.\n\nThe manifest's `assets.cards` value is an array of card-asset objects. Example: `\"cards\": [{\"asset_type\": \"card\", \"media\": {\"asset_type\": \"image\", \"url\": \"...\"}, \"headline\": \"Buy now\", \"landing_page_url\": {\"asset_type\": \"url\", \"url_type\": \"clickthrough\", \"url\": \"...\"}}, ...]`. Each card-asset validates against the card schema; per-card platform extensions attach via the card's `platform_extensions` field, never via inline non-canonical keys.") }), z.object({ "format_kind": z.literal("video_hosted"), "params": z.intersection(z.object({ "experimental": z.boolean().describe("When true, this canonical or seller narrowing may not work as declared. Adopters SHOULD preflight it with validate_input or in a sandbox and SHOULD NOT route production budget without testing; experimental status never makes the deprecated v1 path preferable. Drivers include unsettled spec shape, an adopter runtime gap, and custom shapes awaiting promotion. This replaces the earlier status plus runtime_status axes. Sellers SHOULD set experimental whenever a canonical or declaration is not production-ready.").default(false), "deprecated": z.boolean().describe("When true, this canonical (or a seller's specific narrowing of it) is going away. Existing adopters are supported through the deprecation cycle; new adoption is discouraged. Pair with `migration_target_version` to indicate when the canonical is expected to be removed. Distinct from `experimental`: an experimental canonical may stabilize and stop being experimental; a deprecated canonical is on a sunset path.").default(false), "v1_translatable": z.boolean().describe("Whether this canonical has any v1 named-format equivalent. `true` (default) — the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1→v2 projection via `v1-canonical-mapping.json` is meaningful. `false` — the canonical is inherently new in v2 and has no v1 form; v1's `list_creative_formats` couldn't express it because the underlying concept (algorithmic surface composition, AI-surface mentions, retail-media catalog placements, multi-card carousels) didn't exist as a v1 named-format archetype.\n\nLets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The six inherently-v2 canonicals in 3.2 are `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`, `seller_rendered_stateful_display`, and `coordinated_placements`.").default(true), "since_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version that introduced this canonical (e.g., '3.1', '3.2'). Lets adopters reason about minimum protocol version requirements when consuming a format declaration. Patch precision is intentionally rejected — canonicals are introduced at minor-version boundaries.").optional(), "migration_target_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version by which the working group expects this canonical to stabilize, surface a breaking revision, or (when `deprecated: true`) be removed. Patch precision is intentionally rejected — canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown').").optional(), "composition_model": z.enum(["deterministic","algorithmic"]).describe("Whether the surface composes deterministically (buyer can predict per-slot rendering — sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing — responsive_creative, agent_placement).").optional(), "provenance_required": z.boolean().describe("When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent.").optional(), "platform_extensions": z.array(z.object({ "uri": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL identifying the extension. `https://` is mandatory — `http://`, `file://`, `data:`, and other schemes are rejected at the schema layer (defense-in-depth on top of the fetch-contract normative rules). The URI base is the owning agent's URL; the path identifies the extension within that agent. Example: 'https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel'. The full fetch contract — SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds — is documented on `product-format-declaration.json#format_schema` and applies to ALL fetches of this reference shape regardless of whether the field is named `format_schema` (load-bearing for validation) or `platform_extensions` (informational); the *transport* rules are identical, only the *consumption* semantics differ."), "digest": z.string().regex(new RegExp("^sha256:[a-f0-9]{64}$")).describe("SHA-256 content digest of the extension definition (sha256:). Used to detect drift — if the agent revises the extension, the digest changes and cached definitions become invalid.") }).catchall(z.any()).describe("Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` — divergent digests in one response indicate producer-side error (e.g., concurrent extension revision mid-render). Buyers encountering divergent digests for the same URI MUST fail closed: treat all references to that URI as unresolved and surface a validation error rather than picking one branch silently. **Across responses**, digest divergence is normal — extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.")).describe("Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\n\n**Collision precedence (normative).** When two or more `platform_extensions[]` entries on the same declaration extend the same target (e.g., both extend `tracking`) with overlapping field names, **array order is authoritative — later entries override earlier ones on a per-field basis** (last-in-array-wins). SDKs MUST surface the overlap via the `errors[]` array on the `get_products` response with a structured code (`FORMAT_DECLARATION_DIVERGENT` is appropriate when the overlap appears across dual-emitted shapes; a producer-self-emitted overlap on a single declaration SHOULD use the same code with `error.details: { collision_kind: \"platform_extension_field\", target, overlapping_fields, winning_extension_uri }`). Producers SHOULD avoid the collision by emitting one extension per target or by partitioning fields across extensions; the deterministic precedence is for last-resort consistency across SDK implementations, not a sanctioned merging strategy.").optional(), "synthesis_nondeterministic": z.boolean().describe("When true, the format's production pipeline is genuinely nondeterministic — the platform cannot guarantee that synthesis from a given input set produces in-spec output. Veo / Sora / Runway-class generative video, and other AI-synthesis flows where output dimensions, duration, or quality vary per run. Implies a different validation contract: predictive `validate_input` is impossible; the platform's own post-synthesis QA loop applies; if the QA loop exhausts without producing a valid artifact, `build_creative` returns task_failed with a synthesis_failed reason. Distinct from `composition_model` (which describes how the surface composes per-slot rendering, not whether synthesis is deterministic). When false or absent, the format's production is predictable enough that `validate_input` can predict output properties from input properties.\n\n**Compatibility with `asset_source` / `item_production_model`**: `synthesis_nondeterministic: true` MAY pair with any of `seller_pre_rendered_from_brief`, `seller_human_designed`, or `agent_synthesized` (the QA loop is concept-level, not source-specific — 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.").default(false), "slots": z.array(z.object({ "asset_group_id": z.string().describe("Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings."), "asset_type": z.enum(["image","video","audio","text","markdown","url","html","css","javascript","vast","daast","webhook","brief","catalog","published_post","zip","card","object","pixel_tracker","vast_tracker","daast_tracker"]).describe("Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `published_post` is an existing-post reference asset, not uploaded media bytes and not a catalog row. `card` is the multi-card carousel element type (see card-asset.json). `pixel_tracker` / `vast_tracker` / `daast_tracker` are the renderer-fired measurement-tracker primitives — see `/schemas/core/assets/pixel-tracker-asset.json` and the VAST / DAAST tracker schemas. `object` is a last-resort fallback for structured non-asset inputs that don't fit any primitive asset_type — prefer specific types whenever possible."), "required": z.boolean().describe("Whether this slot is required for a valid manifest.").default(false), "min": z.number().int().gte(0).describe("Minimum count for repeatable / pool slots.").optional(), "max": z.number().int().gte(1).describe("Maximum count for repeatable / pool slots.").optional(), "max_chars": z.number().int().gte(1).describe("Per-slot character limit. Valid only when `asset_type` is `text`, `markdown`, or `brief`. Mutually exclusive with `max_size_kb` (which applies to binary asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "max_size_kb": z.number().int().gte(1).describe("Per-slot file size limit in kilobytes. Valid only when `asset_type` is `image`, `video`, `audio`, or `zip`. Mutually exclusive with `max_chars` (which applies to text asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Accepted intrinsic-pixel densities for this image-bearing slot. Valid when `asset_type` is `image`, and on a `card` slot where it constrains each card's image media (video media is unaffected). This makes density available to every canonical carrying image assets (native, carousel, responsive, companion images, and image itself), not only `format_kind: image`. When the image canonical also declares top-level `params.pixel_ratios`, the effective set is the intersection; an empty intersection is invalid. One matching asset satisfies the slot unless `required_pixel_ratios` requires rendition coverage.").optional(), "required_pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Required density coverage for an image rendition set. Valid only when `asset_type` is `image` and `pixel_ratios` is also declared. Every value MUST appear in the effective accepted set after intersecting any top-level image `params.pixel_ratios`, and the manifest slot value MUST be an array containing exactly one matching image rendition for each required ratio. Other accepted ratios remain optional. For example, `pixel_ratios: [1, 1.5, 2]` with `required_pixel_ratios: [1, 2]` requires the 1x and 2x renditions while making 1.5x optional. SDKs enforce intersection, subset, coverage, and duplicate-ratio rules because JSON Schema draft-07 cannot express them generically.").optional(), "logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("When `asset_group_id` is `logo`, renderer-facing brand.json logo slots acceptable for this format slot. Producers selecting from brand.json SHOULD prefer `logos[]` entries whose `slots[]` intersects this list, then apply `visual_guidelines.logo_usage_rules[]`.").optional(), "required_logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Subset of `logo_slots` for which this format expects explicit logo coverage. A manifest or brand-derived logo pool SHOULD include at least one usable logo for each required slot; if coverage is missing, builders SHOULD surface a validation warning or approval mapping instead of guessing from prose.").optional(), "description": z.string().describe("Human-readable description of what the slot expects from the buyer.").optional(), "consumed_for_production": z.boolean().describe("Dispatch hint for `build_creative` and v1↔v2 wire translators: when `true`, the slot's value is consumed as INPUT to a production step (host-read script, brief copy fed to generative synthesis, catalog feed driving per-SKU rendering) and is not rendered verbatim. When `false` (default), the slot's value is rendered verbatim on the placement (image bytes, video file, display tag).\n\nMotivates the v1↔v2 dispatch table: pre-v2 buyers shipped production-consumed inputs separately in a `inputs` map on the build_creative request; v2 collapses inputs and rendered assets into a single `assets` map keyed by `asset_group_id`. SDK translators between v1 and v2 use this flag per canonical to know which assets in the v2 manifest map back to v1 `inputs` vs v1 `assets`. Without the per-slot flag the dispatch table lives in adopter code and every SDK gets it slightly different.\n\nProducers SHOULD set this explicitly on slots whose consumption pattern isn't obvious (host-read scripts on `audio_hosted`, briefs on generative `video_hosted`, catalog feeds on `sponsored_placement`). For canonicals where every slot is render-verbatim (`image`, `display_tag`, `video_vast`), the default `false` is sufficient and the flag MAY be omitted.").default(false) }).catchall(z.any()).and(z.intersection(z.intersection(z.any(), z.intersection(z.any(), z.any())), z.intersection(z.any(), z.intersection(z.any(), z.any()))))).describe("Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.").optional(), "required_connections": z.array(z.object({ "provider": z.string().describe("Stable provider or platform namespace, preferably lowercase. Examples: `social.example`, `shortvideo.example`, or a seller-defined namespace. Omit only when the requirement is provider-agnostic, or when an `authorization_url` fully routes the human to the correct provider-specific connection flow.").optional(), "connection_type": z.enum(["advertiser_account","publisher_identity","post_authorization"]).describe("Kind of downstream connection required. `advertiser_account` is the platform account used to buy/manage ads. `publisher_identity` is the creator, page, channel, organization, or profile that owns source posts. `post_authorization` is a post-scoped grant when the platform authorizes individual posts instead of, or in addition to, the owning identity."), "required_for": z.array(z.string().min(1)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Concrete AdCP protocol operation names that require this downstream connection. Sellers SHOULD include this in product declarations when the requirement is known ahead of time, and in AUTHORIZATION_REQUIRED details when it explains the failed operation. Prefer specific operation names such as `list_creatives`, `sync_creatives`, `create_media_buy`, `get_media_buy_delivery`, or `get_creative_delivery` over broad category labels such as `reporting`.").optional(), "scope": z.enum(["account","identity","post","unknown"]).describe("Granularity of the downstream grant.").optional(), "status": z.enum(["connected","missing","pending","expired","revoked","not_required","unknown"]).describe("Current seller-observed state for this downstream connection when known. Product declarations MAY omit status or use `unknown`; AUTHORIZATION_REQUIRED details SHOULD use `missing`, `expired`, or `revoked` for the connection that blocked the call.").optional(), "connection_id": z.string().describe("Seller-defined identifier for an already-created downstream connection. Omit when no connection exists yet or when exposing it would leak platform/account state.").optional(), "resource_ref": z.object({ "platform_account_id": z.string().describe("Provider-native advertiser or business account id, when safe to disclose.").optional(), "identity_id": z.string().describe("Provider-native creator, page, channel, organization, or profile id, when safe to disclose.").optional(), "handle": z.string().describe("Provider-native public handle for the owning identity, when available.").optional(), "profile_url": z.string().url().describe("Public URL for the owning identity, when available.").optional(), "post_id": z.string().describe("Provider-native post id, when the grant is post-scoped or the failed request referenced a specific post.").optional(), "post_url": z.string().url().describe("Public URL for the referenced post, when available.").optional() }).catchall(z.any()).describe("Optional opaque provider-native resource hint, such as a platform account id, profile URL, handle, channel id, post id, or post URL. This is a hint for routing authorization, not proof that authorization exists.").optional(), "authorization_url": z.string().url().describe("Seller-hosted or provider-hosted URL where a human can complete or restore this downstream connection.").optional(), "authorization_instructions": z.string().describe("Human-readable instructions for completing or restoring this downstream connection.").optional(), "expires_at": z.string().datetime().describe("Expiration time for the downstream grant, when known.").optional() }).catchall(z.any()).and(z.any()).describe("A seller/platform-side connection or grant required by a product, format, or request. This is not the AdCP caller credential: the AdCP request is still authenticated once, and the seller uses these stored downstream connections to call a platform or service on the buyer's behalf. Use this shape for platforms that require more than one downstream grant, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.")).describe("Downstream platform connections or grants required to use this format declaration. These are in addition to the single AdCP caller credential. Use this when a platform product requires multiple downstream grants, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.").optional(), "reference_mutability": z.enum(["immutable_snapshot","mutable_requires_reapproval","mutable_auto_recheck"]).describe("Policy for formats whose `slots` accept a `published_post` reference. `immutable_snapshot`: seller snapshots the referenced post at approval and later source changes do not change the served creative. `mutable_requires_reapproval`: the source post may change and material changes require review before continued serving. `mutable_auto_recheck`: the source post may change and the seller continuously or periodically rechecks authorization/policy without requiring buyer resubmission. Omit when the format has no `published_post` slot.").optional(), "production_window_business_days": z.number().int().gte(0).describe("Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative).").optional() }).catchall(z.any()).describe("Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints)."), z.any()).describe("Direct video file (mp4/webm/mov) hosted by the buyer. Slot: `video_main` (video asset, file or hosted URL), optional `headline`, `brand_name`, `cta`, `companion_banner`, `landing_page_url`. Tracking model: IAB Open Measurement SDK + external impression/click/quartile pixels via universal_macros. Orientation is a parameter (vertical 9:16 / horizontal 16:9 / square 1:1); slot shape includes optional `brand_name` (typical for vertical short-form) and optional `companion_banner` (typical for horizontal instream). Distinct from `video_vast` (VAST tag, inherent VAST event tracking) — receivers fire impression and click pixels at delivery time.") }), z.object({ "format_kind": z.literal("video_vast"), "params": z.intersection(z.object({ "experimental": z.boolean().describe("When true, this canonical or seller narrowing may not work as declared. Adopters SHOULD preflight it with validate_input or in a sandbox and SHOULD NOT route production budget without testing; experimental status never makes the deprecated v1 path preferable. Drivers include unsettled spec shape, an adopter runtime gap, and custom shapes awaiting promotion. This replaces the earlier status plus runtime_status axes. Sellers SHOULD set experimental whenever a canonical or declaration is not production-ready.").default(false), "deprecated": z.boolean().describe("When true, this canonical (or a seller's specific narrowing of it) is going away. Existing adopters are supported through the deprecation cycle; new adoption is discouraged. Pair with `migration_target_version` to indicate when the canonical is expected to be removed. Distinct from `experimental`: an experimental canonical may stabilize and stop being experimental; a deprecated canonical is on a sunset path.").default(false), "v1_translatable": z.boolean().describe("Whether this canonical has any v1 named-format equivalent. `true` (default) — the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1→v2 projection via `v1-canonical-mapping.json` is meaningful. `false` — the canonical is inherently new in v2 and has no v1 form; v1's `list_creative_formats` couldn't express it because the underlying concept (algorithmic surface composition, AI-surface mentions, retail-media catalog placements, multi-card carousels) didn't exist as a v1 named-format archetype.\n\nLets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The six inherently-v2 canonicals in 3.2 are `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`, `seller_rendered_stateful_display`, and `coordinated_placements`.").default(true), "since_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version that introduced this canonical (e.g., '3.1', '3.2'). Lets adopters reason about minimum protocol version requirements when consuming a format declaration. Patch precision is intentionally rejected — canonicals are introduced at minor-version boundaries.").optional(), "migration_target_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version by which the working group expects this canonical to stabilize, surface a breaking revision, or (when `deprecated: true`) be removed. Patch precision is intentionally rejected — canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown').").optional(), "composition_model": z.enum(["deterministic","algorithmic"]).describe("Whether the surface composes deterministically (buyer can predict per-slot rendering — sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing — responsive_creative, agent_placement).").optional(), "provenance_required": z.boolean().describe("When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent.").optional(), "platform_extensions": z.array(z.object({ "uri": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL identifying the extension. `https://` is mandatory — `http://`, `file://`, `data:`, and other schemes are rejected at the schema layer (defense-in-depth on top of the fetch-contract normative rules). The URI base is the owning agent's URL; the path identifies the extension within that agent. Example: 'https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel'. The full fetch contract — SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds — is documented on `product-format-declaration.json#format_schema` and applies to ALL fetches of this reference shape regardless of whether the field is named `format_schema` (load-bearing for validation) or `platform_extensions` (informational); the *transport* rules are identical, only the *consumption* semantics differ."), "digest": z.string().regex(new RegExp("^sha256:[a-f0-9]{64}$")).describe("SHA-256 content digest of the extension definition (sha256:). Used to detect drift — if the agent revises the extension, the digest changes and cached definitions become invalid.") }).catchall(z.any()).describe("Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` — divergent digests in one response indicate producer-side error (e.g., concurrent extension revision mid-render). Buyers encountering divergent digests for the same URI MUST fail closed: treat all references to that URI as unresolved and surface a validation error rather than picking one branch silently. **Across responses**, digest divergence is normal — extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.")).describe("Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\n\n**Collision precedence (normative).** When two or more `platform_extensions[]` entries on the same declaration extend the same target (e.g., both extend `tracking`) with overlapping field names, **array order is authoritative — later entries override earlier ones on a per-field basis** (last-in-array-wins). SDKs MUST surface the overlap via the `errors[]` array on the `get_products` response with a structured code (`FORMAT_DECLARATION_DIVERGENT` is appropriate when the overlap appears across dual-emitted shapes; a producer-self-emitted overlap on a single declaration SHOULD use the same code with `error.details: { collision_kind: \"platform_extension_field\", target, overlapping_fields, winning_extension_uri }`). Producers SHOULD avoid the collision by emitting one extension per target or by partitioning fields across extensions; the deterministic precedence is for last-resort consistency across SDK implementations, not a sanctioned merging strategy.").optional(), "synthesis_nondeterministic": z.boolean().describe("When true, the format's production pipeline is genuinely nondeterministic — the platform cannot guarantee that synthesis from a given input set produces in-spec output. Veo / Sora / Runway-class generative video, and other AI-synthesis flows where output dimensions, duration, or quality vary per run. Implies a different validation contract: predictive `validate_input` is impossible; the platform's own post-synthesis QA loop applies; if the QA loop exhausts without producing a valid artifact, `build_creative` returns task_failed with a synthesis_failed reason. Distinct from `composition_model` (which describes how the surface composes per-slot rendering, not whether synthesis is deterministic). When false or absent, the format's production is predictable enough that `validate_input` can predict output properties from input properties.\n\n**Compatibility with `asset_source` / `item_production_model`**: `synthesis_nondeterministic: true` MAY pair with any of `seller_pre_rendered_from_brief`, `seller_human_designed`, or `agent_synthesized` (the QA loop is concept-level, not source-specific — 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.").default(false), "slots": z.array(z.object({ "asset_group_id": z.string().describe("Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings."), "asset_type": z.enum(["image","video","audio","text","markdown","url","html","css","javascript","vast","daast","webhook","brief","catalog","published_post","zip","card","object","pixel_tracker","vast_tracker","daast_tracker"]).describe("Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `published_post` is an existing-post reference asset, not uploaded media bytes and not a catalog row. `card` is the multi-card carousel element type (see card-asset.json). `pixel_tracker` / `vast_tracker` / `daast_tracker` are the renderer-fired measurement-tracker primitives — see `/schemas/core/assets/pixel-tracker-asset.json` and the VAST / DAAST tracker schemas. `object` is a last-resort fallback for structured non-asset inputs that don't fit any primitive asset_type — prefer specific types whenever possible."), "required": z.boolean().describe("Whether this slot is required for a valid manifest.").default(false), "min": z.number().int().gte(0).describe("Minimum count for repeatable / pool slots.").optional(), "max": z.number().int().gte(1).describe("Maximum count for repeatable / pool slots.").optional(), "max_chars": z.number().int().gte(1).describe("Per-slot character limit. Valid only when `asset_type` is `text`, `markdown`, or `brief`. Mutually exclusive with `max_size_kb` (which applies to binary asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "max_size_kb": z.number().int().gte(1).describe("Per-slot file size limit in kilobytes. Valid only when `asset_type` is `image`, `video`, `audio`, or `zip`. Mutually exclusive with `max_chars` (which applies to text asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Accepted intrinsic-pixel densities for this image-bearing slot. Valid when `asset_type` is `image`, and on a `card` slot where it constrains each card's image media (video media is unaffected). This makes density available to every canonical carrying image assets (native, carousel, responsive, companion images, and image itself), not only `format_kind: image`. When the image canonical also declares top-level `params.pixel_ratios`, the effective set is the intersection; an empty intersection is invalid. One matching asset satisfies the slot unless `required_pixel_ratios` requires rendition coverage.").optional(), "required_pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Required density coverage for an image rendition set. Valid only when `asset_type` is `image` and `pixel_ratios` is also declared. Every value MUST appear in the effective accepted set after intersecting any top-level image `params.pixel_ratios`, and the manifest slot value MUST be an array containing exactly one matching image rendition for each required ratio. Other accepted ratios remain optional. For example, `pixel_ratios: [1, 1.5, 2]` with `required_pixel_ratios: [1, 2]` requires the 1x and 2x renditions while making 1.5x optional. SDKs enforce intersection, subset, coverage, and duplicate-ratio rules because JSON Schema draft-07 cannot express them generically.").optional(), "logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("When `asset_group_id` is `logo`, renderer-facing brand.json logo slots acceptable for this format slot. Producers selecting from brand.json SHOULD prefer `logos[]` entries whose `slots[]` intersects this list, then apply `visual_guidelines.logo_usage_rules[]`.").optional(), "required_logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Subset of `logo_slots` for which this format expects explicit logo coverage. A manifest or brand-derived logo pool SHOULD include at least one usable logo for each required slot; if coverage is missing, builders SHOULD surface a validation warning or approval mapping instead of guessing from prose.").optional(), "description": z.string().describe("Human-readable description of what the slot expects from the buyer.").optional(), "consumed_for_production": z.boolean().describe("Dispatch hint for `build_creative` and v1↔v2 wire translators: when `true`, the slot's value is consumed as INPUT to a production step (host-read script, brief copy fed to generative synthesis, catalog feed driving per-SKU rendering) and is not rendered verbatim. When `false` (default), the slot's value is rendered verbatim on the placement (image bytes, video file, display tag).\n\nMotivates the v1↔v2 dispatch table: pre-v2 buyers shipped production-consumed inputs separately in a `inputs` map on the build_creative request; v2 collapses inputs and rendered assets into a single `assets` map keyed by `asset_group_id`. SDK translators between v1 and v2 use this flag per canonical to know which assets in the v2 manifest map back to v1 `inputs` vs v1 `assets`. Without the per-slot flag the dispatch table lives in adopter code and every SDK gets it slightly different.\n\nProducers SHOULD set this explicitly on slots whose consumption pattern isn't obvious (host-read scripts on `audio_hosted`, briefs on generative `video_hosted`, catalog feeds on `sponsored_placement`). For canonicals where every slot is render-verbatim (`image`, `display_tag`, `video_vast`), the default `false` is sufficient and the flag MAY be omitted.").default(false) }).catchall(z.any()).and(z.intersection(z.intersection(z.any(), z.intersection(z.any(), z.any())), z.intersection(z.any(), z.intersection(z.any(), z.any()))))).describe("Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.").optional(), "required_connections": z.array(z.object({ "provider": z.string().describe("Stable provider or platform namespace, preferably lowercase. Examples: `social.example`, `shortvideo.example`, or a seller-defined namespace. Omit only when the requirement is provider-agnostic, or when an `authorization_url` fully routes the human to the correct provider-specific connection flow.").optional(), "connection_type": z.enum(["advertiser_account","publisher_identity","post_authorization"]).describe("Kind of downstream connection required. `advertiser_account` is the platform account used to buy/manage ads. `publisher_identity` is the creator, page, channel, organization, or profile that owns source posts. `post_authorization` is a post-scoped grant when the platform authorizes individual posts instead of, or in addition to, the owning identity."), "required_for": z.array(z.string().min(1)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Concrete AdCP protocol operation names that require this downstream connection. Sellers SHOULD include this in product declarations when the requirement is known ahead of time, and in AUTHORIZATION_REQUIRED details when it explains the failed operation. Prefer specific operation names such as `list_creatives`, `sync_creatives`, `create_media_buy`, `get_media_buy_delivery`, or `get_creative_delivery` over broad category labels such as `reporting`.").optional(), "scope": z.enum(["account","identity","post","unknown"]).describe("Granularity of the downstream grant.").optional(), "status": z.enum(["connected","missing","pending","expired","revoked","not_required","unknown"]).describe("Current seller-observed state for this downstream connection when known. Product declarations MAY omit status or use `unknown`; AUTHORIZATION_REQUIRED details SHOULD use `missing`, `expired`, or `revoked` for the connection that blocked the call.").optional(), "connection_id": z.string().describe("Seller-defined identifier for an already-created downstream connection. Omit when no connection exists yet or when exposing it would leak platform/account state.").optional(), "resource_ref": z.object({ "platform_account_id": z.string().describe("Provider-native advertiser or business account id, when safe to disclose.").optional(), "identity_id": z.string().describe("Provider-native creator, page, channel, organization, or profile id, when safe to disclose.").optional(), "handle": z.string().describe("Provider-native public handle for the owning identity, when available.").optional(), "profile_url": z.string().url().describe("Public URL for the owning identity, when available.").optional(), "post_id": z.string().describe("Provider-native post id, when the grant is post-scoped or the failed request referenced a specific post.").optional(), "post_url": z.string().url().describe("Public URL for the referenced post, when available.").optional() }).catchall(z.any()).describe("Optional opaque provider-native resource hint, such as a platform account id, profile URL, handle, channel id, post id, or post URL. This is a hint for routing authorization, not proof that authorization exists.").optional(), "authorization_url": z.string().url().describe("Seller-hosted or provider-hosted URL where a human can complete or restore this downstream connection.").optional(), "authorization_instructions": z.string().describe("Human-readable instructions for completing or restoring this downstream connection.").optional(), "expires_at": z.string().datetime().describe("Expiration time for the downstream grant, when known.").optional() }).catchall(z.any()).and(z.any()).describe("A seller/platform-side connection or grant required by a product, format, or request. This is not the AdCP caller credential: the AdCP request is still authenticated once, and the seller uses these stored downstream connections to call a platform or service on the buyer's behalf. Use this shape for platforms that require more than one downstream grant, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.")).describe("Downstream platform connections or grants required to use this format declaration. These are in addition to the single AdCP caller credential. Use this when a platform product requires multiple downstream grants, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.").optional(), "reference_mutability": z.enum(["immutable_snapshot","mutable_requires_reapproval","mutable_auto_recheck"]).describe("Policy for formats whose `slots` accept a `published_post` reference. `immutable_snapshot`: seller snapshots the referenced post at approval and later source changes do not change the served creative. `mutable_requires_reapproval`: the source post may change and material changes require review before continued serving. `mutable_auto_recheck`: the source post may change and the seller continuously or periodically rechecks authorization/policy without requiring buyer resubmission. Omit when the format has no `published_post` slot.").optional(), "production_window_business_days": z.number().int().gte(0).describe("Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative).").optional() }).catchall(z.any()).describe("Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints)."), z.intersection(z.any(), z.any())).describe("VAST-tag-delivered video creative. Slot: `vast_tag` (vast asset, URL or inline XML, VAST 2.x-4.x). Tracking model: VAST events inherent to the spec — `impression`, `firstQuartile`, `midpoint`, `thirdQuartile`, `complete`, `start`, `pause`, `resume`, `mute`, `unmute`, `expand`, `collapse`, `fullscreen`, `creativeView`, `clickTracking`, `error`. VPAID interactivity via `vpaid_enabled: true` flag. SIMID is carried by the first-class VAST 4.1+ `` element on Linear creatives. Orientation is a parameter (vertical / horizontal / square). Distinct from `video_hosted` (direct file with external tracking).") }), z.object({ "format_kind": z.literal("audio_hosted"), "params": z.object({ "experimental": z.boolean().describe("When true, this canonical or seller narrowing may not work as declared. Adopters SHOULD preflight it with validate_input or in a sandbox and SHOULD NOT route production budget without testing; experimental status never makes the deprecated v1 path preferable. Drivers include unsettled spec shape, an adopter runtime gap, and custom shapes awaiting promotion. This replaces the earlier status plus runtime_status axes. Sellers SHOULD set experimental whenever a canonical or declaration is not production-ready.").default(false), "deprecated": z.boolean().describe("When true, this canonical (or a seller's specific narrowing of it) is going away. Existing adopters are supported through the deprecation cycle; new adoption is discouraged. Pair with `migration_target_version` to indicate when the canonical is expected to be removed. Distinct from `experimental`: an experimental canonical may stabilize and stop being experimental; a deprecated canonical is on a sunset path.").default(false), "v1_translatable": z.boolean().describe("Whether this canonical has any v1 named-format equivalent. `true` (default) — the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1→v2 projection via `v1-canonical-mapping.json` is meaningful. `false` — the canonical is inherently new in v2 and has no v1 form; v1's `list_creative_formats` couldn't express it because the underlying concept (algorithmic surface composition, AI-surface mentions, retail-media catalog placements, multi-card carousels) didn't exist as a v1 named-format archetype.\n\nLets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The six inherently-v2 canonicals in 3.2 are `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`, `seller_rendered_stateful_display`, and `coordinated_placements`.").default(true), "since_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version that introduced this canonical (e.g., '3.1', '3.2'). Lets adopters reason about minimum protocol version requirements when consuming a format declaration. Patch precision is intentionally rejected — canonicals are introduced at minor-version boundaries.").optional(), "migration_target_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version by which the working group expects this canonical to stabilize, surface a breaking revision, or (when `deprecated: true`) be removed. Patch precision is intentionally rejected — canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown').").optional(), "composition_model": z.enum(["deterministic","algorithmic"]).describe("Whether the surface composes deterministically (buyer can predict per-slot rendering — sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing — responsive_creative, agent_placement).").optional(), "provenance_required": z.boolean().describe("When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent.").optional(), "platform_extensions": z.array(z.object({ "uri": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL identifying the extension. `https://` is mandatory — `http://`, `file://`, `data:`, and other schemes are rejected at the schema layer (defense-in-depth on top of the fetch-contract normative rules). The URI base is the owning agent's URL; the path identifies the extension within that agent. Example: 'https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel'. The full fetch contract — SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds — is documented on `product-format-declaration.json#format_schema` and applies to ALL fetches of this reference shape regardless of whether the field is named `format_schema` (load-bearing for validation) or `platform_extensions` (informational); the *transport* rules are identical, only the *consumption* semantics differ."), "digest": z.string().regex(new RegExp("^sha256:[a-f0-9]{64}$")).describe("SHA-256 content digest of the extension definition (sha256:). Used to detect drift — if the agent revises the extension, the digest changes and cached definitions become invalid.") }).catchall(z.any()).describe("Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` — divergent digests in one response indicate producer-side error (e.g., concurrent extension revision mid-render). Buyers encountering divergent digests for the same URI MUST fail closed: treat all references to that URI as unresolved and surface a validation error rather than picking one branch silently. **Across responses**, digest divergence is normal — extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.")).describe("Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\n\n**Collision precedence (normative).** When two or more `platform_extensions[]` entries on the same declaration extend the same target (e.g., both extend `tracking`) with overlapping field names, **array order is authoritative — later entries override earlier ones on a per-field basis** (last-in-array-wins). SDKs MUST surface the overlap via the `errors[]` array on the `get_products` response with a structured code (`FORMAT_DECLARATION_DIVERGENT` is appropriate when the overlap appears across dual-emitted shapes; a producer-self-emitted overlap on a single declaration SHOULD use the same code with `error.details: { collision_kind: \"platform_extension_field\", target, overlapping_fields, winning_extension_uri }`). Producers SHOULD avoid the collision by emitting one extension per target or by partitioning fields across extensions; the deterministic precedence is for last-resort consistency across SDK implementations, not a sanctioned merging strategy.").optional(), "synthesis_nondeterministic": z.boolean().describe("When true, the format's production pipeline is genuinely nondeterministic — the platform cannot guarantee that synthesis from a given input set produces in-spec output. Veo / Sora / Runway-class generative video, and other AI-synthesis flows where output dimensions, duration, or quality vary per run. Implies a different validation contract: predictive `validate_input` is impossible; the platform's own post-synthesis QA loop applies; if the QA loop exhausts without producing a valid artifact, `build_creative` returns task_failed with a synthesis_failed reason. Distinct from `composition_model` (which describes how the surface composes per-slot rendering, not whether synthesis is deterministic). When false or absent, the format's production is predictable enough that `validate_input` can predict output properties from input properties.\n\n**Compatibility with `asset_source` / `item_production_model`**: `synthesis_nondeterministic: true` MAY pair with any of `seller_pre_rendered_from_brief`, `seller_human_designed`, or `agent_synthesized` (the QA loop is concept-level, not source-specific — 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.").default(false), "slots": z.array(z.object({ "asset_group_id": z.string().describe("Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings."), "asset_type": z.enum(["image","video","audio","text","markdown","url","html","css","javascript","vast","daast","webhook","brief","catalog","published_post","zip","card","object","pixel_tracker","vast_tracker","daast_tracker"]).describe("Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `published_post` is an existing-post reference asset, not uploaded media bytes and not a catalog row. `card` is the multi-card carousel element type (see card-asset.json). `pixel_tracker` / `vast_tracker` / `daast_tracker` are the renderer-fired measurement-tracker primitives — see `/schemas/core/assets/pixel-tracker-asset.json` and the VAST / DAAST tracker schemas. `object` is a last-resort fallback for structured non-asset inputs that don't fit any primitive asset_type — prefer specific types whenever possible."), "required": z.boolean().describe("Whether this slot is required for a valid manifest.").default(false), "min": z.number().int().gte(0).describe("Minimum count for repeatable / pool slots.").optional(), "max": z.number().int().gte(1).describe("Maximum count for repeatable / pool slots.").optional(), "max_chars": z.number().int().gte(1).describe("Per-slot character limit. Valid only when `asset_type` is `text`, `markdown`, or `brief`. Mutually exclusive with `max_size_kb` (which applies to binary asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "max_size_kb": z.number().int().gte(1).describe("Per-slot file size limit in kilobytes. Valid only when `asset_type` is `image`, `video`, `audio`, or `zip`. Mutually exclusive with `max_chars` (which applies to text asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Accepted intrinsic-pixel densities for this image-bearing slot. Valid when `asset_type` is `image`, and on a `card` slot where it constrains each card's image media (video media is unaffected). This makes density available to every canonical carrying image assets (native, carousel, responsive, companion images, and image itself), not only `format_kind: image`. When the image canonical also declares top-level `params.pixel_ratios`, the effective set is the intersection; an empty intersection is invalid. One matching asset satisfies the slot unless `required_pixel_ratios` requires rendition coverage.").optional(), "required_pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Required density coverage for an image rendition set. Valid only when `asset_type` is `image` and `pixel_ratios` is also declared. Every value MUST appear in the effective accepted set after intersecting any top-level image `params.pixel_ratios`, and the manifest slot value MUST be an array containing exactly one matching image rendition for each required ratio. Other accepted ratios remain optional. For example, `pixel_ratios: [1, 1.5, 2]` with `required_pixel_ratios: [1, 2]` requires the 1x and 2x renditions while making 1.5x optional. SDKs enforce intersection, subset, coverage, and duplicate-ratio rules because JSON Schema draft-07 cannot express them generically.").optional(), "logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("When `asset_group_id` is `logo`, renderer-facing brand.json logo slots acceptable for this format slot. Producers selecting from brand.json SHOULD prefer `logos[]` entries whose `slots[]` intersects this list, then apply `visual_guidelines.logo_usage_rules[]`.").optional(), "required_logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Subset of `logo_slots` for which this format expects explicit logo coverage. A manifest or brand-derived logo pool SHOULD include at least one usable logo for each required slot; if coverage is missing, builders SHOULD surface a validation warning or approval mapping instead of guessing from prose.").optional(), "description": z.string().describe("Human-readable description of what the slot expects from the buyer.").optional(), "consumed_for_production": z.boolean().describe("Dispatch hint for `build_creative` and v1↔v2 wire translators: when `true`, the slot's value is consumed as INPUT to a production step (host-read script, brief copy fed to generative synthesis, catalog feed driving per-SKU rendering) and is not rendered verbatim. When `false` (default), the slot's value is rendered verbatim on the placement (image bytes, video file, display tag).\n\nMotivates the v1↔v2 dispatch table: pre-v2 buyers shipped production-consumed inputs separately in a `inputs` map on the build_creative request; v2 collapses inputs and rendered assets into a single `assets` map keyed by `asset_group_id`. SDK translators between v1 and v2 use this flag per canonical to know which assets in the v2 manifest map back to v1 `inputs` vs v1 `assets`. Without the per-slot flag the dispatch table lives in adopter code and every SDK gets it slightly different.\n\nProducers SHOULD set this explicitly on slots whose consumption pattern isn't obvious (host-read scripts on `audio_hosted`, briefs on generative `video_hosted`, catalog feeds on `sponsored_placement`). For canonicals where every slot is render-verbatim (`image`, `display_tag`, `video_vast`), the default `false` is sufficient and the flag MAY be omitted.").default(false) }).catchall(z.any()).and(z.intersection(z.intersection(z.any(), z.intersection(z.any(), z.any())), z.intersection(z.any(), z.intersection(z.any(), z.any()))))).describe("Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.").optional(), "required_connections": z.array(z.object({ "provider": z.string().describe("Stable provider or platform namespace, preferably lowercase. Examples: `social.example`, `shortvideo.example`, or a seller-defined namespace. Omit only when the requirement is provider-agnostic, or when an `authorization_url` fully routes the human to the correct provider-specific connection flow.").optional(), "connection_type": z.enum(["advertiser_account","publisher_identity","post_authorization"]).describe("Kind of downstream connection required. `advertiser_account` is the platform account used to buy/manage ads. `publisher_identity` is the creator, page, channel, organization, or profile that owns source posts. `post_authorization` is a post-scoped grant when the platform authorizes individual posts instead of, or in addition to, the owning identity."), "required_for": z.array(z.string().min(1)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Concrete AdCP protocol operation names that require this downstream connection. Sellers SHOULD include this in product declarations when the requirement is known ahead of time, and in AUTHORIZATION_REQUIRED details when it explains the failed operation. Prefer specific operation names such as `list_creatives`, `sync_creatives`, `create_media_buy`, `get_media_buy_delivery`, or `get_creative_delivery` over broad category labels such as `reporting`.").optional(), "scope": z.enum(["account","identity","post","unknown"]).describe("Granularity of the downstream grant.").optional(), "status": z.enum(["connected","missing","pending","expired","revoked","not_required","unknown"]).describe("Current seller-observed state for this downstream connection when known. Product declarations MAY omit status or use `unknown`; AUTHORIZATION_REQUIRED details SHOULD use `missing`, `expired`, or `revoked` for the connection that blocked the call.").optional(), "connection_id": z.string().describe("Seller-defined identifier for an already-created downstream connection. Omit when no connection exists yet or when exposing it would leak platform/account state.").optional(), "resource_ref": z.object({ "platform_account_id": z.string().describe("Provider-native advertiser or business account id, when safe to disclose.").optional(), "identity_id": z.string().describe("Provider-native creator, page, channel, organization, or profile id, when safe to disclose.").optional(), "handle": z.string().describe("Provider-native public handle for the owning identity, when available.").optional(), "profile_url": z.string().url().describe("Public URL for the owning identity, when available.").optional(), "post_id": z.string().describe("Provider-native post id, when the grant is post-scoped or the failed request referenced a specific post.").optional(), "post_url": z.string().url().describe("Public URL for the referenced post, when available.").optional() }).catchall(z.any()).describe("Optional opaque provider-native resource hint, such as a platform account id, profile URL, handle, channel id, post id, or post URL. This is a hint for routing authorization, not proof that authorization exists.").optional(), "authorization_url": z.string().url().describe("Seller-hosted or provider-hosted URL where a human can complete or restore this downstream connection.").optional(), "authorization_instructions": z.string().describe("Human-readable instructions for completing or restoring this downstream connection.").optional(), "expires_at": z.string().datetime().describe("Expiration time for the downstream grant, when known.").optional() }).catchall(z.any()).and(z.any()).describe("A seller/platform-side connection or grant required by a product, format, or request. This is not the AdCP caller credential: the AdCP request is still authenticated once, and the seller uses these stored downstream connections to call a platform or service on the buyer's behalf. Use this shape for platforms that require more than one downstream grant, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.")).describe("Downstream platform connections or grants required to use this format declaration. These are in addition to the single AdCP caller credential. Use this when a platform product requires multiple downstream grants, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.").optional(), "reference_mutability": z.enum(["immutable_snapshot","mutable_requires_reapproval","mutable_auto_recheck"]).describe("Policy for formats whose `slots` accept a `published_post` reference. `immutable_snapshot`: seller snapshots the referenced post at approval and later source changes do not change the served creative. `mutable_requires_reapproval`: the source post may change and material changes require review before continued serving. `mutable_auto_recheck`: the source post may change and the seller continuously or periodically rechecks authorization/policy without requiring buyer resubmission. Omit when the format has no `published_post` slot.").optional(), "production_window_business_days": z.number().int().gte(0).describe("Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative).").optional() }).catchall(z.any()).describe("Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).").describe("Direct audio creative — buyer ships an `audio` asset (mp3/aac/wav) for asset-driven products, or ships a `script` / `creative_brief` text asset for products where the seller produces audio internally (podcast host-reads, TTS synthesis). Optional companion slots: `companion_image`, `brand_name`, `landing_page_url`. Tracking model: standard impression + completion + companion-image-click pixels via universal_macros. Distinct from `audio_daast` (DAAST tag, inherent DAAST event tracking). For host-reads and synthesized audio, the format declares `asset_source: 'publisher_host_recorded'` or `'agent_synthesized'` plus `buyer_asset_acceptance: 'rejected'`; the format's `slots` declaration enumerates which assets the buyer ships (e.g., `script` text asset for host-reads). The seller decides how to consume each asset (render verbatim vs produce audio from text) — there is no separate manifest 'inputs' map; everything the buyer ships goes in `assets`.") }), z.object({ "format_kind": z.literal("audio_daast"), "params": z.object({ "experimental": z.boolean().describe("When true, this canonical or seller narrowing may not work as declared. Adopters SHOULD preflight it with validate_input or in a sandbox and SHOULD NOT route production budget without testing; experimental status never makes the deprecated v1 path preferable. Drivers include unsettled spec shape, an adopter runtime gap, and custom shapes awaiting promotion. This replaces the earlier status plus runtime_status axes. Sellers SHOULD set experimental whenever a canonical or declaration is not production-ready.").default(false), "deprecated": z.boolean().describe("When true, this canonical (or a seller's specific narrowing of it) is going away. Existing adopters are supported through the deprecation cycle; new adoption is discouraged. Pair with `migration_target_version` to indicate when the canonical is expected to be removed. Distinct from `experimental`: an experimental canonical may stabilize and stop being experimental; a deprecated canonical is on a sunset path.").default(false), "v1_translatable": z.boolean().describe("Whether this canonical has any v1 named-format equivalent. `true` (default) — the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1→v2 projection via `v1-canonical-mapping.json` is meaningful. `false` — the canonical is inherently new in v2 and has no v1 form; v1's `list_creative_formats` couldn't express it because the underlying concept (algorithmic surface composition, AI-surface mentions, retail-media catalog placements, multi-card carousels) didn't exist as a v1 named-format archetype.\n\nLets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The six inherently-v2 canonicals in 3.2 are `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`, `seller_rendered_stateful_display`, and `coordinated_placements`.").default(true), "since_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version that introduced this canonical (e.g., '3.1', '3.2'). Lets adopters reason about minimum protocol version requirements when consuming a format declaration. Patch precision is intentionally rejected — canonicals are introduced at minor-version boundaries.").optional(), "migration_target_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version by which the working group expects this canonical to stabilize, surface a breaking revision, or (when `deprecated: true`) be removed. Patch precision is intentionally rejected — canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown').").optional(), "composition_model": z.enum(["deterministic","algorithmic"]).describe("Whether the surface composes deterministically (buyer can predict per-slot rendering — sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing — responsive_creative, agent_placement).").optional(), "provenance_required": z.boolean().describe("When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent.").optional(), "platform_extensions": z.array(z.object({ "uri": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL identifying the extension. `https://` is mandatory — `http://`, `file://`, `data:`, and other schemes are rejected at the schema layer (defense-in-depth on top of the fetch-contract normative rules). The URI base is the owning agent's URL; the path identifies the extension within that agent. Example: 'https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel'. The full fetch contract — SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds — is documented on `product-format-declaration.json#format_schema` and applies to ALL fetches of this reference shape regardless of whether the field is named `format_schema` (load-bearing for validation) or `platform_extensions` (informational); the *transport* rules are identical, only the *consumption* semantics differ."), "digest": z.string().regex(new RegExp("^sha256:[a-f0-9]{64}$")).describe("SHA-256 content digest of the extension definition (sha256:). Used to detect drift — if the agent revises the extension, the digest changes and cached definitions become invalid.") }).catchall(z.any()).describe("Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` — divergent digests in one response indicate producer-side error (e.g., concurrent extension revision mid-render). Buyers encountering divergent digests for the same URI MUST fail closed: treat all references to that URI as unresolved and surface a validation error rather than picking one branch silently. **Across responses**, digest divergence is normal — extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.")).describe("Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\n\n**Collision precedence (normative).** When two or more `platform_extensions[]` entries on the same declaration extend the same target (e.g., both extend `tracking`) with overlapping field names, **array order is authoritative — later entries override earlier ones on a per-field basis** (last-in-array-wins). SDKs MUST surface the overlap via the `errors[]` array on the `get_products` response with a structured code (`FORMAT_DECLARATION_DIVERGENT` is appropriate when the overlap appears across dual-emitted shapes; a producer-self-emitted overlap on a single declaration SHOULD use the same code with `error.details: { collision_kind: \"platform_extension_field\", target, overlapping_fields, winning_extension_uri }`). Producers SHOULD avoid the collision by emitting one extension per target or by partitioning fields across extensions; the deterministic precedence is for last-resort consistency across SDK implementations, not a sanctioned merging strategy.").optional(), "synthesis_nondeterministic": z.boolean().describe("When true, the format's production pipeline is genuinely nondeterministic — the platform cannot guarantee that synthesis from a given input set produces in-spec output. Veo / Sora / Runway-class generative video, and other AI-synthesis flows where output dimensions, duration, or quality vary per run. Implies a different validation contract: predictive `validate_input` is impossible; the platform's own post-synthesis QA loop applies; if the QA loop exhausts without producing a valid artifact, `build_creative` returns task_failed with a synthesis_failed reason. Distinct from `composition_model` (which describes how the surface composes per-slot rendering, not whether synthesis is deterministic). When false or absent, the format's production is predictable enough that `validate_input` can predict output properties from input properties.\n\n**Compatibility with `asset_source` / `item_production_model`**: `synthesis_nondeterministic: true` MAY pair with any of `seller_pre_rendered_from_brief`, `seller_human_designed`, or `agent_synthesized` (the QA loop is concept-level, not source-specific — 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.").default(false), "slots": z.array(z.object({ "asset_group_id": z.string().describe("Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings."), "asset_type": z.enum(["image","video","audio","text","markdown","url","html","css","javascript","vast","daast","webhook","brief","catalog","published_post","zip","card","object","pixel_tracker","vast_tracker","daast_tracker"]).describe("Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `published_post` is an existing-post reference asset, not uploaded media bytes and not a catalog row. `card` is the multi-card carousel element type (see card-asset.json). `pixel_tracker` / `vast_tracker` / `daast_tracker` are the renderer-fired measurement-tracker primitives — see `/schemas/core/assets/pixel-tracker-asset.json` and the VAST / DAAST tracker schemas. `object` is a last-resort fallback for structured non-asset inputs that don't fit any primitive asset_type — prefer specific types whenever possible."), "required": z.boolean().describe("Whether this slot is required for a valid manifest.").default(false), "min": z.number().int().gte(0).describe("Minimum count for repeatable / pool slots.").optional(), "max": z.number().int().gte(1).describe("Maximum count for repeatable / pool slots.").optional(), "max_chars": z.number().int().gte(1).describe("Per-slot character limit. Valid only when `asset_type` is `text`, `markdown`, or `brief`. Mutually exclusive with `max_size_kb` (which applies to binary asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "max_size_kb": z.number().int().gte(1).describe("Per-slot file size limit in kilobytes. Valid only when `asset_type` is `image`, `video`, `audio`, or `zip`. Mutually exclusive with `max_chars` (which applies to text asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Accepted intrinsic-pixel densities for this image-bearing slot. Valid when `asset_type` is `image`, and on a `card` slot where it constrains each card's image media (video media is unaffected). This makes density available to every canonical carrying image assets (native, carousel, responsive, companion images, and image itself), not only `format_kind: image`. When the image canonical also declares top-level `params.pixel_ratios`, the effective set is the intersection; an empty intersection is invalid. One matching asset satisfies the slot unless `required_pixel_ratios` requires rendition coverage.").optional(), "required_pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Required density coverage for an image rendition set. Valid only when `asset_type` is `image` and `pixel_ratios` is also declared. Every value MUST appear in the effective accepted set after intersecting any top-level image `params.pixel_ratios`, and the manifest slot value MUST be an array containing exactly one matching image rendition for each required ratio. Other accepted ratios remain optional. For example, `pixel_ratios: [1, 1.5, 2]` with `required_pixel_ratios: [1, 2]` requires the 1x and 2x renditions while making 1.5x optional. SDKs enforce intersection, subset, coverage, and duplicate-ratio rules because JSON Schema draft-07 cannot express them generically.").optional(), "logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("When `asset_group_id` is `logo`, renderer-facing brand.json logo slots acceptable for this format slot. Producers selecting from brand.json SHOULD prefer `logos[]` entries whose `slots[]` intersects this list, then apply `visual_guidelines.logo_usage_rules[]`.").optional(), "required_logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Subset of `logo_slots` for which this format expects explicit logo coverage. A manifest or brand-derived logo pool SHOULD include at least one usable logo for each required slot; if coverage is missing, builders SHOULD surface a validation warning or approval mapping instead of guessing from prose.").optional(), "description": z.string().describe("Human-readable description of what the slot expects from the buyer.").optional(), "consumed_for_production": z.boolean().describe("Dispatch hint for `build_creative` and v1↔v2 wire translators: when `true`, the slot's value is consumed as INPUT to a production step (host-read script, brief copy fed to generative synthesis, catalog feed driving per-SKU rendering) and is not rendered verbatim. When `false` (default), the slot's value is rendered verbatim on the placement (image bytes, video file, display tag).\n\nMotivates the v1↔v2 dispatch table: pre-v2 buyers shipped production-consumed inputs separately in a `inputs` map on the build_creative request; v2 collapses inputs and rendered assets into a single `assets` map keyed by `asset_group_id`. SDK translators between v1 and v2 use this flag per canonical to know which assets in the v2 manifest map back to v1 `inputs` vs v1 `assets`. Without the per-slot flag the dispatch table lives in adopter code and every SDK gets it slightly different.\n\nProducers SHOULD set this explicitly on slots whose consumption pattern isn't obvious (host-read scripts on `audio_hosted`, briefs on generative `video_hosted`, catalog feeds on `sponsored_placement`). For canonicals where every slot is render-verbatim (`image`, `display_tag`, `video_vast`), the default `false` is sufficient and the flag MAY be omitted.").default(false) }).catchall(z.any()).and(z.intersection(z.intersection(z.any(), z.intersection(z.any(), z.any())), z.intersection(z.any(), z.intersection(z.any(), z.any()))))).describe("Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.").optional(), "required_connections": z.array(z.object({ "provider": z.string().describe("Stable provider or platform namespace, preferably lowercase. Examples: `social.example`, `shortvideo.example`, or a seller-defined namespace. Omit only when the requirement is provider-agnostic, or when an `authorization_url` fully routes the human to the correct provider-specific connection flow.").optional(), "connection_type": z.enum(["advertiser_account","publisher_identity","post_authorization"]).describe("Kind of downstream connection required. `advertiser_account` is the platform account used to buy/manage ads. `publisher_identity` is the creator, page, channel, organization, or profile that owns source posts. `post_authorization` is a post-scoped grant when the platform authorizes individual posts instead of, or in addition to, the owning identity."), "required_for": z.array(z.string().min(1)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Concrete AdCP protocol operation names that require this downstream connection. Sellers SHOULD include this in product declarations when the requirement is known ahead of time, and in AUTHORIZATION_REQUIRED details when it explains the failed operation. Prefer specific operation names such as `list_creatives`, `sync_creatives`, `create_media_buy`, `get_media_buy_delivery`, or `get_creative_delivery` over broad category labels such as `reporting`.").optional(), "scope": z.enum(["account","identity","post","unknown"]).describe("Granularity of the downstream grant.").optional(), "status": z.enum(["connected","missing","pending","expired","revoked","not_required","unknown"]).describe("Current seller-observed state for this downstream connection when known. Product declarations MAY omit status or use `unknown`; AUTHORIZATION_REQUIRED details SHOULD use `missing`, `expired`, or `revoked` for the connection that blocked the call.").optional(), "connection_id": z.string().describe("Seller-defined identifier for an already-created downstream connection. Omit when no connection exists yet or when exposing it would leak platform/account state.").optional(), "resource_ref": z.object({ "platform_account_id": z.string().describe("Provider-native advertiser or business account id, when safe to disclose.").optional(), "identity_id": z.string().describe("Provider-native creator, page, channel, organization, or profile id, when safe to disclose.").optional(), "handle": z.string().describe("Provider-native public handle for the owning identity, when available.").optional(), "profile_url": z.string().url().describe("Public URL for the owning identity, when available.").optional(), "post_id": z.string().describe("Provider-native post id, when the grant is post-scoped or the failed request referenced a specific post.").optional(), "post_url": z.string().url().describe("Public URL for the referenced post, when available.").optional() }).catchall(z.any()).describe("Optional opaque provider-native resource hint, such as a platform account id, profile URL, handle, channel id, post id, or post URL. This is a hint for routing authorization, not proof that authorization exists.").optional(), "authorization_url": z.string().url().describe("Seller-hosted or provider-hosted URL where a human can complete or restore this downstream connection.").optional(), "authorization_instructions": z.string().describe("Human-readable instructions for completing or restoring this downstream connection.").optional(), "expires_at": z.string().datetime().describe("Expiration time for the downstream grant, when known.").optional() }).catchall(z.any()).and(z.any()).describe("A seller/platform-side connection or grant required by a product, format, or request. This is not the AdCP caller credential: the AdCP request is still authenticated once, and the seller uses these stored downstream connections to call a platform or service on the buyer's behalf. Use this shape for platforms that require more than one downstream grant, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.")).describe("Downstream platform connections or grants required to use this format declaration. These are in addition to the single AdCP caller credential. Use this when a platform product requires multiple downstream grants, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.").optional(), "reference_mutability": z.enum(["immutable_snapshot","mutable_requires_reapproval","mutable_auto_recheck"]).describe("Policy for formats whose `slots` accept a `published_post` reference. `immutable_snapshot`: seller snapshots the referenced post at approval and later source changes do not change the served creative. `mutable_requires_reapproval`: the source post may change and material changes require review before continued serving. `mutable_auto_recheck`: the source post may change and the seller continuously or periodically rechecks authorization/policy without requiring buyer resubmission. Omit when the format has no `published_post` slot.").optional(), "production_window_business_days": z.number().int().gte(0).describe("Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative).").optional() }).catchall(z.any()).describe("Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).").describe("DAAST-tag-delivered audio creative (audio analog of VAST). Slot: `daast_tag` (daast asset, URL or inline XML). Tracking model: DAAST events inherent to the spec — `impression`, `firstQuartile`, `midpoint`, `thirdQuartile`, `complete`, `start`, `pause`, `resume`, `mute`, `unmute`, `clickTracking`, `error`. Distinct from `audio_hosted` (direct file with external tracking).") }), z.object({ "format_kind": z.literal("sponsored_placement"), "params": z.intersection(z.object({ "experimental": z.boolean().describe("When true, this canonical or seller narrowing may not work as declared. Adopters SHOULD preflight it with validate_input or in a sandbox and SHOULD NOT route production budget without testing; experimental status never makes the deprecated v1 path preferable. Drivers include unsettled spec shape, an adopter runtime gap, and custom shapes awaiting promotion. This replaces the earlier status plus runtime_status axes. Sellers SHOULD set experimental whenever a canonical or declaration is not production-ready.").default(false), "deprecated": z.boolean().describe("When true, this canonical (or a seller's specific narrowing of it) is going away. Existing adopters are supported through the deprecation cycle; new adoption is discouraged. Pair with `migration_target_version` to indicate when the canonical is expected to be removed. Distinct from `experimental`: an experimental canonical may stabilize and stop being experimental; a deprecated canonical is on a sunset path.").default(false), "v1_translatable": z.boolean().describe("Whether this canonical has any v1 named-format equivalent. `true` (default) — the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1→v2 projection via `v1-canonical-mapping.json` is meaningful. `false` — the canonical is inherently new in v2 and has no v1 form; v1's `list_creative_formats` couldn't express it because the underlying concept (algorithmic surface composition, AI-surface mentions, retail-media catalog placements, multi-card carousels) didn't exist as a v1 named-format archetype.\n\nLets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The six inherently-v2 canonicals in 3.2 are `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`, `seller_rendered_stateful_display`, and `coordinated_placements`.").default(true), "since_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version that introduced this canonical (e.g., '3.1', '3.2'). Lets adopters reason about minimum protocol version requirements when consuming a format declaration. Patch precision is intentionally rejected — canonicals are introduced at minor-version boundaries.").optional(), "migration_target_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version by which the working group expects this canonical to stabilize, surface a breaking revision, or (when `deprecated: true`) be removed. Patch precision is intentionally rejected — canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown').").optional(), "composition_model": z.enum(["deterministic","algorithmic"]).describe("Whether the surface composes deterministically (buyer can predict per-slot rendering — sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing — responsive_creative, agent_placement).").optional(), "provenance_required": z.boolean().describe("When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent.").optional(), "platform_extensions": z.array(z.object({ "uri": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL identifying the extension. `https://` is mandatory — `http://`, `file://`, `data:`, and other schemes are rejected at the schema layer (defense-in-depth on top of the fetch-contract normative rules). The URI base is the owning agent's URL; the path identifies the extension within that agent. Example: 'https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel'. The full fetch contract — SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds — is documented on `product-format-declaration.json#format_schema` and applies to ALL fetches of this reference shape regardless of whether the field is named `format_schema` (load-bearing for validation) or `platform_extensions` (informational); the *transport* rules are identical, only the *consumption* semantics differ."), "digest": z.string().regex(new RegExp("^sha256:[a-f0-9]{64}$")).describe("SHA-256 content digest of the extension definition (sha256:). Used to detect drift — if the agent revises the extension, the digest changes and cached definitions become invalid.") }).catchall(z.any()).describe("Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` — divergent digests in one response indicate producer-side error (e.g., concurrent extension revision mid-render). Buyers encountering divergent digests for the same URI MUST fail closed: treat all references to that URI as unresolved and surface a validation error rather than picking one branch silently. **Across responses**, digest divergence is normal — extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.")).describe("Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\n\n**Collision precedence (normative).** When two or more `platform_extensions[]` entries on the same declaration extend the same target (e.g., both extend `tracking`) with overlapping field names, **array order is authoritative — later entries override earlier ones on a per-field basis** (last-in-array-wins). SDKs MUST surface the overlap via the `errors[]` array on the `get_products` response with a structured code (`FORMAT_DECLARATION_DIVERGENT` is appropriate when the overlap appears across dual-emitted shapes; a producer-self-emitted overlap on a single declaration SHOULD use the same code with `error.details: { collision_kind: \"platform_extension_field\", target, overlapping_fields, winning_extension_uri }`). Producers SHOULD avoid the collision by emitting one extension per target or by partitioning fields across extensions; the deterministic precedence is for last-resort consistency across SDK implementations, not a sanctioned merging strategy.").optional(), "synthesis_nondeterministic": z.boolean().describe("When true, the format's production pipeline is genuinely nondeterministic — the platform cannot guarantee that synthesis from a given input set produces in-spec output. Veo / Sora / Runway-class generative video, and other AI-synthesis flows where output dimensions, duration, or quality vary per run. Implies a different validation contract: predictive `validate_input` is impossible; the platform's own post-synthesis QA loop applies; if the QA loop exhausts without producing a valid artifact, `build_creative` returns task_failed with a synthesis_failed reason. Distinct from `composition_model` (which describes how the surface composes per-slot rendering, not whether synthesis is deterministic). When false or absent, the format's production is predictable enough that `validate_input` can predict output properties from input properties.\n\n**Compatibility with `asset_source` / `item_production_model`**: `synthesis_nondeterministic: true` MAY pair with any of `seller_pre_rendered_from_brief`, `seller_human_designed`, or `agent_synthesized` (the QA loop is concept-level, not source-specific — 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.").default(false), "slots": z.array(z.object({ "asset_group_id": z.string().describe("Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings."), "asset_type": z.enum(["image","video","audio","text","markdown","url","html","css","javascript","vast","daast","webhook","brief","catalog","published_post","zip","card","object","pixel_tracker","vast_tracker","daast_tracker"]).describe("Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `published_post` is an existing-post reference asset, not uploaded media bytes and not a catalog row. `card` is the multi-card carousel element type (see card-asset.json). `pixel_tracker` / `vast_tracker` / `daast_tracker` are the renderer-fired measurement-tracker primitives — see `/schemas/core/assets/pixel-tracker-asset.json` and the VAST / DAAST tracker schemas. `object` is a last-resort fallback for structured non-asset inputs that don't fit any primitive asset_type — prefer specific types whenever possible."), "required": z.boolean().describe("Whether this slot is required for a valid manifest.").default(false), "min": z.number().int().gte(0).describe("Minimum count for repeatable / pool slots.").optional(), "max": z.number().int().gte(1).describe("Maximum count for repeatable / pool slots.").optional(), "max_chars": z.number().int().gte(1).describe("Per-slot character limit. Valid only when `asset_type` is `text`, `markdown`, or `brief`. Mutually exclusive with `max_size_kb` (which applies to binary asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "max_size_kb": z.number().int().gte(1).describe("Per-slot file size limit in kilobytes. Valid only when `asset_type` is `image`, `video`, `audio`, or `zip`. Mutually exclusive with `max_chars` (which applies to text asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Accepted intrinsic-pixel densities for this image-bearing slot. Valid when `asset_type` is `image`, and on a `card` slot where it constrains each card's image media (video media is unaffected). This makes density available to every canonical carrying image assets (native, carousel, responsive, companion images, and image itself), not only `format_kind: image`. When the image canonical also declares top-level `params.pixel_ratios`, the effective set is the intersection; an empty intersection is invalid. One matching asset satisfies the slot unless `required_pixel_ratios` requires rendition coverage.").optional(), "required_pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Required density coverage for an image rendition set. Valid only when `asset_type` is `image` and `pixel_ratios` is also declared. Every value MUST appear in the effective accepted set after intersecting any top-level image `params.pixel_ratios`, and the manifest slot value MUST be an array containing exactly one matching image rendition for each required ratio. Other accepted ratios remain optional. For example, `pixel_ratios: [1, 1.5, 2]` with `required_pixel_ratios: [1, 2]` requires the 1x and 2x renditions while making 1.5x optional. SDKs enforce intersection, subset, coverage, and duplicate-ratio rules because JSON Schema draft-07 cannot express them generically.").optional(), "logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("When `asset_group_id` is `logo`, renderer-facing brand.json logo slots acceptable for this format slot. Producers selecting from brand.json SHOULD prefer `logos[]` entries whose `slots[]` intersects this list, then apply `visual_guidelines.logo_usage_rules[]`.").optional(), "required_logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Subset of `logo_slots` for which this format expects explicit logo coverage. A manifest or brand-derived logo pool SHOULD include at least one usable logo for each required slot; if coverage is missing, builders SHOULD surface a validation warning or approval mapping instead of guessing from prose.").optional(), "description": z.string().describe("Human-readable description of what the slot expects from the buyer.").optional(), "consumed_for_production": z.boolean().describe("Dispatch hint for `build_creative` and v1↔v2 wire translators: when `true`, the slot's value is consumed as INPUT to a production step (host-read script, brief copy fed to generative synthesis, catalog feed driving per-SKU rendering) and is not rendered verbatim. When `false` (default), the slot's value is rendered verbatim on the placement (image bytes, video file, display tag).\n\nMotivates the v1↔v2 dispatch table: pre-v2 buyers shipped production-consumed inputs separately in a `inputs` map on the build_creative request; v2 collapses inputs and rendered assets into a single `assets` map keyed by `asset_group_id`. SDK translators between v1 and v2 use this flag per canonical to know which assets in the v2 manifest map back to v1 `inputs` vs v1 `assets`. Without the per-slot flag the dispatch table lives in adopter code and every SDK gets it slightly different.\n\nProducers SHOULD set this explicitly on slots whose consumption pattern isn't obvious (host-read scripts on `audio_hosted`, briefs on generative `video_hosted`, catalog feeds on `sponsored_placement`). For canonicals where every slot is render-verbatim (`image`, `display_tag`, `video_vast`), the default `false` is sufficient and the flag MAY be omitted.").default(false) }).catchall(z.any()).and(z.intersection(z.intersection(z.any(), z.intersection(z.any(), z.any())), z.intersection(z.any(), z.intersection(z.any(), z.any()))))).describe("Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.").optional(), "required_connections": z.array(z.object({ "provider": z.string().describe("Stable provider or platform namespace, preferably lowercase. Examples: `social.example`, `shortvideo.example`, or a seller-defined namespace. Omit only when the requirement is provider-agnostic, or when an `authorization_url` fully routes the human to the correct provider-specific connection flow.").optional(), "connection_type": z.enum(["advertiser_account","publisher_identity","post_authorization"]).describe("Kind of downstream connection required. `advertiser_account` is the platform account used to buy/manage ads. `publisher_identity` is the creator, page, channel, organization, or profile that owns source posts. `post_authorization` is a post-scoped grant when the platform authorizes individual posts instead of, or in addition to, the owning identity."), "required_for": z.array(z.string().min(1)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Concrete AdCP protocol operation names that require this downstream connection. Sellers SHOULD include this in product declarations when the requirement is known ahead of time, and in AUTHORIZATION_REQUIRED details when it explains the failed operation. Prefer specific operation names such as `list_creatives`, `sync_creatives`, `create_media_buy`, `get_media_buy_delivery`, or `get_creative_delivery` over broad category labels such as `reporting`.").optional(), "scope": z.enum(["account","identity","post","unknown"]).describe("Granularity of the downstream grant.").optional(), "status": z.enum(["connected","missing","pending","expired","revoked","not_required","unknown"]).describe("Current seller-observed state for this downstream connection when known. Product declarations MAY omit status or use `unknown`; AUTHORIZATION_REQUIRED details SHOULD use `missing`, `expired`, or `revoked` for the connection that blocked the call.").optional(), "connection_id": z.string().describe("Seller-defined identifier for an already-created downstream connection. Omit when no connection exists yet or when exposing it would leak platform/account state.").optional(), "resource_ref": z.object({ "platform_account_id": z.string().describe("Provider-native advertiser or business account id, when safe to disclose.").optional(), "identity_id": z.string().describe("Provider-native creator, page, channel, organization, or profile id, when safe to disclose.").optional(), "handle": z.string().describe("Provider-native public handle for the owning identity, when available.").optional(), "profile_url": z.string().url().describe("Public URL for the owning identity, when available.").optional(), "post_id": z.string().describe("Provider-native post id, when the grant is post-scoped or the failed request referenced a specific post.").optional(), "post_url": z.string().url().describe("Public URL for the referenced post, when available.").optional() }).catchall(z.any()).describe("Optional opaque provider-native resource hint, such as a platform account id, profile URL, handle, channel id, post id, or post URL. This is a hint for routing authorization, not proof that authorization exists.").optional(), "authorization_url": z.string().url().describe("Seller-hosted or provider-hosted URL where a human can complete or restore this downstream connection.").optional(), "authorization_instructions": z.string().describe("Human-readable instructions for completing or restoring this downstream connection.").optional(), "expires_at": z.string().datetime().describe("Expiration time for the downstream grant, when known.").optional() }).catchall(z.any()).and(z.any()).describe("A seller/platform-side connection or grant required by a product, format, or request. This is not the AdCP caller credential: the AdCP request is still authenticated once, and the seller uses these stored downstream connections to call a platform or service on the buyer's behalf. Use this shape for platforms that require more than one downstream grant, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.")).describe("Downstream platform connections or grants required to use this format declaration. These are in addition to the single AdCP caller credential. Use this when a platform product requires multiple downstream grants, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.").optional(), "reference_mutability": z.enum(["immutable_snapshot","mutable_requires_reapproval","mutable_auto_recheck"]).describe("Policy for formats whose `slots` accept a `published_post` reference. `immutable_snapshot`: seller snapshots the referenced post at approval and later source changes do not change the served creative. `mutable_requires_reapproval`: the source post may change and material changes require review before continued serving. `mutable_auto_recheck`: the source post may change and the seller continuously or periodically rechecks authorization/policy without requiring buyer resubmission. Omit when the format has no `published_post` slot.").optional(), "production_window_business_days": z.number().int().gte(0).describe("Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative).").optional() }).catchall(z.any()).describe("Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints)."), z.any()).describe("Catalog-driven retail-media format. Slot: `source_catalog` (catalog asset — product/SKU/ASIN/GTIN catalog reference, REQUIRED), optional `hero_asset`, optional `landing_page_url`. Buyer supplies the catalog reference; surface composes per-item or multi-item rendering using its native placement template. **Composition is deterministic** — buyer can predict per-slot rendering from the catalog item structure. Tracking model: per-item impression + click + conversion (catalog-keyed via offering_id/sku/gtin macros). Covers Amazon Sponsored Products, Criteo Sponsored Products, CitrusAd Sponsored Products, Walmart Connect Sponsored Products, Pinterest Collection (catalog-driven mode).\n\n**Scope (normative — buyer-agent routing).** This canonical is the home for catalog-driven retail-media placements ONLY. The defining feature is the `source_catalog` slot — products under this canonical compose their creative *per catalog item* using the buyer-supplied catalog feed. Without a catalog feed there is nothing to render against. Buyer agents reading `format_kind: sponsored_placement` MUST attach a catalog reference; sellers MUST require `source_catalog` in the manifest.\n\n**Not this canonical (route elsewhere):**\n- IAB in-feed native ads, content-recommendation widgets (Taboola, Outbrain, Yahoo Native, AdMob Native, in-feed sponsored cards) — use `native_in_feed` (asset-bundle composition; no catalog).\n- Algorithmic surface that picks from a buyer-supplied asset pool (Google PMax, Meta Advantage+) — use `responsive_creative`.\n- Single-image or single-video creative — use `image` or `video_hosted`.\n\nThe earlier broader framing ('any sponsored placement') was too loose for buyer-agent routing — a buyer reading `sponsored_placement` couldn't disambiguate a catalog-driven Amazon SP from an in-feed Taboola widget. As of 3.1, the canonical is narrowed to catalog-keyed retail-media; native moves to `native_in_feed`. Distinct from `responsive_creative` (algorithmic combinator from buyer pool) and `agent_placement` (text/audio AI-surface composition).") }), z.object({ "format_kind": z.literal("native_in_feed"), "params": z.intersection(z.object({ "experimental": z.boolean().describe("When true, this canonical or seller narrowing may not work as declared. Adopters SHOULD preflight it with validate_input or in a sandbox and SHOULD NOT route production budget without testing; experimental status never makes the deprecated v1 path preferable. Drivers include unsettled spec shape, an adopter runtime gap, and custom shapes awaiting promotion. This replaces the earlier status plus runtime_status axes. Sellers SHOULD set experimental whenever a canonical or declaration is not production-ready.").default(false), "deprecated": z.boolean().describe("When true, this canonical (or a seller's specific narrowing of it) is going away. Existing adopters are supported through the deprecation cycle; new adoption is discouraged. Pair with `migration_target_version` to indicate when the canonical is expected to be removed. Distinct from `experimental`: an experimental canonical may stabilize and stop being experimental; a deprecated canonical is on a sunset path.").default(false), "v1_translatable": z.boolean().describe("Whether this canonical has any v1 named-format equivalent. `true` (default) — the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1→v2 projection via `v1-canonical-mapping.json` is meaningful. `false` — the canonical is inherently new in v2 and has no v1 form; v1's `list_creative_formats` couldn't express it because the underlying concept (algorithmic surface composition, AI-surface mentions, retail-media catalog placements, multi-card carousels) didn't exist as a v1 named-format archetype.\n\nLets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The six inherently-v2 canonicals in 3.2 are `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`, `seller_rendered_stateful_display`, and `coordinated_placements`.").default(true), "since_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version that introduced this canonical (e.g., '3.1', '3.2'). Lets adopters reason about minimum protocol version requirements when consuming a format declaration. Patch precision is intentionally rejected — canonicals are introduced at minor-version boundaries.").optional(), "migration_target_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version by which the working group expects this canonical to stabilize, surface a breaking revision, or (when `deprecated: true`) be removed. Patch precision is intentionally rejected — canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown').").optional(), "composition_model": z.enum(["deterministic","algorithmic"]).describe("Whether the surface composes deterministically (buyer can predict per-slot rendering — sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing — responsive_creative, agent_placement).").optional(), "provenance_required": z.boolean().describe("When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent.").optional(), "platform_extensions": z.array(z.object({ "uri": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL identifying the extension. `https://` is mandatory — `http://`, `file://`, `data:`, and other schemes are rejected at the schema layer (defense-in-depth on top of the fetch-contract normative rules). The URI base is the owning agent's URL; the path identifies the extension within that agent. Example: 'https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel'. The full fetch contract — SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds — is documented on `product-format-declaration.json#format_schema` and applies to ALL fetches of this reference shape regardless of whether the field is named `format_schema` (load-bearing for validation) or `platform_extensions` (informational); the *transport* rules are identical, only the *consumption* semantics differ."), "digest": z.string().regex(new RegExp("^sha256:[a-f0-9]{64}$")).describe("SHA-256 content digest of the extension definition (sha256:). Used to detect drift — if the agent revises the extension, the digest changes and cached definitions become invalid.") }).catchall(z.any()).describe("Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` — divergent digests in one response indicate producer-side error (e.g., concurrent extension revision mid-render). Buyers encountering divergent digests for the same URI MUST fail closed: treat all references to that URI as unresolved and surface a validation error rather than picking one branch silently. **Across responses**, digest divergence is normal — extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.")).describe("Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\n\n**Collision precedence (normative).** When two or more `platform_extensions[]` entries on the same declaration extend the same target (e.g., both extend `tracking`) with overlapping field names, **array order is authoritative — later entries override earlier ones on a per-field basis** (last-in-array-wins). SDKs MUST surface the overlap via the `errors[]` array on the `get_products` response with a structured code (`FORMAT_DECLARATION_DIVERGENT` is appropriate when the overlap appears across dual-emitted shapes; a producer-self-emitted overlap on a single declaration SHOULD use the same code with `error.details: { collision_kind: \"platform_extension_field\", target, overlapping_fields, winning_extension_uri }`). Producers SHOULD avoid the collision by emitting one extension per target or by partitioning fields across extensions; the deterministic precedence is for last-resort consistency across SDK implementations, not a sanctioned merging strategy.").optional(), "synthesis_nondeterministic": z.boolean().describe("When true, the format's production pipeline is genuinely nondeterministic — the platform cannot guarantee that synthesis from a given input set produces in-spec output. Veo / Sora / Runway-class generative video, and other AI-synthesis flows where output dimensions, duration, or quality vary per run. Implies a different validation contract: predictive `validate_input` is impossible; the platform's own post-synthesis QA loop applies; if the QA loop exhausts without producing a valid artifact, `build_creative` returns task_failed with a synthesis_failed reason. Distinct from `composition_model` (which describes how the surface composes per-slot rendering, not whether synthesis is deterministic). When false or absent, the format's production is predictable enough that `validate_input` can predict output properties from input properties.\n\n**Compatibility with `asset_source` / `item_production_model`**: `synthesis_nondeterministic: true` MAY pair with any of `seller_pre_rendered_from_brief`, `seller_human_designed`, or `agent_synthesized` (the QA loop is concept-level, not source-specific — 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.").default(false), "slots": z.array(z.object({ "asset_group_id": z.string().describe("Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings."), "asset_type": z.enum(["image","video","audio","text","markdown","url","html","css","javascript","vast","daast","webhook","brief","catalog","published_post","zip","card","object","pixel_tracker","vast_tracker","daast_tracker"]).describe("Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `published_post` is an existing-post reference asset, not uploaded media bytes and not a catalog row. `card` is the multi-card carousel element type (see card-asset.json). `pixel_tracker` / `vast_tracker` / `daast_tracker` are the renderer-fired measurement-tracker primitives — see `/schemas/core/assets/pixel-tracker-asset.json` and the VAST / DAAST tracker schemas. `object` is a last-resort fallback for structured non-asset inputs that don't fit any primitive asset_type — prefer specific types whenever possible."), "required": z.boolean().describe("Whether this slot is required for a valid manifest.").default(false), "min": z.number().int().gte(0).describe("Minimum count for repeatable / pool slots.").optional(), "max": z.number().int().gte(1).describe("Maximum count for repeatable / pool slots.").optional(), "max_chars": z.number().int().gte(1).describe("Per-slot character limit. Valid only when `asset_type` is `text`, `markdown`, or `brief`. Mutually exclusive with `max_size_kb` (which applies to binary asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "max_size_kb": z.number().int().gte(1).describe("Per-slot file size limit in kilobytes. Valid only when `asset_type` is `image`, `video`, `audio`, or `zip`. Mutually exclusive with `max_chars` (which applies to text asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Accepted intrinsic-pixel densities for this image-bearing slot. Valid when `asset_type` is `image`, and on a `card` slot where it constrains each card's image media (video media is unaffected). This makes density available to every canonical carrying image assets (native, carousel, responsive, companion images, and image itself), not only `format_kind: image`. When the image canonical also declares top-level `params.pixel_ratios`, the effective set is the intersection; an empty intersection is invalid. One matching asset satisfies the slot unless `required_pixel_ratios` requires rendition coverage.").optional(), "required_pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Required density coverage for an image rendition set. Valid only when `asset_type` is `image` and `pixel_ratios` is also declared. Every value MUST appear in the effective accepted set after intersecting any top-level image `params.pixel_ratios`, and the manifest slot value MUST be an array containing exactly one matching image rendition for each required ratio. Other accepted ratios remain optional. For example, `pixel_ratios: [1, 1.5, 2]` with `required_pixel_ratios: [1, 2]` requires the 1x and 2x renditions while making 1.5x optional. SDKs enforce intersection, subset, coverage, and duplicate-ratio rules because JSON Schema draft-07 cannot express them generically.").optional(), "logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("When `asset_group_id` is `logo`, renderer-facing brand.json logo slots acceptable for this format slot. Producers selecting from brand.json SHOULD prefer `logos[]` entries whose `slots[]` intersects this list, then apply `visual_guidelines.logo_usage_rules[]`.").optional(), "required_logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Subset of `logo_slots` for which this format expects explicit logo coverage. A manifest or brand-derived logo pool SHOULD include at least one usable logo for each required slot; if coverage is missing, builders SHOULD surface a validation warning or approval mapping instead of guessing from prose.").optional(), "description": z.string().describe("Human-readable description of what the slot expects from the buyer.").optional(), "consumed_for_production": z.boolean().describe("Dispatch hint for `build_creative` and v1↔v2 wire translators: when `true`, the slot's value is consumed as INPUT to a production step (host-read script, brief copy fed to generative synthesis, catalog feed driving per-SKU rendering) and is not rendered verbatim. When `false` (default), the slot's value is rendered verbatim on the placement (image bytes, video file, display tag).\n\nMotivates the v1↔v2 dispatch table: pre-v2 buyers shipped production-consumed inputs separately in a `inputs` map on the build_creative request; v2 collapses inputs and rendered assets into a single `assets` map keyed by `asset_group_id`. SDK translators between v1 and v2 use this flag per canonical to know which assets in the v2 manifest map back to v1 `inputs` vs v1 `assets`. Without the per-slot flag the dispatch table lives in adopter code and every SDK gets it slightly different.\n\nProducers SHOULD set this explicitly on slots whose consumption pattern isn't obvious (host-read scripts on `audio_hosted`, briefs on generative `video_hosted`, catalog feeds on `sponsored_placement`). For canonicals where every slot is render-verbatim (`image`, `display_tag`, `video_vast`), the default `false` is sufficient and the flag MAY be omitted.").default(false) }).catchall(z.any()).and(z.intersection(z.intersection(z.any(), z.intersection(z.any(), z.any())), z.intersection(z.any(), z.intersection(z.any(), z.any()))))).describe("Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.").optional(), "required_connections": z.array(z.object({ "provider": z.string().describe("Stable provider or platform namespace, preferably lowercase. Examples: `social.example`, `shortvideo.example`, or a seller-defined namespace. Omit only when the requirement is provider-agnostic, or when an `authorization_url` fully routes the human to the correct provider-specific connection flow.").optional(), "connection_type": z.enum(["advertiser_account","publisher_identity","post_authorization"]).describe("Kind of downstream connection required. `advertiser_account` is the platform account used to buy/manage ads. `publisher_identity` is the creator, page, channel, organization, or profile that owns source posts. `post_authorization` is a post-scoped grant when the platform authorizes individual posts instead of, or in addition to, the owning identity."), "required_for": z.array(z.string().min(1)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Concrete AdCP protocol operation names that require this downstream connection. Sellers SHOULD include this in product declarations when the requirement is known ahead of time, and in AUTHORIZATION_REQUIRED details when it explains the failed operation. Prefer specific operation names such as `list_creatives`, `sync_creatives`, `create_media_buy`, `get_media_buy_delivery`, or `get_creative_delivery` over broad category labels such as `reporting`.").optional(), "scope": z.enum(["account","identity","post","unknown"]).describe("Granularity of the downstream grant.").optional(), "status": z.enum(["connected","missing","pending","expired","revoked","not_required","unknown"]).describe("Current seller-observed state for this downstream connection when known. Product declarations MAY omit status or use `unknown`; AUTHORIZATION_REQUIRED details SHOULD use `missing`, `expired`, or `revoked` for the connection that blocked the call.").optional(), "connection_id": z.string().describe("Seller-defined identifier for an already-created downstream connection. Omit when no connection exists yet or when exposing it would leak platform/account state.").optional(), "resource_ref": z.object({ "platform_account_id": z.string().describe("Provider-native advertiser or business account id, when safe to disclose.").optional(), "identity_id": z.string().describe("Provider-native creator, page, channel, organization, or profile id, when safe to disclose.").optional(), "handle": z.string().describe("Provider-native public handle for the owning identity, when available.").optional(), "profile_url": z.string().url().describe("Public URL for the owning identity, when available.").optional(), "post_id": z.string().describe("Provider-native post id, when the grant is post-scoped or the failed request referenced a specific post.").optional(), "post_url": z.string().url().describe("Public URL for the referenced post, when available.").optional() }).catchall(z.any()).describe("Optional opaque provider-native resource hint, such as a platform account id, profile URL, handle, channel id, post id, or post URL. This is a hint for routing authorization, not proof that authorization exists.").optional(), "authorization_url": z.string().url().describe("Seller-hosted or provider-hosted URL where a human can complete or restore this downstream connection.").optional(), "authorization_instructions": z.string().describe("Human-readable instructions for completing or restoring this downstream connection.").optional(), "expires_at": z.string().datetime().describe("Expiration time for the downstream grant, when known.").optional() }).catchall(z.any()).and(z.any()).describe("A seller/platform-side connection or grant required by a product, format, or request. This is not the AdCP caller credential: the AdCP request is still authenticated once, and the seller uses these stored downstream connections to call a platform or service on the buyer's behalf. Use this shape for platforms that require more than one downstream grant, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.")).describe("Downstream platform connections or grants required to use this format declaration. These are in addition to the single AdCP caller credential. Use this when a platform product requires multiple downstream grants, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.").optional(), "reference_mutability": z.enum(["immutable_snapshot","mutable_requires_reapproval","mutable_auto_recheck"]).describe("Policy for formats whose `slots` accept a `published_post` reference. `immutable_snapshot`: seller snapshots the referenced post at approval and later source changes do not change the served creative. `mutable_requires_reapproval`: the source post may change and material changes require review before continued serving. `mutable_auto_recheck`: the source post may change and the seller continuously or periodically rechecks authorization/policy without requiring buyer resubmission. Omit when the format has no `published_post` slot.").optional(), "production_window_business_days": z.number().int().gte(0).describe("Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative).").optional() }).catchall(z.any()).describe("Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints)."), z.intersection(z.any(), z.any())).describe("IAB-shaped native creative for in-feed and content-recommendation surfaces. Default slots cover the primary IAB OpenRTB Native 1.2 asset types — `title` (Title Asset), `body_text` (Data Asset type 2), `main_image` (Image Asset main), `icon` (Image Asset icon), `cta` (Data Asset type 12), `advertiser_name` (Data Asset type 1), `sponsored_label` (Title-adjacent), `landing_page_url` (Link Asset), `display_url` (Data Asset type 11 — visible URL/domain, distinct from clickthrough), `rating` (Data Asset type 3 — app/product rating), `price` (Data Asset type 6 — product price), plus renderer-fired `impression_tracker` / `viewability_tracker` / `click_tracker` (`pixel_tracker`). Products MAY use `slots_override` to add other IAB Native data asset types (likes — type 4, downloads — type 5, saleprice — type 7, phone_number — type 8, address — type 9, desc2 — type 10, etc.) or to remove slots the surface doesn't render. The publisher's renderer assembles these into its own look-and-feel — feed card, content-recommendation slot, in-stream native unit. Buyer ships a single asset bundle; the surface chooses presentation.\n\n**Scope (normative — buyer-agent routing).** This canonical is the home for:\n- IAB OpenRTB Native 1.2 in-feed native ads (publisher feeds, app feeds)\n- Content-recommendation widgets (Taboola, Outbrain, Yahoo Recommendations)\n- AdMob Native / Yahoo Native publisher slots\n- In-feed sponsored placements without catalog dependency\n\n**Not this canonical:**\n- Catalog-driven retail-media (Amazon SP, Criteo SP, CitrusAd SP) — use `sponsored_placement` (requires `source_catalog`).\n- Algorithmic surface that picks from a buyer-supplied asset pool (Google PMax, Meta Advantage+) — use `responsive_creative`.\n- Multi-card carousel — use `image_carousel`.\n- Video-first native units where the asset is a hosted video file — use `video_hosted` with `applies_to_channels: [\"native\"]`. (Distinct from the CTV menu profile: a menu hero remains this canonical because the platform assembles the full asset bundle and the video rides the Native 1.2 `vasttag` video asset, playing on focus rather than being the unit itself.)\n\nDistinct from `sponsored_placement` along the catalog axis: native_in_feed is asset-bundle composition; sponsored_placement is catalog-row composition. A buyer agent reading `format_kind: native_in_feed` knows to assemble title + image + body + CTA; reading `format_kind: sponsored_placement` knows to attach a catalog feed.") }), z.object({ "format_kind": z.literal("responsive_creative"), "params": z.object({ "experimental": z.boolean().describe("When true, this canonical or seller narrowing may not work as declared. Adopters SHOULD preflight it with validate_input or in a sandbox and SHOULD NOT route production budget without testing; experimental status never makes the deprecated v1 path preferable. Drivers include unsettled spec shape, an adopter runtime gap, and custom shapes awaiting promotion. This replaces the earlier status plus runtime_status axes. Sellers SHOULD set experimental whenever a canonical or declaration is not production-ready.").default(false), "deprecated": z.boolean().describe("When true, this canonical (or a seller's specific narrowing of it) is going away. Existing adopters are supported through the deprecation cycle; new adoption is discouraged. Pair with `migration_target_version` to indicate when the canonical is expected to be removed. Distinct from `experimental`: an experimental canonical may stabilize and stop being experimental; a deprecated canonical is on a sunset path.").default(false), "v1_translatable": z.boolean().describe("Whether this canonical has any v1 named-format equivalent. `true` (default) — the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1→v2 projection via `v1-canonical-mapping.json` is meaningful. `false` — the canonical is inherently new in v2 and has no v1 form; v1's `list_creative_formats` couldn't express it because the underlying concept (algorithmic surface composition, AI-surface mentions, retail-media catalog placements, multi-card carousels) didn't exist as a v1 named-format archetype.\n\nLets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The six inherently-v2 canonicals in 3.2 are `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`, `seller_rendered_stateful_display`, and `coordinated_placements`.").default(true), "since_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version that introduced this canonical (e.g., '3.1', '3.2'). Lets adopters reason about minimum protocol version requirements when consuming a format declaration. Patch precision is intentionally rejected — canonicals are introduced at minor-version boundaries.").optional(), "migration_target_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version by which the working group expects this canonical to stabilize, surface a breaking revision, or (when `deprecated: true`) be removed. Patch precision is intentionally rejected — canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown').").optional(), "composition_model": z.enum(["deterministic","algorithmic"]).describe("Whether the surface composes deterministically (buyer can predict per-slot rendering — sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing — responsive_creative, agent_placement).").optional(), "provenance_required": z.boolean().describe("When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent.").optional(), "platform_extensions": z.array(z.object({ "uri": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL identifying the extension. `https://` is mandatory — `http://`, `file://`, `data:`, and other schemes are rejected at the schema layer (defense-in-depth on top of the fetch-contract normative rules). The URI base is the owning agent's URL; the path identifies the extension within that agent. Example: 'https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel'. The full fetch contract — SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds — is documented on `product-format-declaration.json#format_schema` and applies to ALL fetches of this reference shape regardless of whether the field is named `format_schema` (load-bearing for validation) or `platform_extensions` (informational); the *transport* rules are identical, only the *consumption* semantics differ."), "digest": z.string().regex(new RegExp("^sha256:[a-f0-9]{64}$")).describe("SHA-256 content digest of the extension definition (sha256:). Used to detect drift — if the agent revises the extension, the digest changes and cached definitions become invalid.") }).catchall(z.any()).describe("Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` — divergent digests in one response indicate producer-side error (e.g., concurrent extension revision mid-render). Buyers encountering divergent digests for the same URI MUST fail closed: treat all references to that URI as unresolved and surface a validation error rather than picking one branch silently. **Across responses**, digest divergence is normal — extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.")).describe("Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\n\n**Collision precedence (normative).** When two or more `platform_extensions[]` entries on the same declaration extend the same target (e.g., both extend `tracking`) with overlapping field names, **array order is authoritative — later entries override earlier ones on a per-field basis** (last-in-array-wins). SDKs MUST surface the overlap via the `errors[]` array on the `get_products` response with a structured code (`FORMAT_DECLARATION_DIVERGENT` is appropriate when the overlap appears across dual-emitted shapes; a producer-self-emitted overlap on a single declaration SHOULD use the same code with `error.details: { collision_kind: \"platform_extension_field\", target, overlapping_fields, winning_extension_uri }`). Producers SHOULD avoid the collision by emitting one extension per target or by partitioning fields across extensions; the deterministic precedence is for last-resort consistency across SDK implementations, not a sanctioned merging strategy.").optional(), "synthesis_nondeterministic": z.boolean().describe("When true, the format's production pipeline is genuinely nondeterministic — the platform cannot guarantee that synthesis from a given input set produces in-spec output. Veo / Sora / Runway-class generative video, and other AI-synthesis flows where output dimensions, duration, or quality vary per run. Implies a different validation contract: predictive `validate_input` is impossible; the platform's own post-synthesis QA loop applies; if the QA loop exhausts without producing a valid artifact, `build_creative` returns task_failed with a synthesis_failed reason. Distinct from `composition_model` (which describes how the surface composes per-slot rendering, not whether synthesis is deterministic). When false or absent, the format's production is predictable enough that `validate_input` can predict output properties from input properties.\n\n**Compatibility with `asset_source` / `item_production_model`**: `synthesis_nondeterministic: true` MAY pair with any of `seller_pre_rendered_from_brief`, `seller_human_designed`, or `agent_synthesized` (the QA loop is concept-level, not source-specific — 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.").default(false), "slots": z.array(z.object({ "asset_group_id": z.string().describe("Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings."), "asset_type": z.enum(["image","video","audio","text","markdown","url","html","css","javascript","vast","daast","webhook","brief","catalog","published_post","zip","card","object","pixel_tracker","vast_tracker","daast_tracker"]).describe("Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `published_post` is an existing-post reference asset, not uploaded media bytes and not a catalog row. `card` is the multi-card carousel element type (see card-asset.json). `pixel_tracker` / `vast_tracker` / `daast_tracker` are the renderer-fired measurement-tracker primitives — see `/schemas/core/assets/pixel-tracker-asset.json` and the VAST / DAAST tracker schemas. `object` is a last-resort fallback for structured non-asset inputs that don't fit any primitive asset_type — prefer specific types whenever possible."), "required": z.boolean().describe("Whether this slot is required for a valid manifest.").default(false), "min": z.number().int().gte(0).describe("Minimum count for repeatable / pool slots.").optional(), "max": z.number().int().gte(1).describe("Maximum count for repeatable / pool slots.").optional(), "max_chars": z.number().int().gte(1).describe("Per-slot character limit. Valid only when `asset_type` is `text`, `markdown`, or `brief`. Mutually exclusive with `max_size_kb` (which applies to binary asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "max_size_kb": z.number().int().gte(1).describe("Per-slot file size limit in kilobytes. Valid only when `asset_type` is `image`, `video`, `audio`, or `zip`. Mutually exclusive with `max_chars` (which applies to text asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Accepted intrinsic-pixel densities for this image-bearing slot. Valid when `asset_type` is `image`, and on a `card` slot where it constrains each card's image media (video media is unaffected). This makes density available to every canonical carrying image assets (native, carousel, responsive, companion images, and image itself), not only `format_kind: image`. When the image canonical also declares top-level `params.pixel_ratios`, the effective set is the intersection; an empty intersection is invalid. One matching asset satisfies the slot unless `required_pixel_ratios` requires rendition coverage.").optional(), "required_pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Required density coverage for an image rendition set. Valid only when `asset_type` is `image` and `pixel_ratios` is also declared. Every value MUST appear in the effective accepted set after intersecting any top-level image `params.pixel_ratios`, and the manifest slot value MUST be an array containing exactly one matching image rendition for each required ratio. Other accepted ratios remain optional. For example, `pixel_ratios: [1, 1.5, 2]` with `required_pixel_ratios: [1, 2]` requires the 1x and 2x renditions while making 1.5x optional. SDKs enforce intersection, subset, coverage, and duplicate-ratio rules because JSON Schema draft-07 cannot express them generically.").optional(), "logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("When `asset_group_id` is `logo`, renderer-facing brand.json logo slots acceptable for this format slot. Producers selecting from brand.json SHOULD prefer `logos[]` entries whose `slots[]` intersects this list, then apply `visual_guidelines.logo_usage_rules[]`.").optional(), "required_logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Subset of `logo_slots` for which this format expects explicit logo coverage. A manifest or brand-derived logo pool SHOULD include at least one usable logo for each required slot; if coverage is missing, builders SHOULD surface a validation warning or approval mapping instead of guessing from prose.").optional(), "description": z.string().describe("Human-readable description of what the slot expects from the buyer.").optional(), "consumed_for_production": z.boolean().describe("Dispatch hint for `build_creative` and v1↔v2 wire translators: when `true`, the slot's value is consumed as INPUT to a production step (host-read script, brief copy fed to generative synthesis, catalog feed driving per-SKU rendering) and is not rendered verbatim. When `false` (default), the slot's value is rendered verbatim on the placement (image bytes, video file, display tag).\n\nMotivates the v1↔v2 dispatch table: pre-v2 buyers shipped production-consumed inputs separately in a `inputs` map on the build_creative request; v2 collapses inputs and rendered assets into a single `assets` map keyed by `asset_group_id`. SDK translators between v1 and v2 use this flag per canonical to know which assets in the v2 manifest map back to v1 `inputs` vs v1 `assets`. Without the per-slot flag the dispatch table lives in adopter code and every SDK gets it slightly different.\n\nProducers SHOULD set this explicitly on slots whose consumption pattern isn't obvious (host-read scripts on `audio_hosted`, briefs on generative `video_hosted`, catalog feeds on `sponsored_placement`). For canonicals where every slot is render-verbatim (`image`, `display_tag`, `video_vast`), the default `false` is sufficient and the flag MAY be omitted.").default(false) }).catchall(z.any()).and(z.intersection(z.intersection(z.any(), z.intersection(z.any(), z.any())), z.intersection(z.any(), z.intersection(z.any(), z.any()))))).describe("Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.").optional(), "required_connections": z.array(z.object({ "provider": z.string().describe("Stable provider or platform namespace, preferably lowercase. Examples: `social.example`, `shortvideo.example`, or a seller-defined namespace. Omit only when the requirement is provider-agnostic, or when an `authorization_url` fully routes the human to the correct provider-specific connection flow.").optional(), "connection_type": z.enum(["advertiser_account","publisher_identity","post_authorization"]).describe("Kind of downstream connection required. `advertiser_account` is the platform account used to buy/manage ads. `publisher_identity` is the creator, page, channel, organization, or profile that owns source posts. `post_authorization` is a post-scoped grant when the platform authorizes individual posts instead of, or in addition to, the owning identity."), "required_for": z.array(z.string().min(1)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Concrete AdCP protocol operation names that require this downstream connection. Sellers SHOULD include this in product declarations when the requirement is known ahead of time, and in AUTHORIZATION_REQUIRED details when it explains the failed operation. Prefer specific operation names such as `list_creatives`, `sync_creatives`, `create_media_buy`, `get_media_buy_delivery`, or `get_creative_delivery` over broad category labels such as `reporting`.").optional(), "scope": z.enum(["account","identity","post","unknown"]).describe("Granularity of the downstream grant.").optional(), "status": z.enum(["connected","missing","pending","expired","revoked","not_required","unknown"]).describe("Current seller-observed state for this downstream connection when known. Product declarations MAY omit status or use `unknown`; AUTHORIZATION_REQUIRED details SHOULD use `missing`, `expired`, or `revoked` for the connection that blocked the call.").optional(), "connection_id": z.string().describe("Seller-defined identifier for an already-created downstream connection. Omit when no connection exists yet or when exposing it would leak platform/account state.").optional(), "resource_ref": z.object({ "platform_account_id": z.string().describe("Provider-native advertiser or business account id, when safe to disclose.").optional(), "identity_id": z.string().describe("Provider-native creator, page, channel, organization, or profile id, when safe to disclose.").optional(), "handle": z.string().describe("Provider-native public handle for the owning identity, when available.").optional(), "profile_url": z.string().url().describe("Public URL for the owning identity, when available.").optional(), "post_id": z.string().describe("Provider-native post id, when the grant is post-scoped or the failed request referenced a specific post.").optional(), "post_url": z.string().url().describe("Public URL for the referenced post, when available.").optional() }).catchall(z.any()).describe("Optional opaque provider-native resource hint, such as a platform account id, profile URL, handle, channel id, post id, or post URL. This is a hint for routing authorization, not proof that authorization exists.").optional(), "authorization_url": z.string().url().describe("Seller-hosted or provider-hosted URL where a human can complete or restore this downstream connection.").optional(), "authorization_instructions": z.string().describe("Human-readable instructions for completing or restoring this downstream connection.").optional(), "expires_at": z.string().datetime().describe("Expiration time for the downstream grant, when known.").optional() }).catchall(z.any()).and(z.any()).describe("A seller/platform-side connection or grant required by a product, format, or request. This is not the AdCP caller credential: the AdCP request is still authenticated once, and the seller uses these stored downstream connections to call a platform or service on the buyer's behalf. Use this shape for platforms that require more than one downstream grant, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.")).describe("Downstream platform connections or grants required to use this format declaration. These are in addition to the single AdCP caller credential. Use this when a platform product requires multiple downstream grants, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.").optional(), "reference_mutability": z.enum(["immutable_snapshot","mutable_requires_reapproval","mutable_auto_recheck"]).describe("Policy for formats whose `slots` accept a `published_post` reference. `immutable_snapshot`: seller snapshots the referenced post at approval and later source changes do not change the served creative. `mutable_requires_reapproval`: the source post may change and material changes require review before continued serving. `mutable_auto_recheck`: the source post may change and the seller continuously or periodically rechecks authorization/policy without requiring buyer resubmission. Omit when the format has no `published_post` slot.").optional(), "production_window_business_days": z.number().int().gte(0).describe("Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative).").optional() }).catchall(z.any()).describe("Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).").describe("Buyer supplies a pool of typed assets (multiple headlines, descriptions, images, videos, logos); the surface algorithmically composes combinations per placement. **Composition is algorithmic** — surface picks combinations and reports per-asset performance breakdowns. Covers Google Responsive Display Ads (RDA), Responsive Search Ads (RSA), Performance Max (PMax), Demand Gen, and Meta Advantage+ creative. Industry term: \"Responsive\" (Google) / \"Advantage+ creative\" (Meta) / \"Dynamic Creative\" (older Meta term). Distinct from `sponsored_placement` (catalog-driven, deterministic) and `agent_placement` (AI-surface composition). The structured `slots` field below enumerates expected canonical asset_group_id slots; per-slot count/length narrowing lives in flat parameters (`headlines_min`, `headline_max_chars`, etc.).") }), z.object({ "format_kind": z.literal("agent_placement"), "params": z.object({ "experimental": z.boolean().describe("When true, this canonical or seller narrowing may not work as declared. Adopters SHOULD preflight it with validate_input or in a sandbox and SHOULD NOT route production budget without testing; experimental status never makes the deprecated v1 path preferable. Drivers include unsettled spec shape, an adopter runtime gap, and custom shapes awaiting promotion. This replaces the earlier status plus runtime_status axes. Sellers SHOULD set experimental whenever a canonical or declaration is not production-ready.").default(false), "deprecated": z.boolean().describe("When true, this canonical (or a seller's specific narrowing of it) is going away. Existing adopters are supported through the deprecation cycle; new adoption is discouraged. Pair with `migration_target_version` to indicate when the canonical is expected to be removed. Distinct from `experimental`: an experimental canonical may stabilize and stop being experimental; a deprecated canonical is on a sunset path.").default(false), "v1_translatable": z.boolean().describe("Whether this canonical has any v1 named-format equivalent. `true` (default) — the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1→v2 projection via `v1-canonical-mapping.json` is meaningful. `false` — the canonical is inherently new in v2 and has no v1 form; v1's `list_creative_formats` couldn't express it because the underlying concept (algorithmic surface composition, AI-surface mentions, retail-media catalog placements, multi-card carousels) didn't exist as a v1 named-format archetype.\n\nLets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The six inherently-v2 canonicals in 3.2 are `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`, `seller_rendered_stateful_display`, and `coordinated_placements`.").default(true), "since_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version that introduced this canonical (e.g., '3.1', '3.2'). Lets adopters reason about minimum protocol version requirements when consuming a format declaration. Patch precision is intentionally rejected — canonicals are introduced at minor-version boundaries.").optional(), "migration_target_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version by which the working group expects this canonical to stabilize, surface a breaking revision, or (when `deprecated: true`) be removed. Patch precision is intentionally rejected — canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown').").optional(), "composition_model": z.enum(["deterministic","algorithmic"]).describe("Whether the surface composes deterministically (buyer can predict per-slot rendering — sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing — responsive_creative, agent_placement).").optional(), "provenance_required": z.boolean().describe("When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent.").optional(), "platform_extensions": z.array(z.object({ "uri": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL identifying the extension. `https://` is mandatory — `http://`, `file://`, `data:`, and other schemes are rejected at the schema layer (defense-in-depth on top of the fetch-contract normative rules). The URI base is the owning agent's URL; the path identifies the extension within that agent. Example: 'https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel'. The full fetch contract — SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds — is documented on `product-format-declaration.json#format_schema` and applies to ALL fetches of this reference shape regardless of whether the field is named `format_schema` (load-bearing for validation) or `platform_extensions` (informational); the *transport* rules are identical, only the *consumption* semantics differ."), "digest": z.string().regex(new RegExp("^sha256:[a-f0-9]{64}$")).describe("SHA-256 content digest of the extension definition (sha256:). Used to detect drift — if the agent revises the extension, the digest changes and cached definitions become invalid.") }).catchall(z.any()).describe("Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` — divergent digests in one response indicate producer-side error (e.g., concurrent extension revision mid-render). Buyers encountering divergent digests for the same URI MUST fail closed: treat all references to that URI as unresolved and surface a validation error rather than picking one branch silently. **Across responses**, digest divergence is normal — extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.")).describe("Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\n\n**Collision precedence (normative).** When two or more `platform_extensions[]` entries on the same declaration extend the same target (e.g., both extend `tracking`) with overlapping field names, **array order is authoritative — later entries override earlier ones on a per-field basis** (last-in-array-wins). SDKs MUST surface the overlap via the `errors[]` array on the `get_products` response with a structured code (`FORMAT_DECLARATION_DIVERGENT` is appropriate when the overlap appears across dual-emitted shapes; a producer-self-emitted overlap on a single declaration SHOULD use the same code with `error.details: { collision_kind: \"platform_extension_field\", target, overlapping_fields, winning_extension_uri }`). Producers SHOULD avoid the collision by emitting one extension per target or by partitioning fields across extensions; the deterministic precedence is for last-resort consistency across SDK implementations, not a sanctioned merging strategy.").optional(), "synthesis_nondeterministic": z.boolean().describe("When true, the format's production pipeline is genuinely nondeterministic — the platform cannot guarantee that synthesis from a given input set produces in-spec output. Veo / Sora / Runway-class generative video, and other AI-synthesis flows where output dimensions, duration, or quality vary per run. Implies a different validation contract: predictive `validate_input` is impossible; the platform's own post-synthesis QA loop applies; if the QA loop exhausts without producing a valid artifact, `build_creative` returns task_failed with a synthesis_failed reason. Distinct from `composition_model` (which describes how the surface composes per-slot rendering, not whether synthesis is deterministic). When false or absent, the format's production is predictable enough that `validate_input` can predict output properties from input properties.\n\n**Compatibility with `asset_source` / `item_production_model`**: `synthesis_nondeterministic: true` MAY pair with any of `seller_pre_rendered_from_brief`, `seller_human_designed`, or `agent_synthesized` (the QA loop is concept-level, not source-specific — 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.").default(false), "slots": z.array(z.object({ "asset_group_id": z.string().describe("Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings."), "asset_type": z.enum(["image","video","audio","text","markdown","url","html","css","javascript","vast","daast","webhook","brief","catalog","published_post","zip","card","object","pixel_tracker","vast_tracker","daast_tracker"]).describe("Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `published_post` is an existing-post reference asset, not uploaded media bytes and not a catalog row. `card` is the multi-card carousel element type (see card-asset.json). `pixel_tracker` / `vast_tracker` / `daast_tracker` are the renderer-fired measurement-tracker primitives — see `/schemas/core/assets/pixel-tracker-asset.json` and the VAST / DAAST tracker schemas. `object` is a last-resort fallback for structured non-asset inputs that don't fit any primitive asset_type — prefer specific types whenever possible."), "required": z.boolean().describe("Whether this slot is required for a valid manifest.").default(false), "min": z.number().int().gte(0).describe("Minimum count for repeatable / pool slots.").optional(), "max": z.number().int().gte(1).describe("Maximum count for repeatable / pool slots.").optional(), "max_chars": z.number().int().gte(1).describe("Per-slot character limit. Valid only when `asset_type` is `text`, `markdown`, or `brief`. Mutually exclusive with `max_size_kb` (which applies to binary asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "max_size_kb": z.number().int().gte(1).describe("Per-slot file size limit in kilobytes. Valid only when `asset_type` is `image`, `video`, `audio`, or `zip`. Mutually exclusive with `max_chars` (which applies to text asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Accepted intrinsic-pixel densities for this image-bearing slot. Valid when `asset_type` is `image`, and on a `card` slot where it constrains each card's image media (video media is unaffected). This makes density available to every canonical carrying image assets (native, carousel, responsive, companion images, and image itself), not only `format_kind: image`. When the image canonical also declares top-level `params.pixel_ratios`, the effective set is the intersection; an empty intersection is invalid. One matching asset satisfies the slot unless `required_pixel_ratios` requires rendition coverage.").optional(), "required_pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Required density coverage for an image rendition set. Valid only when `asset_type` is `image` and `pixel_ratios` is also declared. Every value MUST appear in the effective accepted set after intersecting any top-level image `params.pixel_ratios`, and the manifest slot value MUST be an array containing exactly one matching image rendition for each required ratio. Other accepted ratios remain optional. For example, `pixel_ratios: [1, 1.5, 2]` with `required_pixel_ratios: [1, 2]` requires the 1x and 2x renditions while making 1.5x optional. SDKs enforce intersection, subset, coverage, and duplicate-ratio rules because JSON Schema draft-07 cannot express them generically.").optional(), "logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("When `asset_group_id` is `logo`, renderer-facing brand.json logo slots acceptable for this format slot. Producers selecting from brand.json SHOULD prefer `logos[]` entries whose `slots[]` intersects this list, then apply `visual_guidelines.logo_usage_rules[]`.").optional(), "required_logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Subset of `logo_slots` for which this format expects explicit logo coverage. A manifest or brand-derived logo pool SHOULD include at least one usable logo for each required slot; if coverage is missing, builders SHOULD surface a validation warning or approval mapping instead of guessing from prose.").optional(), "description": z.string().describe("Human-readable description of what the slot expects from the buyer.").optional(), "consumed_for_production": z.boolean().describe("Dispatch hint for `build_creative` and v1↔v2 wire translators: when `true`, the slot's value is consumed as INPUT to a production step (host-read script, brief copy fed to generative synthesis, catalog feed driving per-SKU rendering) and is not rendered verbatim. When `false` (default), the slot's value is rendered verbatim on the placement (image bytes, video file, display tag).\n\nMotivates the v1↔v2 dispatch table: pre-v2 buyers shipped production-consumed inputs separately in a `inputs` map on the build_creative request; v2 collapses inputs and rendered assets into a single `assets` map keyed by `asset_group_id`. SDK translators between v1 and v2 use this flag per canonical to know which assets in the v2 manifest map back to v1 `inputs` vs v1 `assets`. Without the per-slot flag the dispatch table lives in adopter code and every SDK gets it slightly different.\n\nProducers SHOULD set this explicitly on slots whose consumption pattern isn't obvious (host-read scripts on `audio_hosted`, briefs on generative `video_hosted`, catalog feeds on `sponsored_placement`). For canonicals where every slot is render-verbatim (`image`, `display_tag`, `video_vast`), the default `false` is sufficient and the flag MAY be omitted.").default(false) }).catchall(z.any()).and(z.intersection(z.intersection(z.any(), z.intersection(z.any(), z.any())), z.intersection(z.any(), z.intersection(z.any(), z.any()))))).describe("Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.").optional(), "required_connections": z.array(z.object({ "provider": z.string().describe("Stable provider or platform namespace, preferably lowercase. Examples: `social.example`, `shortvideo.example`, or a seller-defined namespace. Omit only when the requirement is provider-agnostic, or when an `authorization_url` fully routes the human to the correct provider-specific connection flow.").optional(), "connection_type": z.enum(["advertiser_account","publisher_identity","post_authorization"]).describe("Kind of downstream connection required. `advertiser_account` is the platform account used to buy/manage ads. `publisher_identity` is the creator, page, channel, organization, or profile that owns source posts. `post_authorization` is a post-scoped grant when the platform authorizes individual posts instead of, or in addition to, the owning identity."), "required_for": z.array(z.string().min(1)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Concrete AdCP protocol operation names that require this downstream connection. Sellers SHOULD include this in product declarations when the requirement is known ahead of time, and in AUTHORIZATION_REQUIRED details when it explains the failed operation. Prefer specific operation names such as `list_creatives`, `sync_creatives`, `create_media_buy`, `get_media_buy_delivery`, or `get_creative_delivery` over broad category labels such as `reporting`.").optional(), "scope": z.enum(["account","identity","post","unknown"]).describe("Granularity of the downstream grant.").optional(), "status": z.enum(["connected","missing","pending","expired","revoked","not_required","unknown"]).describe("Current seller-observed state for this downstream connection when known. Product declarations MAY omit status or use `unknown`; AUTHORIZATION_REQUIRED details SHOULD use `missing`, `expired`, or `revoked` for the connection that blocked the call.").optional(), "connection_id": z.string().describe("Seller-defined identifier for an already-created downstream connection. Omit when no connection exists yet or when exposing it would leak platform/account state.").optional(), "resource_ref": z.object({ "platform_account_id": z.string().describe("Provider-native advertiser or business account id, when safe to disclose.").optional(), "identity_id": z.string().describe("Provider-native creator, page, channel, organization, or profile id, when safe to disclose.").optional(), "handle": z.string().describe("Provider-native public handle for the owning identity, when available.").optional(), "profile_url": z.string().url().describe("Public URL for the owning identity, when available.").optional(), "post_id": z.string().describe("Provider-native post id, when the grant is post-scoped or the failed request referenced a specific post.").optional(), "post_url": z.string().url().describe("Public URL for the referenced post, when available.").optional() }).catchall(z.any()).describe("Optional opaque provider-native resource hint, such as a platform account id, profile URL, handle, channel id, post id, or post URL. This is a hint for routing authorization, not proof that authorization exists.").optional(), "authorization_url": z.string().url().describe("Seller-hosted or provider-hosted URL where a human can complete or restore this downstream connection.").optional(), "authorization_instructions": z.string().describe("Human-readable instructions for completing or restoring this downstream connection.").optional(), "expires_at": z.string().datetime().describe("Expiration time for the downstream grant, when known.").optional() }).catchall(z.any()).and(z.any()).describe("A seller/platform-side connection or grant required by a product, format, or request. This is not the AdCP caller credential: the AdCP request is still authenticated once, and the seller uses these stored downstream connections to call a platform or service on the buyer's behalf. Use this shape for platforms that require more than one downstream grant, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.")).describe("Downstream platform connections or grants required to use this format declaration. These are in addition to the single AdCP caller credential. Use this when a platform product requires multiple downstream grants, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.").optional(), "reference_mutability": z.enum(["immutable_snapshot","mutable_requires_reapproval","mutable_auto_recheck"]).describe("Policy for formats whose `slots` accept a `published_post` reference. `immutable_snapshot`: seller snapshots the referenced post at approval and later source changes do not change the served creative. `mutable_requires_reapproval`: the source post may change and material changes require review before continued serving. `mutable_auto_recheck`: the source post may change and the seller continuously or periodically rechecks authorization/policy without requiring buyer resubmission. Omit when the format has no `published_post` slot.").optional(), "production_window_business_days": z.number().int().gte(0).describe("Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative).").optional() }).catchall(z.any()).describe("Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).").describe("**3.2-track canonical.** The structural shape (algorithmic composition + brand-context input + optional offering/landing_page) is captured here so adopters can declare against it in 3.1 catalogs, but the **mention-level tracking contract is intentionally underspecified for 3.1**: no normative macro vocabulary, no postback shape, no cross-surface dedup model. Adopters claiming `agent_placement` in 3.1 ship private tracking integrations and SHOULD leave `experimental: true` on the product declaration that references this canonical; buyer agents MUST treat agent_placement attribution as adapter-defined until the 3.2 tracking-macro spec lands. The canonical promotes to a normatively-buyer-callable surface in 3.2 (or later) once the tracking contract is specified.\n\nSponsored placement integrated into an AI-surface's response to a user. Buyer supplies a `BrandRef` (resolving brand.json for context), an optional `offering_ref` to focus the mention on a specific offering, and an optional `landing_page_url` the surface MAY attach as a citation. The surface (LLM, voice assistant, sponsored-search ranker) composes a natural-language mention, sponsored card, or audio snippet within its response to a user query. **Composition is algorithmic** — the agent chooses phrasing and presentation. Output asset_type varies by surface: `text` for chat UIs and sponsored search snippets; `audio` (synthesized) for voice assistants; `card` for structured AI-surface result cards. Tracking model: mention-level impression + attribution events; per-mention id keys back to brand and offering — but see the 3.2-track note above; the wire shape of these events is not yet specified. Distinct from `si_chat` (which is the user-converses-with-brand's-agent pattern — brand owns the conversational surface) and from `sponsored_placement` (retail-media catalog-driven). Parallels `sponsored_placement` structurally: both are surface-composed placements; agent_placement is for AI/agentic surfaces, sponsored_placement is for retail media.") }), z.object({ "format_kind": z.literal("seller_rendered_stateful_display"), "params": z.object({ "experimental": z.boolean().describe("When true, this canonical or seller narrowing may not work as declared. Adopters SHOULD preflight it with validate_input or in a sandbox and SHOULD NOT route production budget without testing; experimental status never makes the deprecated v1 path preferable. Drivers include unsettled spec shape, an adopter runtime gap, and custom shapes awaiting promotion. This replaces the earlier status plus runtime_status axes. Sellers SHOULD set experimental whenever a canonical or declaration is not production-ready.").default(false), "deprecated": z.boolean().describe("When true, this canonical (or a seller's specific narrowing of it) is going away. Existing adopters are supported through the deprecation cycle; new adoption is discouraged. Pair with `migration_target_version` to indicate when the canonical is expected to be removed. Distinct from `experimental`: an experimental canonical may stabilize and stop being experimental; a deprecated canonical is on a sunset path.").default(false), "v1_translatable": z.boolean().describe("Whether this canonical has any v1 named-format equivalent. `true` (default) — the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1→v2 projection via `v1-canonical-mapping.json` is meaningful. `false` — the canonical is inherently new in v2 and has no v1 form; v1's `list_creative_formats` couldn't express it because the underlying concept (algorithmic surface composition, AI-surface mentions, retail-media catalog placements, multi-card carousels) didn't exist as a v1 named-format archetype.\n\nLets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The six inherently-v2 canonicals in 3.2 are `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`, `seller_rendered_stateful_display`, and `coordinated_placements`.").default(true), "since_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version that introduced this canonical (e.g., '3.1', '3.2'). Lets adopters reason about minimum protocol version requirements when consuming a format declaration. Patch precision is intentionally rejected — canonicals are introduced at minor-version boundaries.").optional(), "migration_target_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version by which the working group expects this canonical to stabilize, surface a breaking revision, or (when `deprecated: true`) be removed. Patch precision is intentionally rejected — canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown').").optional(), "composition_model": z.enum(["deterministic","algorithmic"]).describe("Whether the surface composes deterministically (buyer can predict per-slot rendering — sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing — responsive_creative, agent_placement).").optional(), "provenance_required": z.boolean().describe("When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent.").optional(), "platform_extensions": z.array(z.object({ "uri": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL identifying the extension. `https://` is mandatory — `http://`, `file://`, `data:`, and other schemes are rejected at the schema layer (defense-in-depth on top of the fetch-contract normative rules). The URI base is the owning agent's URL; the path identifies the extension within that agent. Example: 'https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel'. The full fetch contract — SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds — is documented on `product-format-declaration.json#format_schema` and applies to ALL fetches of this reference shape regardless of whether the field is named `format_schema` (load-bearing for validation) or `platform_extensions` (informational); the *transport* rules are identical, only the *consumption* semantics differ."), "digest": z.string().regex(new RegExp("^sha256:[a-f0-9]{64}$")).describe("SHA-256 content digest of the extension definition (sha256:). Used to detect drift — if the agent revises the extension, the digest changes and cached definitions become invalid.") }).catchall(z.any()).describe("Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` — divergent digests in one response indicate producer-side error (e.g., concurrent extension revision mid-render). Buyers encountering divergent digests for the same URI MUST fail closed: treat all references to that URI as unresolved and surface a validation error rather than picking one branch silently. **Across responses**, digest divergence is normal — extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.")).describe("Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\n\n**Collision precedence (normative).** When two or more `platform_extensions[]` entries on the same declaration extend the same target (e.g., both extend `tracking`) with overlapping field names, **array order is authoritative — later entries override earlier ones on a per-field basis** (last-in-array-wins). SDKs MUST surface the overlap via the `errors[]` array on the `get_products` response with a structured code (`FORMAT_DECLARATION_DIVERGENT` is appropriate when the overlap appears across dual-emitted shapes; a producer-self-emitted overlap on a single declaration SHOULD use the same code with `error.details: { collision_kind: \"platform_extension_field\", target, overlapping_fields, winning_extension_uri }`). Producers SHOULD avoid the collision by emitting one extension per target or by partitioning fields across extensions; the deterministic precedence is for last-resort consistency across SDK implementations, not a sanctioned merging strategy.").optional(), "synthesis_nondeterministic": z.boolean().describe("When true, the format's production pipeline is genuinely nondeterministic — the platform cannot guarantee that synthesis from a given input set produces in-spec output. Veo / Sora / Runway-class generative video, and other AI-synthesis flows where output dimensions, duration, or quality vary per run. Implies a different validation contract: predictive `validate_input` is impossible; the platform's own post-synthesis QA loop applies; if the QA loop exhausts without producing a valid artifact, `build_creative` returns task_failed with a synthesis_failed reason. Distinct from `composition_model` (which describes how the surface composes per-slot rendering, not whether synthesis is deterministic). When false or absent, the format's production is predictable enough that `validate_input` can predict output properties from input properties.\n\n**Compatibility with `asset_source` / `item_production_model`**: `synthesis_nondeterministic: true` MAY pair with any of `seller_pre_rendered_from_brief`, `seller_human_designed`, or `agent_synthesized` (the QA loop is concept-level, not source-specific — 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.").default(false), "slots": z.array(z.object({ "asset_group_id": z.string().describe("Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings."), "asset_type": z.enum(["image","video","audio","text","markdown","url","html","css","javascript","vast","daast","webhook","brief","catalog","published_post","zip","card","object","pixel_tracker","vast_tracker","daast_tracker"]).describe("Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `published_post` is an existing-post reference asset, not uploaded media bytes and not a catalog row. `card` is the multi-card carousel element type (see card-asset.json). `pixel_tracker` / `vast_tracker` / `daast_tracker` are the renderer-fired measurement-tracker primitives — see `/schemas/core/assets/pixel-tracker-asset.json` and the VAST / DAAST tracker schemas. `object` is a last-resort fallback for structured non-asset inputs that don't fit any primitive asset_type — prefer specific types whenever possible."), "required": z.boolean().describe("Whether this slot is required for a valid manifest.").default(false), "min": z.number().int().gte(0).describe("Minimum count for repeatable / pool slots.").optional(), "max": z.number().int().gte(1).describe("Maximum count for repeatable / pool slots.").optional(), "max_chars": z.number().int().gte(1).describe("Per-slot character limit. Valid only when `asset_type` is `text`, `markdown`, or `brief`. Mutually exclusive with `max_size_kb` (which applies to binary asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "max_size_kb": z.number().int().gte(1).describe("Per-slot file size limit in kilobytes. Valid only when `asset_type` is `image`, `video`, `audio`, or `zip`. Mutually exclusive with `max_chars` (which applies to text asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Accepted intrinsic-pixel densities for this image-bearing slot. Valid when `asset_type` is `image`, and on a `card` slot where it constrains each card's image media (video media is unaffected). This makes density available to every canonical carrying image assets (native, carousel, responsive, companion images, and image itself), not only `format_kind: image`. When the image canonical also declares top-level `params.pixel_ratios`, the effective set is the intersection; an empty intersection is invalid. One matching asset satisfies the slot unless `required_pixel_ratios` requires rendition coverage.").optional(), "required_pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Required density coverage for an image rendition set. Valid only when `asset_type` is `image` and `pixel_ratios` is also declared. Every value MUST appear in the effective accepted set after intersecting any top-level image `params.pixel_ratios`, and the manifest slot value MUST be an array containing exactly one matching image rendition for each required ratio. Other accepted ratios remain optional. For example, `pixel_ratios: [1, 1.5, 2]` with `required_pixel_ratios: [1, 2]` requires the 1x and 2x renditions while making 1.5x optional. SDKs enforce intersection, subset, coverage, and duplicate-ratio rules because JSON Schema draft-07 cannot express them generically.").optional(), "logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("When `asset_group_id` is `logo`, renderer-facing brand.json logo slots acceptable for this format slot. Producers selecting from brand.json SHOULD prefer `logos[]` entries whose `slots[]` intersects this list, then apply `visual_guidelines.logo_usage_rules[]`.").optional(), "required_logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Subset of `logo_slots` for which this format expects explicit logo coverage. A manifest or brand-derived logo pool SHOULD include at least one usable logo for each required slot; if coverage is missing, builders SHOULD surface a validation warning or approval mapping instead of guessing from prose.").optional(), "description": z.string().describe("Human-readable description of what the slot expects from the buyer.").optional(), "consumed_for_production": z.boolean().describe("Dispatch hint for `build_creative` and v1↔v2 wire translators: when `true`, the slot's value is consumed as INPUT to a production step (host-read script, brief copy fed to generative synthesis, catalog feed driving per-SKU rendering) and is not rendered verbatim. When `false` (default), the slot's value is rendered verbatim on the placement (image bytes, video file, display tag).\n\nMotivates the v1↔v2 dispatch table: pre-v2 buyers shipped production-consumed inputs separately in a `inputs` map on the build_creative request; v2 collapses inputs and rendered assets into a single `assets` map keyed by `asset_group_id`. SDK translators between v1 and v2 use this flag per canonical to know which assets in the v2 manifest map back to v1 `inputs` vs v1 `assets`. Without the per-slot flag the dispatch table lives in adopter code and every SDK gets it slightly different.\n\nProducers SHOULD set this explicitly on slots whose consumption pattern isn't obvious (host-read scripts on `audio_hosted`, briefs on generative `video_hosted`, catalog feeds on `sponsored_placement`). For canonicals where every slot is render-verbatim (`image`, `display_tag`, `video_vast`), the default `false` is sufficient and the flag MAY be omitted.").default(false) }).catchall(z.any()).and(z.intersection(z.intersection(z.any(), z.intersection(z.any(), z.any())), z.intersection(z.any(), z.intersection(z.any(), z.any()))))).describe("Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.").optional(), "required_connections": z.array(z.object({ "provider": z.string().describe("Stable provider or platform namespace, preferably lowercase. Examples: `social.example`, `shortvideo.example`, or a seller-defined namespace. Omit only when the requirement is provider-agnostic, or when an `authorization_url` fully routes the human to the correct provider-specific connection flow.").optional(), "connection_type": z.enum(["advertiser_account","publisher_identity","post_authorization"]).describe("Kind of downstream connection required. `advertiser_account` is the platform account used to buy/manage ads. `publisher_identity` is the creator, page, channel, organization, or profile that owns source posts. `post_authorization` is a post-scoped grant when the platform authorizes individual posts instead of, or in addition to, the owning identity."), "required_for": z.array(z.string().min(1)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Concrete AdCP protocol operation names that require this downstream connection. Sellers SHOULD include this in product declarations when the requirement is known ahead of time, and in AUTHORIZATION_REQUIRED details when it explains the failed operation. Prefer specific operation names such as `list_creatives`, `sync_creatives`, `create_media_buy`, `get_media_buy_delivery`, or `get_creative_delivery` over broad category labels such as `reporting`.").optional(), "scope": z.enum(["account","identity","post","unknown"]).describe("Granularity of the downstream grant.").optional(), "status": z.enum(["connected","missing","pending","expired","revoked","not_required","unknown"]).describe("Current seller-observed state for this downstream connection when known. Product declarations MAY omit status or use `unknown`; AUTHORIZATION_REQUIRED details SHOULD use `missing`, `expired`, or `revoked` for the connection that blocked the call.").optional(), "connection_id": z.string().describe("Seller-defined identifier for an already-created downstream connection. Omit when no connection exists yet or when exposing it would leak platform/account state.").optional(), "resource_ref": z.object({ "platform_account_id": z.string().describe("Provider-native advertiser or business account id, when safe to disclose.").optional(), "identity_id": z.string().describe("Provider-native creator, page, channel, organization, or profile id, when safe to disclose.").optional(), "handle": z.string().describe("Provider-native public handle for the owning identity, when available.").optional(), "profile_url": z.string().url().describe("Public URL for the owning identity, when available.").optional(), "post_id": z.string().describe("Provider-native post id, when the grant is post-scoped or the failed request referenced a specific post.").optional(), "post_url": z.string().url().describe("Public URL for the referenced post, when available.").optional() }).catchall(z.any()).describe("Optional opaque provider-native resource hint, such as a platform account id, profile URL, handle, channel id, post id, or post URL. This is a hint for routing authorization, not proof that authorization exists.").optional(), "authorization_url": z.string().url().describe("Seller-hosted or provider-hosted URL where a human can complete or restore this downstream connection.").optional(), "authorization_instructions": z.string().describe("Human-readable instructions for completing or restoring this downstream connection.").optional(), "expires_at": z.string().datetime().describe("Expiration time for the downstream grant, when known.").optional() }).catchall(z.any()).and(z.any()).describe("A seller/platform-side connection or grant required by a product, format, or request. This is not the AdCP caller credential: the AdCP request is still authenticated once, and the seller uses these stored downstream connections to call a platform or service on the buyer's behalf. Use this shape for platforms that require more than one downstream grant, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.")).describe("Downstream platform connections or grants required to use this format declaration. These are in addition to the single AdCP caller credential. Use this when a platform product requires multiple downstream grants, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.").optional(), "reference_mutability": z.enum(["immutable_snapshot","mutable_requires_reapproval","mutable_auto_recheck"]).describe("Policy for formats whose `slots` accept a `published_post` reference. `immutable_snapshot`: seller snapshots the referenced post at approval and later source changes do not change the served creative. `mutable_requires_reapproval`: the source post may change and material changes require review before continued serving. `mutable_auto_recheck`: the source post may change and the seller continuously or periodically rechecks authorization/policy without requiring buyer resubmission. Omit when the format has no `published_post` slot.").optional(), "production_window_business_days": z.number().int().gte(0).describe("Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative).").optional() }).catchall(z.any()).describe("Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).").describe("Seller-rendered display unit whose declaration is an executable template contract: buyer-known visual states, explicit transitions, breakpoint canvases, and per-state slot bindings. The seller owns the runtime; `supply_mode` declares which end the buyer feeds. For machine-rendered `components` and `rendered_canvases` supply, sellers MUST support `preview_creative` returning every state × breakpoint from a candidate manifest. `layered_source` instead follows the asynchronous seller-production preview path after the declared production window. `composition_model: deterministic` describes serving the finished states, not instant derivation from layered source. Buyer-executable HTML/MRAID is `html5`, a buyer-delivered tag is `display_tag`, arbitrary games/hotspots/scripts remain `custom`, and per-impression algorithmic assembly is `responsive_creative`.") }), z.object({ "format_kind": z.literal("coordinated_placements"), "params": z.object({ "experimental": z.boolean().describe("When true, this canonical or seller narrowing may not work as declared. Adopters SHOULD preflight it with validate_input or in a sandbox and SHOULD NOT route production budget without testing; experimental status never makes the deprecated v1 path preferable. Drivers include unsettled spec shape, an adopter runtime gap, and custom shapes awaiting promotion. This replaces the earlier status plus runtime_status axes. Sellers SHOULD set experimental whenever a canonical or declaration is not production-ready.").default(false), "deprecated": z.boolean().describe("When true, this canonical (or a seller's specific narrowing of it) is going away. Existing adopters are supported through the deprecation cycle; new adoption is discouraged. Pair with `migration_target_version` to indicate when the canonical is expected to be removed. Distinct from `experimental`: an experimental canonical may stabilize and stop being experimental; a deprecated canonical is on a sunset path.").default(false), "v1_translatable": z.boolean().describe("Whether this canonical has any v1 named-format equivalent. `true` (default) — the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1→v2 projection via `v1-canonical-mapping.json` is meaningful. `false` — the canonical is inherently new in v2 and has no v1 form; v1's `list_creative_formats` couldn't express it because the underlying concept (algorithmic surface composition, AI-surface mentions, retail-media catalog placements, multi-card carousels) didn't exist as a v1 named-format archetype.\n\nLets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The six inherently-v2 canonicals in 3.2 are `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`, `seller_rendered_stateful_display`, and `coordinated_placements`.").default(true), "since_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version that introduced this canonical (e.g., '3.1', '3.2'). Lets adopters reason about minimum protocol version requirements when consuming a format declaration. Patch precision is intentionally rejected — canonicals are introduced at minor-version boundaries.").optional(), "migration_target_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version by which the working group expects this canonical to stabilize, surface a breaking revision, or (when `deprecated: true`) be removed. Patch precision is intentionally rejected — canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown').").optional(), "composition_model": z.enum(["deterministic","algorithmic"]).describe("Whether the surface composes deterministically (buyer can predict per-slot rendering — sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing — responsive_creative, agent_placement).").optional(), "provenance_required": z.boolean().describe("When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent.").optional(), "platform_extensions": z.array(z.object({ "uri": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL identifying the extension. `https://` is mandatory — `http://`, `file://`, `data:`, and other schemes are rejected at the schema layer (defense-in-depth on top of the fetch-contract normative rules). The URI base is the owning agent's URL; the path identifies the extension within that agent. Example: 'https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel'. The full fetch contract — SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds — is documented on `product-format-declaration.json#format_schema` and applies to ALL fetches of this reference shape regardless of whether the field is named `format_schema` (load-bearing for validation) or `platform_extensions` (informational); the *transport* rules are identical, only the *consumption* semantics differ."), "digest": z.string().regex(new RegExp("^sha256:[a-f0-9]{64}$")).describe("SHA-256 content digest of the extension definition (sha256:). Used to detect drift — if the agent revises the extension, the digest changes and cached definitions become invalid.") }).catchall(z.any()).describe("Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` — divergent digests in one response indicate producer-side error (e.g., concurrent extension revision mid-render). Buyers encountering divergent digests for the same URI MUST fail closed: treat all references to that URI as unresolved and surface a validation error rather than picking one branch silently. **Across responses**, digest divergence is normal — extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.")).describe("Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\n\n**Collision precedence (normative).** When two or more `platform_extensions[]` entries on the same declaration extend the same target (e.g., both extend `tracking`) with overlapping field names, **array order is authoritative — later entries override earlier ones on a per-field basis** (last-in-array-wins). SDKs MUST surface the overlap via the `errors[]` array on the `get_products` response with a structured code (`FORMAT_DECLARATION_DIVERGENT` is appropriate when the overlap appears across dual-emitted shapes; a producer-self-emitted overlap on a single declaration SHOULD use the same code with `error.details: { collision_kind: \"platform_extension_field\", target, overlapping_fields, winning_extension_uri }`). Producers SHOULD avoid the collision by emitting one extension per target or by partitioning fields across extensions; the deterministic precedence is for last-resort consistency across SDK implementations, not a sanctioned merging strategy.").optional(), "synthesis_nondeterministic": z.boolean().describe("When true, the format's production pipeline is genuinely nondeterministic — the platform cannot guarantee that synthesis from a given input set produces in-spec output. Veo / Sora / Runway-class generative video, and other AI-synthesis flows where output dimensions, duration, or quality vary per run. Implies a different validation contract: predictive `validate_input` is impossible; the platform's own post-synthesis QA loop applies; if the QA loop exhausts without producing a valid artifact, `build_creative` returns task_failed with a synthesis_failed reason. Distinct from `composition_model` (which describes how the surface composes per-slot rendering, not whether synthesis is deterministic). When false or absent, the format's production is predictable enough that `validate_input` can predict output properties from input properties.\n\n**Compatibility with `asset_source` / `item_production_model`**: `synthesis_nondeterministic: true` MAY pair with any of `seller_pre_rendered_from_brief`, `seller_human_designed`, or `agent_synthesized` (the QA loop is concept-level, not source-specific — 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.").default(false), "slots": z.array(z.object({ "asset_group_id": z.string().describe("Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings."), "asset_type": z.enum(["image","video","audio","text","markdown","url","html","css","javascript","vast","daast","webhook","brief","catalog","published_post","zip","card","object","pixel_tracker","vast_tracker","daast_tracker"]).describe("Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `published_post` is an existing-post reference asset, not uploaded media bytes and not a catalog row. `card` is the multi-card carousel element type (see card-asset.json). `pixel_tracker` / `vast_tracker` / `daast_tracker` are the renderer-fired measurement-tracker primitives — see `/schemas/core/assets/pixel-tracker-asset.json` and the VAST / DAAST tracker schemas. `object` is a last-resort fallback for structured non-asset inputs that don't fit any primitive asset_type — prefer specific types whenever possible."), "required": z.boolean().describe("Whether this slot is required for a valid manifest.").default(false), "min": z.number().int().gte(0).describe("Minimum count for repeatable / pool slots.").optional(), "max": z.number().int().gte(1).describe("Maximum count for repeatable / pool slots.").optional(), "max_chars": z.number().int().gte(1).describe("Per-slot character limit. Valid only when `asset_type` is `text`, `markdown`, or `brief`. Mutually exclusive with `max_size_kb` (which applies to binary asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "max_size_kb": z.number().int().gte(1).describe("Per-slot file size limit in kilobytes. Valid only when `asset_type` is `image`, `video`, `audio`, or `zip`. Mutually exclusive with `max_chars` (which applies to text asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Accepted intrinsic-pixel densities for this image-bearing slot. Valid when `asset_type` is `image`, and on a `card` slot where it constrains each card's image media (video media is unaffected). This makes density available to every canonical carrying image assets (native, carousel, responsive, companion images, and image itself), not only `format_kind: image`. When the image canonical also declares top-level `params.pixel_ratios`, the effective set is the intersection; an empty intersection is invalid. One matching asset satisfies the slot unless `required_pixel_ratios` requires rendition coverage.").optional(), "required_pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Required density coverage for an image rendition set. Valid only when `asset_type` is `image` and `pixel_ratios` is also declared. Every value MUST appear in the effective accepted set after intersecting any top-level image `params.pixel_ratios`, and the manifest slot value MUST be an array containing exactly one matching image rendition for each required ratio. Other accepted ratios remain optional. For example, `pixel_ratios: [1, 1.5, 2]` with `required_pixel_ratios: [1, 2]` requires the 1x and 2x renditions while making 1.5x optional. SDKs enforce intersection, subset, coverage, and duplicate-ratio rules because JSON Schema draft-07 cannot express them generically.").optional(), "logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("When `asset_group_id` is `logo`, renderer-facing brand.json logo slots acceptable for this format slot. Producers selecting from brand.json SHOULD prefer `logos[]` entries whose `slots[]` intersects this list, then apply `visual_guidelines.logo_usage_rules[]`.").optional(), "required_logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Subset of `logo_slots` for which this format expects explicit logo coverage. A manifest or brand-derived logo pool SHOULD include at least one usable logo for each required slot; if coverage is missing, builders SHOULD surface a validation warning or approval mapping instead of guessing from prose.").optional(), "description": z.string().describe("Human-readable description of what the slot expects from the buyer.").optional(), "consumed_for_production": z.boolean().describe("Dispatch hint for `build_creative` and v1↔v2 wire translators: when `true`, the slot's value is consumed as INPUT to a production step (host-read script, brief copy fed to generative synthesis, catalog feed driving per-SKU rendering) and is not rendered verbatim. When `false` (default), the slot's value is rendered verbatim on the placement (image bytes, video file, display tag).\n\nMotivates the v1↔v2 dispatch table: pre-v2 buyers shipped production-consumed inputs separately in a `inputs` map on the build_creative request; v2 collapses inputs and rendered assets into a single `assets` map keyed by `asset_group_id`. SDK translators between v1 and v2 use this flag per canonical to know which assets in the v2 manifest map back to v1 `inputs` vs v1 `assets`. Without the per-slot flag the dispatch table lives in adopter code and every SDK gets it slightly different.\n\nProducers SHOULD set this explicitly on slots whose consumption pattern isn't obvious (host-read scripts on `audio_hosted`, briefs on generative `video_hosted`, catalog feeds on `sponsored_placement`). For canonicals where every slot is render-verbatim (`image`, `display_tag`, `video_vast`), the default `false` is sufficient and the flag MAY be omitted.").default(false) }).catchall(z.any()).and(z.intersection(z.intersection(z.any(), z.intersection(z.any(), z.any())), z.intersection(z.any(), z.intersection(z.any(), z.any()))))).describe("Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.").optional(), "required_connections": z.array(z.object({ "provider": z.string().describe("Stable provider or platform namespace, preferably lowercase. Examples: `social.example`, `shortvideo.example`, or a seller-defined namespace. Omit only when the requirement is provider-agnostic, or when an `authorization_url` fully routes the human to the correct provider-specific connection flow.").optional(), "connection_type": z.enum(["advertiser_account","publisher_identity","post_authorization"]).describe("Kind of downstream connection required. `advertiser_account` is the platform account used to buy/manage ads. `publisher_identity` is the creator, page, channel, organization, or profile that owns source posts. `post_authorization` is a post-scoped grant when the platform authorizes individual posts instead of, or in addition to, the owning identity."), "required_for": z.array(z.string().min(1)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Concrete AdCP protocol operation names that require this downstream connection. Sellers SHOULD include this in product declarations when the requirement is known ahead of time, and in AUTHORIZATION_REQUIRED details when it explains the failed operation. Prefer specific operation names such as `list_creatives`, `sync_creatives`, `create_media_buy`, `get_media_buy_delivery`, or `get_creative_delivery` over broad category labels such as `reporting`.").optional(), "scope": z.enum(["account","identity","post","unknown"]).describe("Granularity of the downstream grant.").optional(), "status": z.enum(["connected","missing","pending","expired","revoked","not_required","unknown"]).describe("Current seller-observed state for this downstream connection when known. Product declarations MAY omit status or use `unknown`; AUTHORIZATION_REQUIRED details SHOULD use `missing`, `expired`, or `revoked` for the connection that blocked the call.").optional(), "connection_id": z.string().describe("Seller-defined identifier for an already-created downstream connection. Omit when no connection exists yet or when exposing it would leak platform/account state.").optional(), "resource_ref": z.object({ "platform_account_id": z.string().describe("Provider-native advertiser or business account id, when safe to disclose.").optional(), "identity_id": z.string().describe("Provider-native creator, page, channel, organization, or profile id, when safe to disclose.").optional(), "handle": z.string().describe("Provider-native public handle for the owning identity, when available.").optional(), "profile_url": z.string().url().describe("Public URL for the owning identity, when available.").optional(), "post_id": z.string().describe("Provider-native post id, when the grant is post-scoped or the failed request referenced a specific post.").optional(), "post_url": z.string().url().describe("Public URL for the referenced post, when available.").optional() }).catchall(z.any()).describe("Optional opaque provider-native resource hint, such as a platform account id, profile URL, handle, channel id, post id, or post URL. This is a hint for routing authorization, not proof that authorization exists.").optional(), "authorization_url": z.string().url().describe("Seller-hosted or provider-hosted URL where a human can complete or restore this downstream connection.").optional(), "authorization_instructions": z.string().describe("Human-readable instructions for completing or restoring this downstream connection.").optional(), "expires_at": z.string().datetime().describe("Expiration time for the downstream grant, when known.").optional() }).catchall(z.any()).and(z.any()).describe("A seller/platform-side connection or grant required by a product, format, or request. This is not the AdCP caller credential: the AdCP request is still authenticated once, and the seller uses these stored downstream connections to call a platform or service on the buyer's behalf. Use this shape for platforms that require more than one downstream grant, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.")).describe("Downstream platform connections or grants required to use this format declaration. These are in addition to the single AdCP caller credential. Use this when a platform product requires multiple downstream grants, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.").optional(), "reference_mutability": z.enum(["immutable_snapshot","mutable_requires_reapproval","mutable_auto_recheck"]).describe("Policy for formats whose `slots` accept a `published_post` reference. `immutable_snapshot`: seller snapshots the referenced post at approval and later source changes do not change the served creative. `mutable_requires_reapproval`: the source post may change and material changes require review before continued serving. `mutable_auto_recheck`: the source post may change and the seller continuously or periodically rechecks authorization/policy without requiring buyer resubmission. Omit when the format has no `published_post` slot.").optional(), "production_window_business_days": z.number().int().gte(0).describe("Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative).").optional() }).catchall(z.any()).describe("Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).").describe("One creative manifest atomically supplies assets for multiple declared product placements. Each component binds to a public `Product.placements[]` entry and either declares an inline non-custom canonical format or references a sibling format option on the same product. Components cannot nest coordinated placements. The manifest supplies component slots under `component_assets.`; `shared_slots` assets are supplied once at top level. Inventory exclusivity remains `Product.exclusivity`, not a creative-format parameter. Ordinary products whose placements accept independently assigned creatives do not need this canonical.") }), z.object({ "format_kind": z.literal("custom"), "params": z.record(z.string(), z.any()).describe("Custom shape's params. Validated against the schema fetched from `format_schema.uri` at the cached `format_schema.digest`.") }).describe("Adopter-defined shape that doesn't fit the 14 canonicals. Requires `format_shape` (vocabulary-registered global pattern) and `format_schema` (URI+digest reference to a fetchable schema describing the actual params/slots). `params` shape is governed by the fetched schema rather than baked into AdCP — kept as `type: object` here with `additionalProperties: true` because the canonical schema validates dynamically post-fetch.")])).and(z.intersection(z.intersection(z.any(), z.union([z.union([z.any(), z.any()]), z.any().refine((value) => !z.union([z.any(), z.any()]).safeParse(value).success, "Invalid input: Should NOT be valid against schema")]).superRefine((value,ctx) => { const result = z.any().safeParse(value).success ? z.union([z.any(), z.any()]).safeParse(value) : z.any().refine((value) => !z.union([z.any(), z.any()]).safeParse(value).success, "Invalid input: Should NOT be valid against schema").safeParse(value); if (!result.success) { result.error.issues.forEach((error: any) => ctx.addIssue(error)) } -})), z.intersection(z.any().refine((value) => !z.intersection(z.any(), z.any()).safeParse(value).success, "Invalid input: Should NOT be valid against schema"), z.intersection(z.any().refine((value) => !z.any().safeParse(value).success, "Invalid input: Should NOT be valid against schema"), z.any())))).describe("Inline 3.1+ canonical format-option declaration. Use when the placement narrows a format in a way that's not worth a top-level catalog entry.")])).describe("Optional 3.1+ canonical format-option declarations supported by this placement. This `adagents.json` placement surface supports two entry shapes: (1) reference an entry in the same file's top-level `formats[]` by `format_option_id` only — buyers resolve the full declaration from `formats[]` by matching `format_option_id` (recommended; avoids duplication). Top-level formats may be publisher-owned custom formats or narrowed canonical formats; their `format_kind` is the canonical anchor that the placement reference inherits. (2) carry an inline `ProductFormatDeclaration` directly — for placement-specific canonical narrowing that doesn't fit a reusable catalog entry. This bare-reference shape is placement-catalog specific; Product `format_options[]` entries are always full `ProductFormatDeclaration` objects with required `format_kind` and `params`.\n\nProduct-level formats remain the upper bound for a sellable product. Catalog placement formats describe placement support; when a product references the placement and also declares product-level formats, buyers use the intersection for that product placement. A catalog placement format that is absent from the product-level declaration is not accepted for that product unless the product explicitly includes it. When the product locale policy is absent, placement locale_policy may introduce any concrete narrowing; when both are present, every placement accepted_language_range must be contained by a product range under RFC 4647 Basic Filtering. An effective placement policy is canonical-only: its resolved catalog and matching product declarations cannot project through legacy format_ids.\n\n**Format-option reference shape.** A format-option reference entry SHOULD carry ONLY `format_option_id` — extra fields are allowed (`additionalProperties: true`) so adopters who want to attach a placement-local override like `display_name` or a narrower `locale_policy` don't get rejected by the branch boundary, but buyer SDKs MUST resolve the format from the top-level `formats[]` by `format_option_id` and apply additional fields on the entry as placement-level overrides (NOT as a partial inline declaration). If a publisher needs to materially narrow the format at the placement, use the inline-declaration form instead.\n\n**Resolution scope is same-file only.** `format_option_id` resolves only within this file's top-level `formats[]`; cross-file references are not supported by design because same-file resolution keeps validation bounded and prevents a file from squatting on or narrowing another publisher's format_option_id. When `format_options[]` references a `format_option_id` not declared in the file's top-level `formats[]`, validators MUST surface this as `FORMAT_OPTION_UNRESOLVED` on the response `errors[]`. Buyers MUST fail closed for that placement (drop the format from the placement's accepted set) rather than silently dropping the placement or guessing intent.").optional(), "video_placement_types": z.array(z.enum(["instream","accompanying_content","interstitial","standalone"]).describe("Declared video placement classification for OLV and other video inventory, using the IAB Tech Lab/OpenRTB 2.6 video.plcmt definitions with AdCP-native value names. This is seller-declared discovery metadata, not independent verification of inventory quality or delivery context.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Declared video placement types for this publisher placement, using IAB Tech Lab/OpenRTB 2.6 video.plcmt definitions with AdCP-native names. Most concrete placements SHOULD declare a single value; aggregate placements MAY declare multiple values. Product-level placement declarations may narrow this set but SHOULD NOT broaden it. This is seller-declared discovery metadata, not independent verification of inventory quality or delivery context.").optional(), "audio_distribution_types": z.array(z.enum(["music_streaming_service","fm_am_broadcast","podcast","catch_up_radio","web_radio","video_game","text_to_speech"]).describe("Declared audio distribution classification for radio, streaming-audio, podcast, and other audio inventory, using IAB Tech Lab/OpenRTB 2.6 audio.feed definitions with AdCP-native value names. This is seller-declared discovery metadata, not independent verification of inventory quality or delivery context.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Declared audio distribution types for this publisher placement, using IAB Tech Lab/OpenRTB 2.6 audio.feed definitions with AdCP-native names. Most concrete placements SHOULD declare a single value; aggregate placements MAY declare multiple values. Product-level placement declarations may narrow this set but SHOULD NOT broaden it. This is seller-declared discovery metadata, not independent verification of inventory quality or delivery context.").optional(), "sponsored_placement_types": z.array(z.enum(["sponsored_search","sponsored_display","sponsored_native"]).describe("Declared sponsored-placement classification for catalog-driven retail-media inventory, distinguishing where the sponsored placement renders on the retailer surface. This is seller-declared discovery metadata, not independent verification of inventory quality or delivery context.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Declared sponsored-placement types for this publisher placement, distinguishing where the catalog-driven retail-media placement renders on the retailer surface. Most concrete placements SHOULD declare a single value; aggregate placements MAY declare multiple values. Product-level placement declarations may narrow this set but SHOULD NOT broaden it. This is seller-declared discovery metadata, not independent verification of inventory quality or delivery context.").optional(), "social_placement_surfaces": z.array(z.enum(["feed","stories","short_video","explore","search"]).describe("Declared social-placement surface classification for social inventory, distinguishing the in-app surface where a social placement renders. Values name the semantic surface, not platform brands. This is seller-declared discovery metadata, not independent verification of inventory quality or delivery context.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Declared social-placement surfaces for this publisher placement, distinguishing the in-app surface where the social placement renders. Most concrete placements SHOULD declare a single value; aggregate placements MAY declare multiple values. Product-level placement declarations may narrow this set but SHOULD NOT broaden it. This is seller-declared discovery metadata, not independent verification of inventory quality or delivery context.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension object for platform-specific, vendor-namespaced parameters. Extensions are always optional and must be namespaced under a vendor/platform key (e.g., ext.gam, ext.roku). Used for custom capabilities, partner-specific configuration, and features being proposed for standardization.").optional() }).catchall(z.any()).and(z.union([z.any(), z.any()])).and(z.any().refine((value) => !z.union([z.any(), z.any(), z.any(), z.any(), z.any()]).safeParse(value).success, "Invalid input: Should NOT be valid against schema")).describe("Canonical placement definition published in a publisher's adagents.json. Defines stable placement IDs that products can reuse and that authorization rules can reference. When a product reuses a registered placement_id, it is referring to this same semantic placement, not inventing a new one with the same ID.")).describe("Canonical placement definitions for properties in this file. Products SHOULD reuse these placement_id values when exposing inventory in get_products, and authorized agents can scope authorization to these placement IDs.").optional(), "formats": z.array(z.intersection(z.object({ "format_option_id": z.string().describe("Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'.").optional(), "publisher_domain": z.string().regex(new RegExp("^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$")).describe("Namespace for `format_option_id` when this declaration references or narrows a publisher-declared format option from that publisher's adagents.json top-level `formats[]`. Product-local options omit this field and are selected by `format_option_id` within the target product.").optional(), "display_name": z.string().describe("Optional seller-controlled human-readable label for this format declaration. Used by buyer dashboards, catalog UIs, and reporting surfaces to show a seller's own naming ('Homepage Takeover', 'Branded Canvas', 'Reels Premium Video') rather than the raw `format_kind` or `format_option_id`. Has no machine semantics — buyer agents route on `format_kind` and `format_option_id`; `display_name` is purely for human presentation. Freeform; no enumeration. Sellers SHOULD keep it stable once published to avoid dashboard churn.").optional(), "sample_render_url": z.string().url().regex(new RegExp("^https://")).describe("Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.").optional(), "applies_to_channels": z.array(z.enum(["display","olv","social","search","ctv","linear_tv","radio","streaming_audio","podcast","dooh","ooh","print","cinema","email","gaming","retail_media","influencer","affiliate","product_placement","sponsored_intelligence"]).describe("Standardized advertising media channels describing how buyers allocate budget. Channels are planning abstractions, not technical substrates. See the Media Channel Taxonomy specification for detailed definitions.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Optional subset of the parent product's `channels` to which this declaration applies. When omitted, the declaration applies to ALL channels declared on the product. Lets a multi-channel product (e.g., `channels: ['display', 'video']`) carry distinct format_options per channel — `format_options: [{format_kind: 'image', applies_to_channels: ['display']}, {format_kind: 'video_hosted', applies_to_channels: ['video']}]`. Buyers ship channel-appropriate manifests per `applies_to_channels`.").optional(), "seller_preference": z.enum(["preferred","accepted","discouraged"]).describe("Optional soft routing hint *within* a product's accepted set of formats — NOT an enforcement axis. `preferred` — seller actively recommends this format (often because of measurement, viewability, or render-quality differences); `accepted` — supported on equal footing with other format_options (default when omitted); `discouraged` — supported but suboptimal (e.g., legacy 3p-tag where the seller would prefer html5 for OM-SDK coverage). Buyer agents picking between format_options SHOULD respect seller preferences when their own constraints don't override.\n\n**Not an enforcement axis (normative).** `seller_preference` does NOT carry the meaning of 'this format won't work / required-only'. That case is structural: `format_options[]` IS the closed set of accepted formats; anything outside the list is rejected at `create_media_buy` regardless of preference. A seller that accepts only one format lists exactly that one entry — the structural fact does the enforcement work, no enum value needed. There is intentionally no `required` value; preference is bounded to *ranking within the already-accepted set*, not gating into it.").optional(), "locale_policy": z.object({ "accepted_language_ranges": z.array(z.string().regex(new RegExp("^(?:[a-z]{2,8}(?:-[A-Z][a-z]{3})?(?:-(?:[A-Z]{2}|[0-9]{3}))?(?:-(?:[a-z0-9]{5,8}|[0-9][a-z0-9]{3}))*(?:-[0-9a-wy-z](?:-[a-z0-9]{2,8})+)*(?:-x(?:-[a-z0-9]{1,8})+)?|x(?:-[a-z0-9]{1,8})+)$")).min(2).max(63).describe("A well-formed BCP 47 language tag used by AdCP only as language identity. Script and region may refine that identity; other valid BCP 47 subtags remain part of tag matching but do not make this a general locale-settings object. It does not determine currency, time zone, number/date formatting, market, or legal jurisdiction. The AdCP canonical wire profile requires lower-case language and variants, title-case script, and upper-case region (for example `en-US`, `zh-Hant-TW`, or `x-private`). RFC 5646 comparisons are case-insensitive and its case regularization is optional; AdCP intentionally requires this stricter single wire spelling and receivers MUST reject differently cased tags rather than silently normalizing them. The schema pattern enforces the AdCP casing profile and extension structure for commonly used tags; conforming receivers additionally validate the complete RFC 5646 grammar and registry rules. Every new AdCP field carrying BCP 47 language identity or a concrete language range MUST reference this schema instead of declaring independent string constraints.")).max(50).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Concrete canonical BCP 47 language ranges accepted by this format option. RFC 4647 Basic Filtering is directional: seller range fr accepts variant fr-CA, but seller range fr-CA does not accept variant fr or fr-FR. Use zxx explicitly for language-neutral creative; und means unknown and is not a wildcard.") }).catchall(z.any()).describe("Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.").optional(), "canonical_formats_only": z.boolean().describe("When true, this format declaration has no clean v1 projection and SDKs MUST NOT synthesize a v1 `format_id` for it. Buyers reading the product on the v1 wire path see this declaration absent from `format_ids`; only v2-aware buyers (reading `format_options`) discover it. Set explicitly for `format_kind: \"custom\"` declarations (no canonical exists in v1 to project onto) and for declarations whose canonical/parameter shape cannot round-trip through a v1 named format without semantic loss. The protocol does NOT mint synthetic v1 format_ids for unmappable declarations — the alternative (an `aao-synth/*` namespace populated automatically) was considered and rejected because adopters would index on synthetic IDs that have no stable identity. Producers SHOULD set `canonical_formats_only: true` rather than omit the declaration from `format_options` — explicit v2-only is more useful than silent absence.").default(false), "experimental": z.boolean().describe("When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum.").default(false), "format_shape": z.string().describe("REQUIRED when `format_kind: \"custom\"`; otherwise MUST be absent. Recognized global pattern this custom shape is an instance of, drawn from the [format-shape vocabulary registry](/schemas/core/format-shape-vocabulary.json) (`multi_placement_takeover`, `roadblock`, `branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values valid (validators MAY soft-warn) — adopters CAN ship a shape that isn't yet in the registry. Adding entries is a vocabulary PR. Once a `format_shape` entry sees 2+ adopters with substantively similar `format_schema` content for 90+ days, the working group promotes it to a first-class canonical.").optional(), "v1_format_ref": z.array(z.object({ "agent_url": z.string().url().describe("URL of the agent that defines this format (e.g., 'https://creative.adcontextprotocol.org' for standard formats, or 'https://publisher.com/.well-known/adcp/sales' for custom formats). Callers comparing two `format-id` values MUST canonicalize `agent_url` per the AdCP URL canonicalization rules before treating two formats as the same. See docs/reference/url-canonicalization."), "id": z.string().regex(new RegExp("^[a-zA-Z0-9_-]+$")).describe("Format identifier within the agent's namespace (e.g., 'display_static', 'video_hosted', 'audio_standard'). When used alone, references a template format. When combined with dimension/duration fields, creates a parameterized format ID for a specific variant."), "width": z.number().int().gte(1).describe("Width in pixels for visual formats. When specified, height must also be specified. Both fields together create a parameterized format ID for dimension-specific variants.").optional(), "height": z.number().int().gte(1).describe("Height in pixels for visual formats. When specified, width must also be specified. Both fields together create a parameterized format ID for dimension-specific variants.").optional(), "duration_ms": z.number().gte(1).describe("Duration in milliseconds for time-based formats (video, audio). When specified, creates a parameterized format ID. Omit to reference a template format without parameters.").optional(), "pixel_ratio": z.number().gt(0).describe("Required intrinsic-pixel density for a parameterized visual format, expressed as intrinsic pixels per logical pixel. Requires `width` and `height`. Example: `{id: \"display_image\", width: 300, height: 250, pixel_ratio: 2}` identifies a 300×250 logical render supplied by a 600×500 image. Omit for the backward-compatible 1x variant.").optional() }).catchall(z.any()).describe("**DEPRECATED in 3.2.** Legacy named-format reference retained for 3.x compatibility. This JSON object — never a plain string — identifies a format by its declaring agent and local slug, and may carry legacy logical dimensions, pixel ratio, or duration parameters. New products and manifests use canonical `format_options`, `format_kind`, and `format_option_ref`; creative-agent build routing uses `creative.supported_formats[].capability_id`.")).describe("Authoritative v2 → v1 link, expressed as an array of one or more v1 `format_id` ({agent_url, id}) values. Each entry asserts that this canonical-formats declaration IS the same underlying format as the referenced v1 named format. Always an array (single-ref is `[{...}]`) so the multi-size case below has a clean wire shape — adopters surveyed in the SDK implementor review pushed for this over the lossy single-ref form.\n\nThe v2 declaration's `params` MUST narrow (be compatible with) each referenced v1 format's `requirements` — see the 'Narrows — formal definition' section in canonical-formats.mdx. SDKs comparing dual-emitted shapes (`Product.format_ids[]` ⊇ entries from `v1_format_ref` AND `Product.format_options[]` carrying this declaration) treat the link as the authoritative pairing and run the narrowing check between this declaration and EACH referenced v1 format file's `requirements`.\n\n**Multi-size fan-out (normative).** When the declaration carries `params.sizes: [{w,h}, ...]` (multi-size flexible slot), sellers SHOULD carry one `v1_format_ref[]` entry per size, each pointing at the per-size v1 named format in the AAO catalog. Example: a multi-size image declaration with `sizes: [300x250, 728x90, 970x250]` SHOULD carry `v1_format_ref: [{aao, display_300x250_image}, {aao, display_728x90_image}, {aao, display_970x250_image}]`. v1-only buyers then see the product on all three sizes via the `format_ids[]` dual-emission. When `v1_format_ref[]` count < `sizes[]` count, SDKs MUST emit `FORMAT_DECLARATION_V1_LOSSY_MULTI_SIZE` on the response `errors[]` (advisory, alongside the partial-coverage v1 emit — NOT in place of it). SDKs MAY (non-normative) fan out automatically by catalog lookup when `v1_format_ref[]` has length 1 and `sizes[]` has length N — opt-in, requires catalog access; sellers asserting refs is the source of truth.\n\nMutually exclusive with `canonical_formats_only: true` — a declaration can EITHER assert no v1 projection (`canonical_formats_only: true`) OR link to v1 named formats (`v1_format_ref[]`), never both. When neither is present, SDKs fall back to the resolution order in `v1-canonical-mapping.json` (seller's explicit `canonical` field on the v1 file → registry glob → structural match → fail-closed).\n\nThis is the v2-side authoritative replacement for the v1-side `canonical_parameters` field on `format.json` (which is deprecated for 3.1, removed at 4.0). Sellers SHOULD prefer authoring v2 declarations with `v1_format_ref[]` over mirroring the v2 shape onto v1 files via `canonical_parameters`; the directional link (v2 declaration → v1 identifiers) is the same fact without the parallel-shape drift surface.\n\n**AAO-hosted convention (normative).** For IAB-standard formats (image dimensions, VAST/DAAST tags, standard third-party tags, HTML5 banner bundles), sellers SHOULD point each `v1_format_ref[].agent_url` at the AAO-hosted canonical agent URL `https://creative.adcontextprotocol.org` and use the registry-published id (e.g., `display_300x250_image`, `video_vast_30s`, `audio_standard_30s`, `display_300x250_html`, `display_js`). This converges the v1-wire namespace: every seller's IAB MREC points at the same `{agent_url, id}` pair, so v1-only buyers' allowlists work uniformly. Without this convention, every publisher's 300x250 ships with a different `v1_format_ref` (theirs vs nytimes.example vs cnn.example vs …) and the v1 wire fragments into per-publisher namespaces — exactly what canonical-formats was designed to eliminate.\n\nFor platform-specific formats (Meta Reels, TikTok Spark, Snap Spotlight, etc.), each `v1_format_ref[].agent_url` SHOULD point at the platform's own agent_url when the platform has adopted AdCP and publishes its own `adagents.json` with `formats[]`. When the platform has NOT adopted AdCP, sellers SHOULD point at the AAO community-registry mirror — `https://creative.adcontextprotocol.org/translated/` + `id: ` (e.g., `https://creative.adcontextprotocol.org/translated/meta` + `id: meta_reels`). This keeps the v1 namespace converged across all sellers selling that platform's inventory until the platform owns its own adagents.json.\n\n**Platform-adoption cutover (normative).** When a platform adopts AdCP and publishes its own adagents.json, sellers MUST update `v1_format_ref[].agent_url` to the platform's adopted agent_url in the same minor release as the AAO mirror entry's `superseded_by` field goes live (see `static/schemas/source/adagents.json#superseded_by`). The AAO mirror entry SHOULD continue serving for ≥1 minor release after `superseded_by` is set, returning an advisory 'superseded' marker so v1 buyer allowlists keyed on the mirror URL get an explicit signal rather than a silent break. **Identity-confusion note**: the mirror URL is *format-shape namespace*, NOT seller identity. Inventory authorization always flows from `authorized_agents[]` + publisher signing keys; a buyer matching `v1_format_ref[].agent_url` against an allowlist is matching format-shape provenance, not seller identity.\n\n**Mirror domain migration (3.1).** Earlier drafts used `https://mirror.adcontextprotocol.org/translated/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — sibling content under the AAO catalog domain we already host. Adopters who hardcoded the earlier mirror URL MUST migrate to the new path; the canonical-formats.mdx migration section documents the move. No transitional redirect is currently published (the earlier subdomain was never provisioned).\n\nFor seller-bespoke formats (a publisher's `acme_homepage_takeover` that doesn't fit IAB conventions), each `v1_format_ref[].agent_url` is the seller's own agent_url and the id is seller-namespaced. These won't appear in `v1-canonical-mapping.json`'s registry; they're seller-asserted only.").optional(), "format_schema": z.object({ "uri": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL identifying the extension. `https://` is mandatory — `http://`, `file://`, `data:`, and other schemes are rejected at the schema layer (defense-in-depth on top of the fetch-contract normative rules). The URI base is the owning agent's URL; the path identifies the extension within that agent. Example: 'https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel'. The full fetch contract — SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds — is documented on `product-format-declaration.json#format_schema` and applies to ALL fetches of this reference shape regardless of whether the field is named `format_schema` (load-bearing for validation) or `platform_extensions` (informational); the *transport* rules are identical, only the *consumption* semantics differ."), "digest": z.string().regex(new RegExp("^sha256:[a-f0-9]{64}$")).describe("SHA-256 content digest of the extension definition (sha256:). Used to detect drift — if the agent revises the extension, the digest changes and cached definitions become invalid.") }).catchall(z.any()).describe("REQUIRED when `format_kind: \"custom\"`; otherwise MUST be absent. URI+digest reference to a fetchable schema describing this custom shape's actual `params` and `slots`. Same hosting model as `platform_extensions`: open-ecosystem publishers host the artifact at the canonical URI on their subdomain; closed-platform / walled-garden shapes resolve through the AAO mirror at `https://creative.adcontextprotocol.org/translated/...`. Buyer agents fetch by `uri@digest` (immutable per digest, aggressive caching, `Cache-Control: public, max-age=31536000, immutable`), validate `params` and `slots` against the fetched schema, and reason about manifests structurally — same mechanic as platform_extensions but at the format-structure level. Without `format_schema`, custom shapes would be opaque to buyer agents and the protocol would regress to per-seller integration code; that's why the schema is required, not optional.\n\n**Fetch contract (normative)** — `format_schema` is load-bearing for validation (unlike `platform_extensions`, which is informational on the *consumption* side). The *transport* rules below apply identically to BOTH fields — any SDK fetching a `platform-extension-ref.json` URI MUST apply this contract regardless of whether the field name is `format_schema` or `platform_extensions`. A shared SDK fetch path that drops to the weakest bar undermines `format_schema`'s hardening. The consumption distinction (load-bearing vs informational) is about *what the body means*; the transport distinction is `https`-and-allowlisted regardless.\n\n- **Transport**: `https` only. Buyers MUST reject `http://`, `file://`, `data:`, and any non-`https` scheme. The URI MUST resolve to a JSON document that is itself a valid JSON Schema (Draft 07 or 2020-12; producers MUST declare `$schema`).\n- **SSRF protection**: buyers MUST resolve the URI hostname and reject if any resolved address is in RFC 1918 private space (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`), loopback (`127.0.0.0/8`, `::1`), link-local (`169.254.0.0/16`, `fe80::/10`), CGNAT (`100.64.0.0/10`), or any RFC 6761 special-use name (`.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. Buyers MUST pin the connection to the resolved IP (or re-resolve and re-validate the allowlist per request) to defeat DNS rebinding.\n- **HTTP redirects**: MUST be disabled. If a follow is implemented at all, the redirect target MUST pass the same scheme + SSRF + allowlist checks; otherwise the fetch hard-fails. Open redirects on same-origin paths are otherwise a free SSRF primitive.\n- **Response size cap**: response body MUST be capped at 1 MiB. Enforce during streaming, not after full buffering. Over-cap hard-fails identically to digest mismatch.\n- **Timeout**: SDKs SHOULD apply a fetch timeout ≤5 seconds. Timeout SHOULD be treated identically to an HTTP 5xx response (transient — retry policy at the SDK's discretion; on persistent failure surface as unresolved and skip the declaration for this session).\n- **Digest verification**: SHA-256 of the response body MUST equal `digest`. **Digest mismatch is a hard fail** — the buyer MUST treat the format declaration as unresolvable and MUST NOT validate manifests against the mismatched body. A divergent digest is either a malicious substitution or producer error; either way, falling back to the un-verified body breaks the trust model. Digest format: `sha256:` prefix + 64 lowercase hex characters. Cache key is `uri@digest`; digest mismatch MUST NOT be cached as a negative result keyed on `uri` alone (defeats CDN-flap recovery), and MUST be distinguishable in telemetry from network 5xx / 404 (sustained mismatch is a substitution-attack signal, not a flap).\n- **Sandboxing of `$ref`**: fetched schemas MAY use `$ref`. Buyers MUST resolve `$ref` only to URIs that are (a) same-origin as the parent `format_schema.uri` after RFC 3986 §6 normalization (lowercase scheme + host, strip default port, normalize path dot-segments, no userinfo component), OR (b) hosted under the AAO catalog domain (`https://creative.adcontextprotocol.org/...`), OR (c) intra-document JSON Pointer refs (`#/...`) bounded to the parent document's parsed tree. Cross-origin `$ref` to arbitrary URIs MUST be rejected. `$ref: file://...` MUST be rejected unconditionally. Transitive `$ref` chains MUST be bounded at depth ≤8 AND `$ref` count ≤256 across the resolved tree (depth 8 with breadth 100 per level is 10^16 nodes — depth alone is not enough). Publishers SHOULD inline rather than $ref where possible.\n- **Schema-compile bounds (DoS protection)**: validators MUST bound CPU/memory on fetched schemas. Recommended: compiled-schema keyword count ≤10 000, `pattern` regexes evaluated with a non-backtracking engine (re2) OR under a per-pattern timeout, per-manifest validation budget ≤250 ms (exceeded budget → treat manifest as invalid, surface telemetry signal). Without these, a 'valid' schema with catastrophic regex backtracking or exponential `allOf`/`anyOf` expansion pins a CPU forever.\n- **Cache**: buyers cache fetched schemas by `uri@digest` and treat them as immutable (the same hosting contract as `platform_extensions`). On `404`, network partition, or persistent fetch failure, buyers SHOULD degrade gracefully (treat the declaration as unresolved, skip it for the current `get_products` response, surface via `errors[]` with the relevant code) rather than failing the entire session.\n- **Schema-not-valid handling**: if the fetched body parses as JSON but is not a valid JSON Schema, the buyer MUST treat the declaration as unresolvable (same as digest mismatch) and surface via `errors[]`. Validators MUST NOT attempt partial validation against an invalid schema.\n- **AAO catalog trust**: `https://creative.adcontextprotocol.org/*` is a single trust anchor in the same-origin allowlist; compromise of the catalog domain or its CA compromises every buyer agent. Catalog-served bodies MUST be digest-pinned identically to origin fetches (the digest is on the *parent* `format_schema.uri@digest`, not on the catalog response). Future hardening (signed bodies, transparency log) is tracked separately.").optional() }).and(z.union([z.object({ "format_kind": z.literal("image"), "params": z.intersection(z.object({ "experimental": z.boolean().describe("When true, this canonical or seller narrowing may not work as declared. Adopters SHOULD preflight it with validate_input or in a sandbox and SHOULD NOT route production budget without testing; experimental status never makes the deprecated v1 path preferable. Drivers include unsettled spec shape, an adopter runtime gap, and custom shapes awaiting promotion. This replaces the earlier status plus runtime_status axes. Sellers SHOULD set experimental whenever a canonical or declaration is not production-ready.").default(false), "deprecated": z.boolean().describe("When true, this canonical (or a seller's specific narrowing of it) is going away. Existing adopters are supported through the deprecation cycle; new adoption is discouraged. Pair with `migration_target_version` to indicate when the canonical is expected to be removed. Distinct from `experimental`: an experimental canonical may stabilize and stop being experimental; a deprecated canonical is on a sunset path.").default(false), "v1_translatable": z.boolean().describe("Whether this canonical has any v1 named-format equivalent. `true` (default) — the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1→v2 projection via `v1-canonical-mapping.json` is meaningful. `false` — the canonical is inherently new in v2 and has no v1 form; v1's `list_creative_formats` couldn't express it because the underlying concept (algorithmic surface composition, AI-surface mentions, retail-media catalog placements, multi-card carousels) didn't exist as a v1 named-format archetype.\n\nLets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The 4 inherently-v2 canonicals at 3.1 GA: `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`.").default(true), "since_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version that introduced this canonical (e.g., '3.1', '3.2'). Lets adopters reason about minimum protocol version requirements when consuming a format declaration. Patch precision is intentionally rejected — canonicals are introduced at minor-version boundaries.").optional(), "migration_target_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version by which the working group expects this canonical to stabilize, surface a breaking revision, or (when `deprecated: true`) be removed. Patch precision is intentionally rejected — canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown').").optional(), "composition_model": z.enum(["deterministic","algorithmic"]).describe("Whether the surface composes deterministically (buyer can predict per-slot rendering — sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing — responsive_creative, agent_placement).").optional(), "provenance_required": z.boolean().describe("When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent.").optional(), "platform_extensions": z.array(z.object({ "uri": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL identifying the extension. `https://` is mandatory — `http://`, `file://`, `data:`, and other schemes are rejected at the schema layer (defense-in-depth on top of the fetch-contract normative rules). The URI base is the owning agent's URL; the path identifies the extension within that agent. Example: 'https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel'. The full fetch contract — SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds — is documented on `product-format-declaration.json#format_schema` and applies to ALL fetches of this reference shape regardless of whether the field is named `format_schema` (load-bearing for validation) or `platform_extensions` (informational); the *transport* rules are identical, only the *consumption* semantics differ."), "digest": z.string().regex(new RegExp("^sha256:[a-f0-9]{64}$")).describe("SHA-256 content digest of the extension definition (sha256:). Used to detect drift — if the agent revises the extension, the digest changes and cached definitions become invalid.") }).catchall(z.any()).describe("Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` — divergent digests in one response indicate producer-side error (e.g., concurrent extension revision mid-render). Buyers encountering divergent digests for the same URI MUST fail closed: treat all references to that URI as unresolved and surface a validation error rather than picking one branch silently. **Across responses**, digest divergence is normal — extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.")).describe("Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\n\n**Collision precedence (normative).** When two or more `platform_extensions[]` entries on the same declaration extend the same target (e.g., both extend `tracking`) with overlapping field names, **array order is authoritative — later entries override earlier ones on a per-field basis** (last-in-array-wins). SDKs MUST surface the overlap via the `errors[]` array on the `get_products` response with a structured code (`FORMAT_DECLARATION_DIVERGENT` is appropriate when the overlap appears across dual-emitted shapes; a producer-self-emitted overlap on a single declaration SHOULD use the same code with `error.details: { collision_kind: \"platform_extension_field\", target, overlapping_fields, winning_extension_uri }`). Producers SHOULD avoid the collision by emitting one extension per target or by partitioning fields across extensions; the deterministic precedence is for last-resort consistency across SDK implementations, not a sanctioned merging strategy.").optional(), "synthesis_nondeterministic": z.boolean().describe("When true, the format's production pipeline is genuinely nondeterministic — the platform cannot guarantee that synthesis from a given input set produces in-spec output. Veo / Sora / Runway-class generative video, and other AI-synthesis flows where output dimensions, duration, or quality vary per run. Implies a different validation contract: predictive `validate_input` is impossible; the platform's own post-synthesis QA loop applies; if the QA loop exhausts without producing a valid artifact, `build_creative` returns task_failed with a synthesis_failed reason. Distinct from `composition_model` (which describes how the surface composes per-slot rendering, not whether synthesis is deterministic). When false or absent, the format's production is predictable enough that `validate_input` can predict output properties from input properties.\n\n**Compatibility with `asset_source` / `item_production_model`**: `synthesis_nondeterministic: true` MAY pair with any of `seller_pre_rendered_from_brief`, `seller_human_designed`, or `agent_synthesized` (the QA loop is concept-level, not source-specific — 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.").default(false), "slots": z.array(z.object({ "asset_group_id": z.string().describe("Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings."), "asset_type": z.enum(["image","video","audio","text","markdown","url","html","css","javascript","vast","daast","webhook","brief","catalog","published_post","zip","card","object","pixel_tracker","vast_tracker","daast_tracker"]).describe("Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `published_post` is an existing-post reference asset, not uploaded media bytes and not a catalog row. `card` is the multi-card carousel element type (see card-asset.json). `pixel_tracker` / `vast_tracker` / `daast_tracker` are the renderer-fired measurement-tracker primitives — see `/schemas/core/assets/pixel-tracker-asset.json` and the VAST / DAAST tracker schemas. `object` is a last-resort fallback for structured non-asset inputs that don't fit any primitive asset_type — prefer specific types whenever possible."), "required": z.boolean().describe("Whether this slot is required for a valid manifest.").default(false), "min": z.number().int().gte(0).describe("Minimum count for repeatable / pool slots.").optional(), "max": z.number().int().gte(1).describe("Maximum count for repeatable / pool slots.").optional(), "max_chars": z.number().int().gte(1).describe("Per-slot character limit. Valid only when `asset_type` is `text`, `markdown`, or `brief`. Mutually exclusive with `max_size_kb` (which applies to binary asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "max_size_kb": z.number().int().gte(1).describe("Per-slot file size limit in kilobytes. Valid only when `asset_type` is `image`, `video`, `audio`, or `zip`. Mutually exclusive with `max_chars` (which applies to text asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Accepted intrinsic-pixel densities for this image-bearing slot. Valid when `asset_type` is `image`, and on a `card` slot where it constrains each card's image media (video media is unaffected). This makes density available to every canonical carrying image assets (native, carousel, responsive, companion images, and image itself), not only `format_kind: image`. When the image canonical also declares top-level `params.pixel_ratios`, the effective set is the intersection; an empty intersection is invalid. One matching asset satisfies the slot unless `required_pixel_ratios` requires rendition coverage.").optional(), "required_pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Required density coverage for an image rendition set. Valid only when `asset_type` is `image` and `pixel_ratios` is also declared. Every value MUST appear in the effective accepted set after intersecting any top-level image `params.pixel_ratios`, and the manifest slot value MUST be an array containing exactly one matching image rendition for each required ratio. Other accepted ratios remain optional. For example, `pixel_ratios: [1, 1.5, 2]` with `required_pixel_ratios: [1, 2]` requires the 1x and 2x renditions while making 1.5x optional. SDKs enforce intersection, subset, coverage, and duplicate-ratio rules because JSON Schema draft-07 cannot express them generically.").optional(), "logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("When `asset_group_id` is `logo`, renderer-facing brand.json logo slots acceptable for this format slot. Producers selecting from brand.json SHOULD prefer `logos[]` entries whose `slots[]` intersects this list, then apply `visual_guidelines.logo_usage_rules[]`.").optional(), "required_logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Subset of `logo_slots` for which this format expects explicit logo coverage. A manifest or brand-derived logo pool SHOULD include at least one usable logo for each required slot; if coverage is missing, builders SHOULD surface a validation warning or approval mapping instead of guessing from prose.").optional(), "description": z.string().describe("Human-readable description of what the slot expects from the buyer.").optional(), "consumed_for_production": z.boolean().describe("Dispatch hint for `build_creative` and v1↔v2 wire translators: when `true`, the slot's value is consumed as INPUT to a production step (host-read script, brief copy fed to generative synthesis, catalog feed driving per-SKU rendering) and is not rendered verbatim. When `false` (default), the slot's value is rendered verbatim on the placement (image bytes, video file, display tag).\n\nMotivates the v1↔v2 dispatch table: pre-v2 buyers shipped production-consumed inputs separately in a `inputs` map on the build_creative request; v2 collapses inputs and rendered assets into a single `assets` map keyed by `asset_group_id`. SDK translators between v1 and v2 use this flag per canonical to know which assets in the v2 manifest map back to v1 `inputs` vs v1 `assets`. Without the per-slot flag the dispatch table lives in adopter code and every SDK gets it slightly different.\n\nProducers SHOULD set this explicitly on slots whose consumption pattern isn't obvious (host-read scripts on `audio_hosted`, briefs on generative `video_hosted`, catalog feeds on `sponsored_placement`). For canonicals where every slot is render-verbatim (`image`, `display_tag`, `video_vast`), the default `false` is sufficient and the flag MAY be omitted.").default(false) }).catchall(z.any()).and(z.intersection(z.intersection(z.any(), z.intersection(z.any(), z.any())), z.intersection(z.any(), z.intersection(z.any(), z.any()))))).describe("Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.").optional(), "required_connections": z.array(z.object({ "provider": z.string().describe("Stable provider or platform namespace, preferably lowercase. Examples: `social.example`, `shortvideo.example`, or a seller-defined namespace. Omit only when the requirement is provider-agnostic, or when an `authorization_url` fully routes the human to the correct provider-specific connection flow.").optional(), "connection_type": z.enum(["advertiser_account","publisher_identity","post_authorization"]).describe("Kind of downstream connection required. `advertiser_account` is the platform account used to buy/manage ads. `publisher_identity` is the creator, page, channel, organization, or profile that owns source posts. `post_authorization` is a post-scoped grant when the platform authorizes individual posts instead of, or in addition to, the owning identity."), "required_for": z.array(z.string().min(1)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Concrete AdCP protocol operation names that require this downstream connection. Sellers SHOULD include this in product declarations when the requirement is known ahead of time, and in AUTHORIZATION_REQUIRED details when it explains the failed operation. Prefer specific operation names such as `list_creatives`, `sync_creatives`, `create_media_buy`, `get_media_buy_delivery`, or `get_creative_delivery` over broad category labels such as `reporting`.").optional(), "scope": z.enum(["account","identity","post","unknown"]).describe("Granularity of the downstream grant.").optional(), "status": z.enum(["connected","missing","pending","expired","revoked","not_required","unknown"]).describe("Current seller-observed state for this downstream connection when known. Product declarations MAY omit status or use `unknown`; AUTHORIZATION_REQUIRED details SHOULD use `missing`, `expired`, or `revoked` for the connection that blocked the call.").optional(), "connection_id": z.string().describe("Seller-defined identifier for an already-created downstream connection. Omit when no connection exists yet or when exposing it would leak platform/account state.").optional(), "resource_ref": z.object({ "platform_account_id": z.string().describe("Provider-native advertiser or business account id, when safe to disclose.").optional(), "identity_id": z.string().describe("Provider-native creator, page, channel, organization, or profile id, when safe to disclose.").optional(), "handle": z.string().describe("Provider-native public handle for the owning identity, when available.").optional(), "profile_url": z.string().url().describe("Public URL for the owning identity, when available.").optional(), "post_id": z.string().describe("Provider-native post id, when the grant is post-scoped or the failed request referenced a specific post.").optional(), "post_url": z.string().url().describe("Public URL for the referenced post, when available.").optional() }).catchall(z.any()).describe("Optional opaque provider-native resource hint, such as a platform account id, profile URL, handle, channel id, post id, or post URL. This is a hint for routing authorization, not proof that authorization exists.").optional(), "authorization_url": z.string().url().describe("Seller-hosted or provider-hosted URL where a human can complete or restore this downstream connection.").optional(), "authorization_instructions": z.string().describe("Human-readable instructions for completing or restoring this downstream connection.").optional(), "expires_at": z.string().datetime().describe("Expiration time for the downstream grant, when known.").optional() }).catchall(z.any()).and(z.any()).describe("A seller/platform-side connection or grant required by a product, format, or request. This is not the AdCP caller credential: the AdCP request is still authenticated once, and the seller uses these stored downstream connections to call a platform or service on the buyer's behalf. Use this shape for platforms that require more than one downstream grant, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.")).describe("Downstream platform connections or grants required to use this format declaration. These are in addition to the single AdCP caller credential. Use this when a platform product requires multiple downstream grants, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.").optional(), "reference_mutability": z.enum(["immutable_snapshot","mutable_requires_reapproval","mutable_auto_recheck"]).describe("Policy for formats whose `slots` accept a `published_post` reference. `immutable_snapshot`: seller snapshots the referenced post at approval and later source changes do not change the served creative. `mutable_requires_reapproval`: the source post may change and material changes require review before continued serving. `mutable_auto_recheck`: the source post may change and the seller continuously or periodically rechecks authorization/policy without requiring buyer resubmission. Omit when the format has no `published_post` slot.").optional(), "production_window_business_days": z.number().int().gte(0).describe("Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative).").optional() }).catchall(z.any()).describe("Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints)."), z.union([z.any().refine((value) => !z.union([z.any(), z.any(), z.any(), z.any(), z.any()]).safeParse(value).success, "Invalid input: Should NOT be valid against schema"), z.any().refine((value) => !z.union([z.any(), z.any(), z.any(), z.any(), z.any(), z.any()]).safeParse(value).success, "Invalid input: Should NOT be valid against schema"), z.union([z.any(), z.any(), z.any(), z.any()]), z.any().refine((value) => !z.union([z.any(), z.any(), z.any(), z.any(), z.any(), z.any(), z.any()]).safeParse(value).success, "Invalid input: Should NOT be valid against schema")]).describe("Exactly one of: (a) fixed (`width` + `height` both set), (b) multi-size (`sizes` set), (c) responsive (any of `min_width`/`max_width`/`min_height`/`max_height` set), (d) none (no size constraint declared — accepts any dimensions). Combining modes (e.g., `width` + `sizes`) is rejected at schema layer; same rule on `html5` and `display_tag` canonicals.")).describe("Static image creative format. Slots: `image_main` (image asset, file or hosted URL), optional `headline` (text), `body_text` (text), `cta` (text/enum), `landing_page_url` (url). Tracking model: impression pixel + click URL via universal_macros, with optional viewability pixel. Distinct from `html5` (interactive bundles) and `display_tag` (third-party served). AR/dimensions narrow to specific sizes via product parameters — covers IAB display sizes (300x250, 728x90, 970x250, etc.) without a separate iab_size enum.") }), z.object({ "format_kind": z.literal("html5"), "params": z.intersection(z.object({ "experimental": z.boolean().describe("When true, this canonical or seller narrowing may not work as declared. Adopters SHOULD preflight it with validate_input or in a sandbox and SHOULD NOT route production budget without testing; experimental status never makes the deprecated v1 path preferable. Drivers include unsettled spec shape, an adopter runtime gap, and custom shapes awaiting promotion. This replaces the earlier status plus runtime_status axes. Sellers SHOULD set experimental whenever a canonical or declaration is not production-ready.").default(false), "deprecated": z.boolean().describe("When true, this canonical (or a seller's specific narrowing of it) is going away. Existing adopters are supported through the deprecation cycle; new adoption is discouraged. Pair with `migration_target_version` to indicate when the canonical is expected to be removed. Distinct from `experimental`: an experimental canonical may stabilize and stop being experimental; a deprecated canonical is on a sunset path.").default(false), "v1_translatable": z.boolean().describe("Whether this canonical has any v1 named-format equivalent. `true` (default) — the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1→v2 projection via `v1-canonical-mapping.json` is meaningful. `false` — the canonical is inherently new in v2 and has no v1 form; v1's `list_creative_formats` couldn't express it because the underlying concept (algorithmic surface composition, AI-surface mentions, retail-media catalog placements, multi-card carousels) didn't exist as a v1 named-format archetype.\n\nLets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The 4 inherently-v2 canonicals at 3.1 GA: `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`.").default(true), "since_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version that introduced this canonical (e.g., '3.1', '3.2'). Lets adopters reason about minimum protocol version requirements when consuming a format declaration. Patch precision is intentionally rejected — canonicals are introduced at minor-version boundaries.").optional(), "migration_target_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version by which the working group expects this canonical to stabilize, surface a breaking revision, or (when `deprecated: true`) be removed. Patch precision is intentionally rejected — canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown').").optional(), "composition_model": z.enum(["deterministic","algorithmic"]).describe("Whether the surface composes deterministically (buyer can predict per-slot rendering — sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing — responsive_creative, agent_placement).").optional(), "provenance_required": z.boolean().describe("When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent.").optional(), "platform_extensions": z.array(z.object({ "uri": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL identifying the extension. `https://` is mandatory — `http://`, `file://`, `data:`, and other schemes are rejected at the schema layer (defense-in-depth on top of the fetch-contract normative rules). The URI base is the owning agent's URL; the path identifies the extension within that agent. Example: 'https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel'. The full fetch contract — SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds — is documented on `product-format-declaration.json#format_schema` and applies to ALL fetches of this reference shape regardless of whether the field is named `format_schema` (load-bearing for validation) or `platform_extensions` (informational); the *transport* rules are identical, only the *consumption* semantics differ."), "digest": z.string().regex(new RegExp("^sha256:[a-f0-9]{64}$")).describe("SHA-256 content digest of the extension definition (sha256:). Used to detect drift — if the agent revises the extension, the digest changes and cached definitions become invalid.") }).catchall(z.any()).describe("Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` — divergent digests in one response indicate producer-side error (e.g., concurrent extension revision mid-render). Buyers encountering divergent digests for the same URI MUST fail closed: treat all references to that URI as unresolved and surface a validation error rather than picking one branch silently. **Across responses**, digest divergence is normal — extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.")).describe("Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\n\n**Collision precedence (normative).** When two or more `platform_extensions[]` entries on the same declaration extend the same target (e.g., both extend `tracking`) with overlapping field names, **array order is authoritative — later entries override earlier ones on a per-field basis** (last-in-array-wins). SDKs MUST surface the overlap via the `errors[]` array on the `get_products` response with a structured code (`FORMAT_DECLARATION_DIVERGENT` is appropriate when the overlap appears across dual-emitted shapes; a producer-self-emitted overlap on a single declaration SHOULD use the same code with `error.details: { collision_kind: \"platform_extension_field\", target, overlapping_fields, winning_extension_uri }`). Producers SHOULD avoid the collision by emitting one extension per target or by partitioning fields across extensions; the deterministic precedence is for last-resort consistency across SDK implementations, not a sanctioned merging strategy.").optional(), "synthesis_nondeterministic": z.boolean().describe("When true, the format's production pipeline is genuinely nondeterministic — the platform cannot guarantee that synthesis from a given input set produces in-spec output. Veo / Sora / Runway-class generative video, and other AI-synthesis flows where output dimensions, duration, or quality vary per run. Implies a different validation contract: predictive `validate_input` is impossible; the platform's own post-synthesis QA loop applies; if the QA loop exhausts without producing a valid artifact, `build_creative` returns task_failed with a synthesis_failed reason. Distinct from `composition_model` (which describes how the surface composes per-slot rendering, not whether synthesis is deterministic). When false or absent, the format's production is predictable enough that `validate_input` can predict output properties from input properties.\n\n**Compatibility with `asset_source` / `item_production_model`**: `synthesis_nondeterministic: true` MAY pair with any of `seller_pre_rendered_from_brief`, `seller_human_designed`, or `agent_synthesized` (the QA loop is concept-level, not source-specific — 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.").default(false), "slots": z.array(z.object({ "asset_group_id": z.string().describe("Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings."), "asset_type": z.enum(["image","video","audio","text","markdown","url","html","css","javascript","vast","daast","webhook","brief","catalog","published_post","zip","card","object","pixel_tracker","vast_tracker","daast_tracker"]).describe("Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `published_post` is an existing-post reference asset, not uploaded media bytes and not a catalog row. `card` is the multi-card carousel element type (see card-asset.json). `pixel_tracker` / `vast_tracker` / `daast_tracker` are the renderer-fired measurement-tracker primitives — see `/schemas/core/assets/pixel-tracker-asset.json` and the VAST / DAAST tracker schemas. `object` is a last-resort fallback for structured non-asset inputs that don't fit any primitive asset_type — prefer specific types whenever possible."), "required": z.boolean().describe("Whether this slot is required for a valid manifest.").default(false), "min": z.number().int().gte(0).describe("Minimum count for repeatable / pool slots.").optional(), "max": z.number().int().gte(1).describe("Maximum count for repeatable / pool slots.").optional(), "max_chars": z.number().int().gte(1).describe("Per-slot character limit. Valid only when `asset_type` is `text`, `markdown`, or `brief`. Mutually exclusive with `max_size_kb` (which applies to binary asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "max_size_kb": z.number().int().gte(1).describe("Per-slot file size limit in kilobytes. Valid only when `asset_type` is `image`, `video`, `audio`, or `zip`. Mutually exclusive with `max_chars` (which applies to text asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Accepted intrinsic-pixel densities for this image-bearing slot. Valid when `asset_type` is `image`, and on a `card` slot where it constrains each card's image media (video media is unaffected). This makes density available to every canonical carrying image assets (native, carousel, responsive, companion images, and image itself), not only `format_kind: image`. When the image canonical also declares top-level `params.pixel_ratios`, the effective set is the intersection; an empty intersection is invalid. One matching asset satisfies the slot unless `required_pixel_ratios` requires rendition coverage.").optional(), "required_pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Required density coverage for an image rendition set. Valid only when `asset_type` is `image` and `pixel_ratios` is also declared. Every value MUST appear in the effective accepted set after intersecting any top-level image `params.pixel_ratios`, and the manifest slot value MUST be an array containing exactly one matching image rendition for each required ratio. Other accepted ratios remain optional. For example, `pixel_ratios: [1, 1.5, 2]` with `required_pixel_ratios: [1, 2]` requires the 1x and 2x renditions while making 1.5x optional. SDKs enforce intersection, subset, coverage, and duplicate-ratio rules because JSON Schema draft-07 cannot express them generically.").optional(), "logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("When `asset_group_id` is `logo`, renderer-facing brand.json logo slots acceptable for this format slot. Producers selecting from brand.json SHOULD prefer `logos[]` entries whose `slots[]` intersects this list, then apply `visual_guidelines.logo_usage_rules[]`.").optional(), "required_logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Subset of `logo_slots` for which this format expects explicit logo coverage. A manifest or brand-derived logo pool SHOULD include at least one usable logo for each required slot; if coverage is missing, builders SHOULD surface a validation warning or approval mapping instead of guessing from prose.").optional(), "description": z.string().describe("Human-readable description of what the slot expects from the buyer.").optional(), "consumed_for_production": z.boolean().describe("Dispatch hint for `build_creative` and v1↔v2 wire translators: when `true`, the slot's value is consumed as INPUT to a production step (host-read script, brief copy fed to generative synthesis, catalog feed driving per-SKU rendering) and is not rendered verbatim. When `false` (default), the slot's value is rendered verbatim on the placement (image bytes, video file, display tag).\n\nMotivates the v1↔v2 dispatch table: pre-v2 buyers shipped production-consumed inputs separately in a `inputs` map on the build_creative request; v2 collapses inputs and rendered assets into a single `assets` map keyed by `asset_group_id`. SDK translators between v1 and v2 use this flag per canonical to know which assets in the v2 manifest map back to v1 `inputs` vs v1 `assets`. Without the per-slot flag the dispatch table lives in adopter code and every SDK gets it slightly different.\n\nProducers SHOULD set this explicitly on slots whose consumption pattern isn't obvious (host-read scripts on `audio_hosted`, briefs on generative `video_hosted`, catalog feeds on `sponsored_placement`). For canonicals where every slot is render-verbatim (`image`, `display_tag`, `video_vast`), the default `false` is sufficient and the flag MAY be omitted.").default(false) }).catchall(z.any()).and(z.intersection(z.intersection(z.any(), z.intersection(z.any(), z.any())), z.intersection(z.any(), z.intersection(z.any(), z.any()))))).describe("Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.").optional(), "required_connections": z.array(z.object({ "provider": z.string().describe("Stable provider or platform namespace, preferably lowercase. Examples: `social.example`, `shortvideo.example`, or a seller-defined namespace. Omit only when the requirement is provider-agnostic, or when an `authorization_url` fully routes the human to the correct provider-specific connection flow.").optional(), "connection_type": z.enum(["advertiser_account","publisher_identity","post_authorization"]).describe("Kind of downstream connection required. `advertiser_account` is the platform account used to buy/manage ads. `publisher_identity` is the creator, page, channel, organization, or profile that owns source posts. `post_authorization` is a post-scoped grant when the platform authorizes individual posts instead of, or in addition to, the owning identity."), "required_for": z.array(z.string().min(1)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Concrete AdCP protocol operation names that require this downstream connection. Sellers SHOULD include this in product declarations when the requirement is known ahead of time, and in AUTHORIZATION_REQUIRED details when it explains the failed operation. Prefer specific operation names such as `list_creatives`, `sync_creatives`, `create_media_buy`, `get_media_buy_delivery`, or `get_creative_delivery` over broad category labels such as `reporting`.").optional(), "scope": z.enum(["account","identity","post","unknown"]).describe("Granularity of the downstream grant.").optional(), "status": z.enum(["connected","missing","pending","expired","revoked","not_required","unknown"]).describe("Current seller-observed state for this downstream connection when known. Product declarations MAY omit status or use `unknown`; AUTHORIZATION_REQUIRED details SHOULD use `missing`, `expired`, or `revoked` for the connection that blocked the call.").optional(), "connection_id": z.string().describe("Seller-defined identifier for an already-created downstream connection. Omit when no connection exists yet or when exposing it would leak platform/account state.").optional(), "resource_ref": z.object({ "platform_account_id": z.string().describe("Provider-native advertiser or business account id, when safe to disclose.").optional(), "identity_id": z.string().describe("Provider-native creator, page, channel, organization, or profile id, when safe to disclose.").optional(), "handle": z.string().describe("Provider-native public handle for the owning identity, when available.").optional(), "profile_url": z.string().url().describe("Public URL for the owning identity, when available.").optional(), "post_id": z.string().describe("Provider-native post id, when the grant is post-scoped or the failed request referenced a specific post.").optional(), "post_url": z.string().url().describe("Public URL for the referenced post, when available.").optional() }).catchall(z.any()).describe("Optional opaque provider-native resource hint, such as a platform account id, profile URL, handle, channel id, post id, or post URL. This is a hint for routing authorization, not proof that authorization exists.").optional(), "authorization_url": z.string().url().describe("Seller-hosted or provider-hosted URL where a human can complete or restore this downstream connection.").optional(), "authorization_instructions": z.string().describe("Human-readable instructions for completing or restoring this downstream connection.").optional(), "expires_at": z.string().datetime().describe("Expiration time for the downstream grant, when known.").optional() }).catchall(z.any()).and(z.any()).describe("A seller/platform-side connection or grant required by a product, format, or request. This is not the AdCP caller credential: the AdCP request is still authenticated once, and the seller uses these stored downstream connections to call a platform or service on the buyer's behalf. Use this shape for platforms that require more than one downstream grant, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.")).describe("Downstream platform connections or grants required to use this format declaration. These are in addition to the single AdCP caller credential. Use this when a platform product requires multiple downstream grants, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.").optional(), "reference_mutability": z.enum(["immutable_snapshot","mutable_requires_reapproval","mutable_auto_recheck"]).describe("Policy for formats whose `slots` accept a `published_post` reference. `immutable_snapshot`: seller snapshots the referenced post at approval and later source changes do not change the served creative. `mutable_requires_reapproval`: the source post may change and material changes require review before continued serving. `mutable_auto_recheck`: the source post may change and the seller continuously or periodically rechecks authorization/policy without requiring buyer resubmission. Omit when the format has no `published_post` slot.").optional(), "production_window_business_days": z.number().int().gte(0).describe("Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative).").optional() }).catchall(z.any()).describe("Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints)."), z.union([z.any().refine((value) => !z.union([z.any(), z.any(), z.any(), z.any(), z.any()]).safeParse(value).success, "Invalid input: Should NOT be valid against schema"), z.any().refine((value) => !z.union([z.any(), z.any(), z.any(), z.any(), z.any(), z.any()]).safeParse(value).success, "Invalid input: Should NOT be valid against schema"), z.union([z.any(), z.any(), z.any(), z.any()]), z.any().refine((value) => !z.union([z.any(), z.any(), z.any(), z.any(), z.any(), z.any(), z.any()]).safeParse(value).success, "Invalid input: Should NOT be valid against schema")]).describe("Exactly one of: (a) fixed (`width` + `height` both set), (b) multi-size (`sizes` set), (c) responsive (any of `min_width`/`max_width`/`min_height`/`max_height` set), (d) none (no size constraint declared — accepts any dimensions). Combining modes is rejected at schema layer.")).describe("Interactive HTML5 banner delivered as a zip archive. Slot: `html5_bundle` (zip asset). Tracking model: MRAID + IAB Open Measurement (OM-SDK) + click-tag macro substitution + backup image fallback. Receivers unpack the zip, validate internal structure, and serve from CDN. Distinct from `image` (static, non-interactive) and `display_tag` (third-party served). The zip's entry point is typically `index.html`; click handling uses `clickTag` (or `clickTAG`) macro substitution.") }), z.object({ "format_kind": z.literal("display_tag"), "params": z.intersection(z.object({ "experimental": z.boolean().describe("When true, this canonical or seller narrowing may not work as declared. Adopters SHOULD preflight it with validate_input or in a sandbox and SHOULD NOT route production budget without testing; experimental status never makes the deprecated v1 path preferable. Drivers include unsettled spec shape, an adopter runtime gap, and custom shapes awaiting promotion. This replaces the earlier status plus runtime_status axes. Sellers SHOULD set experimental whenever a canonical or declaration is not production-ready.").default(false), "deprecated": z.boolean().describe("When true, this canonical (or a seller's specific narrowing of it) is going away. Existing adopters are supported through the deprecation cycle; new adoption is discouraged. Pair with `migration_target_version` to indicate when the canonical is expected to be removed. Distinct from `experimental`: an experimental canonical may stabilize and stop being experimental; a deprecated canonical is on a sunset path.").default(false), "v1_translatable": z.boolean().describe("Whether this canonical has any v1 named-format equivalent. `true` (default) — the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1→v2 projection via `v1-canonical-mapping.json` is meaningful. `false` — the canonical is inherently new in v2 and has no v1 form; v1's `list_creative_formats` couldn't express it because the underlying concept (algorithmic surface composition, AI-surface mentions, retail-media catalog placements, multi-card carousels) didn't exist as a v1 named-format archetype.\n\nLets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The 4 inherently-v2 canonicals at 3.1 GA: `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`.").default(true), "since_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version that introduced this canonical (e.g., '3.1', '3.2'). Lets adopters reason about minimum protocol version requirements when consuming a format declaration. Patch precision is intentionally rejected — canonicals are introduced at minor-version boundaries.").optional(), "migration_target_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version by which the working group expects this canonical to stabilize, surface a breaking revision, or (when `deprecated: true`) be removed. Patch precision is intentionally rejected — canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown').").optional(), "composition_model": z.enum(["deterministic","algorithmic"]).describe("Whether the surface composes deterministically (buyer can predict per-slot rendering — sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing — responsive_creative, agent_placement).").optional(), "provenance_required": z.boolean().describe("When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent.").optional(), "platform_extensions": z.array(z.object({ "uri": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL identifying the extension. `https://` is mandatory — `http://`, `file://`, `data:`, and other schemes are rejected at the schema layer (defense-in-depth on top of the fetch-contract normative rules). The URI base is the owning agent's URL; the path identifies the extension within that agent. Example: 'https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel'. The full fetch contract — SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds — is documented on `product-format-declaration.json#format_schema` and applies to ALL fetches of this reference shape regardless of whether the field is named `format_schema` (load-bearing for validation) or `platform_extensions` (informational); the *transport* rules are identical, only the *consumption* semantics differ."), "digest": z.string().regex(new RegExp("^sha256:[a-f0-9]{64}$")).describe("SHA-256 content digest of the extension definition (sha256:). Used to detect drift — if the agent revises the extension, the digest changes and cached definitions become invalid.") }).catchall(z.any()).describe("Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` — divergent digests in one response indicate producer-side error (e.g., concurrent extension revision mid-render). Buyers encountering divergent digests for the same URI MUST fail closed: treat all references to that URI as unresolved and surface a validation error rather than picking one branch silently. **Across responses**, digest divergence is normal — extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.")).describe("Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\n\n**Collision precedence (normative).** When two or more `platform_extensions[]` entries on the same declaration extend the same target (e.g., both extend `tracking`) with overlapping field names, **array order is authoritative — later entries override earlier ones on a per-field basis** (last-in-array-wins). SDKs MUST surface the overlap via the `errors[]` array on the `get_products` response with a structured code (`FORMAT_DECLARATION_DIVERGENT` is appropriate when the overlap appears across dual-emitted shapes; a producer-self-emitted overlap on a single declaration SHOULD use the same code with `error.details: { collision_kind: \"platform_extension_field\", target, overlapping_fields, winning_extension_uri }`). Producers SHOULD avoid the collision by emitting one extension per target or by partitioning fields across extensions; the deterministic precedence is for last-resort consistency across SDK implementations, not a sanctioned merging strategy.").optional(), "synthesis_nondeterministic": z.boolean().describe("When true, the format's production pipeline is genuinely nondeterministic — the platform cannot guarantee that synthesis from a given input set produces in-spec output. Veo / Sora / Runway-class generative video, and other AI-synthesis flows where output dimensions, duration, or quality vary per run. Implies a different validation contract: predictive `validate_input` is impossible; the platform's own post-synthesis QA loop applies; if the QA loop exhausts without producing a valid artifact, `build_creative` returns task_failed with a synthesis_failed reason. Distinct from `composition_model` (which describes how the surface composes per-slot rendering, not whether synthesis is deterministic). When false or absent, the format's production is predictable enough that `validate_input` can predict output properties from input properties.\n\n**Compatibility with `asset_source` / `item_production_model`**: `synthesis_nondeterministic: true` MAY pair with any of `seller_pre_rendered_from_brief`, `seller_human_designed`, or `agent_synthesized` (the QA loop is concept-level, not source-specific — 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.").default(false), "slots": z.array(z.object({ "asset_group_id": z.string().describe("Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings."), "asset_type": z.enum(["image","video","audio","text","markdown","url","html","css","javascript","vast","daast","webhook","brief","catalog","published_post","zip","card","object","pixel_tracker","vast_tracker","daast_tracker"]).describe("Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `published_post` is an existing-post reference asset, not uploaded media bytes and not a catalog row. `card` is the multi-card carousel element type (see card-asset.json). `pixel_tracker` / `vast_tracker` / `daast_tracker` are the renderer-fired measurement-tracker primitives — see `/schemas/core/assets/pixel-tracker-asset.json` and the VAST / DAAST tracker schemas. `object` is a last-resort fallback for structured non-asset inputs that don't fit any primitive asset_type — prefer specific types whenever possible."), "required": z.boolean().describe("Whether this slot is required for a valid manifest.").default(false), "min": z.number().int().gte(0).describe("Minimum count for repeatable / pool slots.").optional(), "max": z.number().int().gte(1).describe("Maximum count for repeatable / pool slots.").optional(), "max_chars": z.number().int().gte(1).describe("Per-slot character limit. Valid only when `asset_type` is `text`, `markdown`, or `brief`. Mutually exclusive with `max_size_kb` (which applies to binary asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "max_size_kb": z.number().int().gte(1).describe("Per-slot file size limit in kilobytes. Valid only when `asset_type` is `image`, `video`, `audio`, or `zip`. Mutually exclusive with `max_chars` (which applies to text asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Accepted intrinsic-pixel densities for this image-bearing slot. Valid when `asset_type` is `image`, and on a `card` slot where it constrains each card's image media (video media is unaffected). This makes density available to every canonical carrying image assets (native, carousel, responsive, companion images, and image itself), not only `format_kind: image`. When the image canonical also declares top-level `params.pixel_ratios`, the effective set is the intersection; an empty intersection is invalid. One matching asset satisfies the slot unless `required_pixel_ratios` requires rendition coverage.").optional(), "required_pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Required density coverage for an image rendition set. Valid only when `asset_type` is `image` and `pixel_ratios` is also declared. Every value MUST appear in the effective accepted set after intersecting any top-level image `params.pixel_ratios`, and the manifest slot value MUST be an array containing exactly one matching image rendition for each required ratio. Other accepted ratios remain optional. For example, `pixel_ratios: [1, 1.5, 2]` with `required_pixel_ratios: [1, 2]` requires the 1x and 2x renditions while making 1.5x optional. SDKs enforce intersection, subset, coverage, and duplicate-ratio rules because JSON Schema draft-07 cannot express them generically.").optional(), "logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("When `asset_group_id` is `logo`, renderer-facing brand.json logo slots acceptable for this format slot. Producers selecting from brand.json SHOULD prefer `logos[]` entries whose `slots[]` intersects this list, then apply `visual_guidelines.logo_usage_rules[]`.").optional(), "required_logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Subset of `logo_slots` for which this format expects explicit logo coverage. A manifest or brand-derived logo pool SHOULD include at least one usable logo for each required slot; if coverage is missing, builders SHOULD surface a validation warning or approval mapping instead of guessing from prose.").optional(), "description": z.string().describe("Human-readable description of what the slot expects from the buyer.").optional(), "consumed_for_production": z.boolean().describe("Dispatch hint for `build_creative` and v1↔v2 wire translators: when `true`, the slot's value is consumed as INPUT to a production step (host-read script, brief copy fed to generative synthesis, catalog feed driving per-SKU rendering) and is not rendered verbatim. When `false` (default), the slot's value is rendered verbatim on the placement (image bytes, video file, display tag).\n\nMotivates the v1↔v2 dispatch table: pre-v2 buyers shipped production-consumed inputs separately in a `inputs` map on the build_creative request; v2 collapses inputs and rendered assets into a single `assets` map keyed by `asset_group_id`. SDK translators between v1 and v2 use this flag per canonical to know which assets in the v2 manifest map back to v1 `inputs` vs v1 `assets`. Without the per-slot flag the dispatch table lives in adopter code and every SDK gets it slightly different.\n\nProducers SHOULD set this explicitly on slots whose consumption pattern isn't obvious (host-read scripts on `audio_hosted`, briefs on generative `video_hosted`, catalog feeds on `sponsored_placement`). For canonicals where every slot is render-verbatim (`image`, `display_tag`, `video_vast`), the default `false` is sufficient and the flag MAY be omitted.").default(false) }).catchall(z.any()).and(z.intersection(z.intersection(z.any(), z.intersection(z.any(), z.any())), z.intersection(z.any(), z.intersection(z.any(), z.any()))))).describe("Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.").optional(), "required_connections": z.array(z.object({ "provider": z.string().describe("Stable provider or platform namespace, preferably lowercase. Examples: `social.example`, `shortvideo.example`, or a seller-defined namespace. Omit only when the requirement is provider-agnostic, or when an `authorization_url` fully routes the human to the correct provider-specific connection flow.").optional(), "connection_type": z.enum(["advertiser_account","publisher_identity","post_authorization"]).describe("Kind of downstream connection required. `advertiser_account` is the platform account used to buy/manage ads. `publisher_identity` is the creator, page, channel, organization, or profile that owns source posts. `post_authorization` is a post-scoped grant when the platform authorizes individual posts instead of, or in addition to, the owning identity."), "required_for": z.array(z.string().min(1)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Concrete AdCP protocol operation names that require this downstream connection. Sellers SHOULD include this in product declarations when the requirement is known ahead of time, and in AUTHORIZATION_REQUIRED details when it explains the failed operation. Prefer specific operation names such as `list_creatives`, `sync_creatives`, `create_media_buy`, `get_media_buy_delivery`, or `get_creative_delivery` over broad category labels such as `reporting`.").optional(), "scope": z.enum(["account","identity","post","unknown"]).describe("Granularity of the downstream grant.").optional(), "status": z.enum(["connected","missing","pending","expired","revoked","not_required","unknown"]).describe("Current seller-observed state for this downstream connection when known. Product declarations MAY omit status or use `unknown`; AUTHORIZATION_REQUIRED details SHOULD use `missing`, `expired`, or `revoked` for the connection that blocked the call.").optional(), "connection_id": z.string().describe("Seller-defined identifier for an already-created downstream connection. Omit when no connection exists yet or when exposing it would leak platform/account state.").optional(), "resource_ref": z.object({ "platform_account_id": z.string().describe("Provider-native advertiser or business account id, when safe to disclose.").optional(), "identity_id": z.string().describe("Provider-native creator, page, channel, organization, or profile id, when safe to disclose.").optional(), "handle": z.string().describe("Provider-native public handle for the owning identity, when available.").optional(), "profile_url": z.string().url().describe("Public URL for the owning identity, when available.").optional(), "post_id": z.string().describe("Provider-native post id, when the grant is post-scoped or the failed request referenced a specific post.").optional(), "post_url": z.string().url().describe("Public URL for the referenced post, when available.").optional() }).catchall(z.any()).describe("Optional opaque provider-native resource hint, such as a platform account id, profile URL, handle, channel id, post id, or post URL. This is a hint for routing authorization, not proof that authorization exists.").optional(), "authorization_url": z.string().url().describe("Seller-hosted or provider-hosted URL where a human can complete or restore this downstream connection.").optional(), "authorization_instructions": z.string().describe("Human-readable instructions for completing or restoring this downstream connection.").optional(), "expires_at": z.string().datetime().describe("Expiration time for the downstream grant, when known.").optional() }).catchall(z.any()).and(z.any()).describe("A seller/platform-side connection or grant required by a product, format, or request. This is not the AdCP caller credential: the AdCP request is still authenticated once, and the seller uses these stored downstream connections to call a platform or service on the buyer's behalf. Use this shape for platforms that require more than one downstream grant, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.")).describe("Downstream platform connections or grants required to use this format declaration. These are in addition to the single AdCP caller credential. Use this when a platform product requires multiple downstream grants, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.").optional(), "reference_mutability": z.enum(["immutable_snapshot","mutable_requires_reapproval","mutable_auto_recheck"]).describe("Policy for formats whose `slots` accept a `published_post` reference. `immutable_snapshot`: seller snapshots the referenced post at approval and later source changes do not change the served creative. `mutable_requires_reapproval`: the source post may change and material changes require review before continued serving. `mutable_auto_recheck`: the source post may change and the seller continuously or periodically rechecks authorization/policy without requiring buyer resubmission. Omit when the format has no `published_post` slot.").optional(), "production_window_business_days": z.number().int().gte(0).describe("Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative).").optional() }).catchall(z.any()).describe("Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints)."), z.union([z.any().refine((value) => !z.union([z.any(), z.any(), z.any(), z.any(), z.any()]).safeParse(value).success, "Invalid input: Should NOT be valid against schema"), z.any().refine((value) => !z.union([z.any(), z.any(), z.any(), z.any(), z.any(), z.any()]).safeParse(value).success, "Invalid input: Should NOT be valid against schema"), z.union([z.any(), z.any(), z.any(), z.any()]), z.any().refine((value) => !z.union([z.any(), z.any(), z.any(), z.any(), z.any(), z.any(), z.any()]).safeParse(value).success, "Invalid input: Should NOT be valid against schema")]).describe("Exactly one of: (a) fixed (`width` + `height` both set), (b) multi-size (`sizes` set), (c) responsive (any of `min_width`/`max_width`/`min_height`/`max_height` set), (d) none (no size constraint declared — accepts any dimensions). Combining modes is rejected at schema layer.")).describe("Third-party-served display tag (JS, iframe, or 1×1 redirect). The buyer's adserver hosts the creative; the seller calls the tag URL at impression time. Slot: `tag_url` (url asset with appropriate `url_type`). Tracking model: opaque to seller — third party serves and measures. Click tracking via redirect URL substitution using universal_macros. Distinct from `image` (static asset hosted by seller) and `html5` (zip bundle hosted by seller).") }), z.object({ "format_kind": z.literal("image_carousel"), "params": z.object({ "experimental": z.boolean().describe("When true, this canonical or seller narrowing may not work as declared. Adopters SHOULD preflight it with validate_input or in a sandbox and SHOULD NOT route production budget without testing; experimental status never makes the deprecated v1 path preferable. Drivers include unsettled spec shape, an adopter runtime gap, and custom shapes awaiting promotion. This replaces the earlier status plus runtime_status axes. Sellers SHOULD set experimental whenever a canonical or declaration is not production-ready.").default(false), "deprecated": z.boolean().describe("When true, this canonical (or a seller's specific narrowing of it) is going away. Existing adopters are supported through the deprecation cycle; new adoption is discouraged. Pair with `migration_target_version` to indicate when the canonical is expected to be removed. Distinct from `experimental`: an experimental canonical may stabilize and stop being experimental; a deprecated canonical is on a sunset path.").default(false), "v1_translatable": z.boolean().describe("Whether this canonical has any v1 named-format equivalent. `true` (default) — the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1→v2 projection via `v1-canonical-mapping.json` is meaningful. `false` — the canonical is inherently new in v2 and has no v1 form; v1's `list_creative_formats` couldn't express it because the underlying concept (algorithmic surface composition, AI-surface mentions, retail-media catalog placements, multi-card carousels) didn't exist as a v1 named-format archetype.\n\nLets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The 4 inherently-v2 canonicals at 3.1 GA: `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`.").default(true), "since_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version that introduced this canonical (e.g., '3.1', '3.2'). Lets adopters reason about minimum protocol version requirements when consuming a format declaration. Patch precision is intentionally rejected — canonicals are introduced at minor-version boundaries.").optional(), "migration_target_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version by which the working group expects this canonical to stabilize, surface a breaking revision, or (when `deprecated: true`) be removed. Patch precision is intentionally rejected — canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown').").optional(), "composition_model": z.enum(["deterministic","algorithmic"]).describe("Whether the surface composes deterministically (buyer can predict per-slot rendering — sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing — responsive_creative, agent_placement).").optional(), "provenance_required": z.boolean().describe("When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent.").optional(), "platform_extensions": z.array(z.object({ "uri": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL identifying the extension. `https://` is mandatory — `http://`, `file://`, `data:`, and other schemes are rejected at the schema layer (defense-in-depth on top of the fetch-contract normative rules). The URI base is the owning agent's URL; the path identifies the extension within that agent. Example: 'https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel'. The full fetch contract — SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds — is documented on `product-format-declaration.json#format_schema` and applies to ALL fetches of this reference shape regardless of whether the field is named `format_schema` (load-bearing for validation) or `platform_extensions` (informational); the *transport* rules are identical, only the *consumption* semantics differ."), "digest": z.string().regex(new RegExp("^sha256:[a-f0-9]{64}$")).describe("SHA-256 content digest of the extension definition (sha256:). Used to detect drift — if the agent revises the extension, the digest changes and cached definitions become invalid.") }).catchall(z.any()).describe("Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` — divergent digests in one response indicate producer-side error (e.g., concurrent extension revision mid-render). Buyers encountering divergent digests for the same URI MUST fail closed: treat all references to that URI as unresolved and surface a validation error rather than picking one branch silently. **Across responses**, digest divergence is normal — extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.")).describe("Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\n\n**Collision precedence (normative).** When two or more `platform_extensions[]` entries on the same declaration extend the same target (e.g., both extend `tracking`) with overlapping field names, **array order is authoritative — later entries override earlier ones on a per-field basis** (last-in-array-wins). SDKs MUST surface the overlap via the `errors[]` array on the `get_products` response with a structured code (`FORMAT_DECLARATION_DIVERGENT` is appropriate when the overlap appears across dual-emitted shapes; a producer-self-emitted overlap on a single declaration SHOULD use the same code with `error.details: { collision_kind: \"platform_extension_field\", target, overlapping_fields, winning_extension_uri }`). Producers SHOULD avoid the collision by emitting one extension per target or by partitioning fields across extensions; the deterministic precedence is for last-resort consistency across SDK implementations, not a sanctioned merging strategy.").optional(), "synthesis_nondeterministic": z.boolean().describe("When true, the format's production pipeline is genuinely nondeterministic — the platform cannot guarantee that synthesis from a given input set produces in-spec output. Veo / Sora / Runway-class generative video, and other AI-synthesis flows where output dimensions, duration, or quality vary per run. Implies a different validation contract: predictive `validate_input` is impossible; the platform's own post-synthesis QA loop applies; if the QA loop exhausts without producing a valid artifact, `build_creative` returns task_failed with a synthesis_failed reason. Distinct from `composition_model` (which describes how the surface composes per-slot rendering, not whether synthesis is deterministic). When false or absent, the format's production is predictable enough that `validate_input` can predict output properties from input properties.\n\n**Compatibility with `asset_source` / `item_production_model`**: `synthesis_nondeterministic: true` MAY pair with any of `seller_pre_rendered_from_brief`, `seller_human_designed`, or `agent_synthesized` (the QA loop is concept-level, not source-specific — 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.").default(false), "slots": z.array(z.object({ "asset_group_id": z.string().describe("Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings."), "asset_type": z.enum(["image","video","audio","text","markdown","url","html","css","javascript","vast","daast","webhook","brief","catalog","published_post","zip","card","object","pixel_tracker","vast_tracker","daast_tracker"]).describe("Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `published_post` is an existing-post reference asset, not uploaded media bytes and not a catalog row. `card` is the multi-card carousel element type (see card-asset.json). `pixel_tracker` / `vast_tracker` / `daast_tracker` are the renderer-fired measurement-tracker primitives — see `/schemas/core/assets/pixel-tracker-asset.json` and the VAST / DAAST tracker schemas. `object` is a last-resort fallback for structured non-asset inputs that don't fit any primitive asset_type — prefer specific types whenever possible."), "required": z.boolean().describe("Whether this slot is required for a valid manifest.").default(false), "min": z.number().int().gte(0).describe("Minimum count for repeatable / pool slots.").optional(), "max": z.number().int().gte(1).describe("Maximum count for repeatable / pool slots.").optional(), "max_chars": z.number().int().gte(1).describe("Per-slot character limit. Valid only when `asset_type` is `text`, `markdown`, or `brief`. Mutually exclusive with `max_size_kb` (which applies to binary asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "max_size_kb": z.number().int().gte(1).describe("Per-slot file size limit in kilobytes. Valid only when `asset_type` is `image`, `video`, `audio`, or `zip`. Mutually exclusive with `max_chars` (which applies to text asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Accepted intrinsic-pixel densities for this image-bearing slot. Valid when `asset_type` is `image`, and on a `card` slot where it constrains each card's image media (video media is unaffected). This makes density available to every canonical carrying image assets (native, carousel, responsive, companion images, and image itself), not only `format_kind: image`. When the image canonical also declares top-level `params.pixel_ratios`, the effective set is the intersection; an empty intersection is invalid. One matching asset satisfies the slot unless `required_pixel_ratios` requires rendition coverage.").optional(), "required_pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Required density coverage for an image rendition set. Valid only when `asset_type` is `image` and `pixel_ratios` is also declared. Every value MUST appear in the effective accepted set after intersecting any top-level image `params.pixel_ratios`, and the manifest slot value MUST be an array containing exactly one matching image rendition for each required ratio. Other accepted ratios remain optional. For example, `pixel_ratios: [1, 1.5, 2]` with `required_pixel_ratios: [1, 2]` requires the 1x and 2x renditions while making 1.5x optional. SDKs enforce intersection, subset, coverage, and duplicate-ratio rules because JSON Schema draft-07 cannot express them generically.").optional(), "logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("When `asset_group_id` is `logo`, renderer-facing brand.json logo slots acceptable for this format slot. Producers selecting from brand.json SHOULD prefer `logos[]` entries whose `slots[]` intersects this list, then apply `visual_guidelines.logo_usage_rules[]`.").optional(), "required_logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Subset of `logo_slots` for which this format expects explicit logo coverage. A manifest or brand-derived logo pool SHOULD include at least one usable logo for each required slot; if coverage is missing, builders SHOULD surface a validation warning or approval mapping instead of guessing from prose.").optional(), "description": z.string().describe("Human-readable description of what the slot expects from the buyer.").optional(), "consumed_for_production": z.boolean().describe("Dispatch hint for `build_creative` and v1↔v2 wire translators: when `true`, the slot's value is consumed as INPUT to a production step (host-read script, brief copy fed to generative synthesis, catalog feed driving per-SKU rendering) and is not rendered verbatim. When `false` (default), the slot's value is rendered verbatim on the placement (image bytes, video file, display tag).\n\nMotivates the v1↔v2 dispatch table: pre-v2 buyers shipped production-consumed inputs separately in a `inputs` map on the build_creative request; v2 collapses inputs and rendered assets into a single `assets` map keyed by `asset_group_id`. SDK translators between v1 and v2 use this flag per canonical to know which assets in the v2 manifest map back to v1 `inputs` vs v1 `assets`. Without the per-slot flag the dispatch table lives in adopter code and every SDK gets it slightly different.\n\nProducers SHOULD set this explicitly on slots whose consumption pattern isn't obvious (host-read scripts on `audio_hosted`, briefs on generative `video_hosted`, catalog feeds on `sponsored_placement`). For canonicals where every slot is render-verbatim (`image`, `display_tag`, `video_vast`), the default `false` is sufficient and the flag MAY be omitted.").default(false) }).catchall(z.any()).and(z.intersection(z.intersection(z.any(), z.intersection(z.any(), z.any())), z.intersection(z.any(), z.intersection(z.any(), z.any()))))).describe("Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.").optional(), "required_connections": z.array(z.object({ "provider": z.string().describe("Stable provider or platform namespace, preferably lowercase. Examples: `social.example`, `shortvideo.example`, or a seller-defined namespace. Omit only when the requirement is provider-agnostic, or when an `authorization_url` fully routes the human to the correct provider-specific connection flow.").optional(), "connection_type": z.enum(["advertiser_account","publisher_identity","post_authorization"]).describe("Kind of downstream connection required. `advertiser_account` is the platform account used to buy/manage ads. `publisher_identity` is the creator, page, channel, organization, or profile that owns source posts. `post_authorization` is a post-scoped grant when the platform authorizes individual posts instead of, or in addition to, the owning identity."), "required_for": z.array(z.string().min(1)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Concrete AdCP protocol operation names that require this downstream connection. Sellers SHOULD include this in product declarations when the requirement is known ahead of time, and in AUTHORIZATION_REQUIRED details when it explains the failed operation. Prefer specific operation names such as `list_creatives`, `sync_creatives`, `create_media_buy`, `get_media_buy_delivery`, or `get_creative_delivery` over broad category labels such as `reporting`.").optional(), "scope": z.enum(["account","identity","post","unknown"]).describe("Granularity of the downstream grant.").optional(), "status": z.enum(["connected","missing","pending","expired","revoked","not_required","unknown"]).describe("Current seller-observed state for this downstream connection when known. Product declarations MAY omit status or use `unknown`; AUTHORIZATION_REQUIRED details SHOULD use `missing`, `expired`, or `revoked` for the connection that blocked the call.").optional(), "connection_id": z.string().describe("Seller-defined identifier for an already-created downstream connection. Omit when no connection exists yet or when exposing it would leak platform/account state.").optional(), "resource_ref": z.object({ "platform_account_id": z.string().describe("Provider-native advertiser or business account id, when safe to disclose.").optional(), "identity_id": z.string().describe("Provider-native creator, page, channel, organization, or profile id, when safe to disclose.").optional(), "handle": z.string().describe("Provider-native public handle for the owning identity, when available.").optional(), "profile_url": z.string().url().describe("Public URL for the owning identity, when available.").optional(), "post_id": z.string().describe("Provider-native post id, when the grant is post-scoped or the failed request referenced a specific post.").optional(), "post_url": z.string().url().describe("Public URL for the referenced post, when available.").optional() }).catchall(z.any()).describe("Optional opaque provider-native resource hint, such as a platform account id, profile URL, handle, channel id, post id, or post URL. This is a hint for routing authorization, not proof that authorization exists.").optional(), "authorization_url": z.string().url().describe("Seller-hosted or provider-hosted URL where a human can complete or restore this downstream connection.").optional(), "authorization_instructions": z.string().describe("Human-readable instructions for completing or restoring this downstream connection.").optional(), "expires_at": z.string().datetime().describe("Expiration time for the downstream grant, when known.").optional() }).catchall(z.any()).and(z.any()).describe("A seller/platform-side connection or grant required by a product, format, or request. This is not the AdCP caller credential: the AdCP request is still authenticated once, and the seller uses these stored downstream connections to call a platform or service on the buyer's behalf. Use this shape for platforms that require more than one downstream grant, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.")).describe("Downstream platform connections or grants required to use this format declaration. These are in addition to the single AdCP caller credential. Use this when a platform product requires multiple downstream grants, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.").optional(), "reference_mutability": z.enum(["immutable_snapshot","mutable_requires_reapproval","mutable_auto_recheck"]).describe("Policy for formats whose `slots` accept a `published_post` reference. `immutable_snapshot`: seller snapshots the referenced post at approval and later source changes do not change the served creative. `mutable_requires_reapproval`: the source post may change and material changes require review before continued serving. `mutable_auto_recheck`: the source post may change and the seller continuously or periodically rechecks authorization/policy without requiring buyer resubmission. Omit when the format has no `published_post` slot.").optional(), "production_window_business_days": z.number().int().gte(0).describe("Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative).").optional() }).catchall(z.any()).describe("Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).").describe("Multi-card swipeable carousel. The buyer ships a `cards` slot whose value is an **array** of [card-asset](/schemas/core/assets/card-asset.json) objects (a single key with an array value — NOT one key per card, NOT dotted/bracketed paths). Each card-asset carries: `asset_type: \"card\"`, `media` (an image or video asset), optional `headline` (text), optional `landing_page_url` (url asset). Per-card structure is the same across all cards; mixed orientations not allowed within a single carousel. Tracking model: per-card impression and engagement pixels + carousel-level engagement (swipe, view-time). Allowed asset types for a card's `media` field: `image` and `video` (Meta-style mixed-media); platforms can narrow to image-only or video-only via `allowed_card_media_asset_types`.\n\nThe manifest's `assets.cards` value is an array of card-asset objects. Example: `\"cards\": [{\"asset_type\": \"card\", \"media\": {\"asset_type\": \"image\", \"url\": \"...\"}, \"headline\": \"Buy now\", \"landing_page_url\": {\"asset_type\": \"url\", \"url_type\": \"clickthrough\", \"url\": \"...\"}}, ...]`. Each card-asset validates against the card schema; per-card platform extensions attach via the card's `platform_extensions` field, never via inline non-canonical keys.") }), z.object({ "format_kind": z.literal("video_hosted"), "params": z.object({ "experimental": z.boolean().describe("When true, this canonical or seller narrowing may not work as declared. Adopters SHOULD preflight it with validate_input or in a sandbox and SHOULD NOT route production budget without testing; experimental status never makes the deprecated v1 path preferable. Drivers include unsettled spec shape, an adopter runtime gap, and custom shapes awaiting promotion. This replaces the earlier status plus runtime_status axes. Sellers SHOULD set experimental whenever a canonical or declaration is not production-ready.").default(false), "deprecated": z.boolean().describe("When true, this canonical (or a seller's specific narrowing of it) is going away. Existing adopters are supported through the deprecation cycle; new adoption is discouraged. Pair with `migration_target_version` to indicate when the canonical is expected to be removed. Distinct from `experimental`: an experimental canonical may stabilize and stop being experimental; a deprecated canonical is on a sunset path.").default(false), "v1_translatable": z.boolean().describe("Whether this canonical has any v1 named-format equivalent. `true` (default) — the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1→v2 projection via `v1-canonical-mapping.json` is meaningful. `false` — the canonical is inherently new in v2 and has no v1 form; v1's `list_creative_formats` couldn't express it because the underlying concept (algorithmic surface composition, AI-surface mentions, retail-media catalog placements, multi-card carousels) didn't exist as a v1 named-format archetype.\n\nLets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The 4 inherently-v2 canonicals at 3.1 GA: `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`.").default(true), "since_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version that introduced this canonical (e.g., '3.1', '3.2'). Lets adopters reason about minimum protocol version requirements when consuming a format declaration. Patch precision is intentionally rejected — canonicals are introduced at minor-version boundaries.").optional(), "migration_target_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version by which the working group expects this canonical to stabilize, surface a breaking revision, or (when `deprecated: true`) be removed. Patch precision is intentionally rejected — canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown').").optional(), "composition_model": z.enum(["deterministic","algorithmic"]).describe("Whether the surface composes deterministically (buyer can predict per-slot rendering — sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing — responsive_creative, agent_placement).").optional(), "provenance_required": z.boolean().describe("When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent.").optional(), "platform_extensions": z.array(z.object({ "uri": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL identifying the extension. `https://` is mandatory — `http://`, `file://`, `data:`, and other schemes are rejected at the schema layer (defense-in-depth on top of the fetch-contract normative rules). The URI base is the owning agent's URL; the path identifies the extension within that agent. Example: 'https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel'. The full fetch contract — SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds — is documented on `product-format-declaration.json#format_schema` and applies to ALL fetches of this reference shape regardless of whether the field is named `format_schema` (load-bearing for validation) or `platform_extensions` (informational); the *transport* rules are identical, only the *consumption* semantics differ."), "digest": z.string().regex(new RegExp("^sha256:[a-f0-9]{64}$")).describe("SHA-256 content digest of the extension definition (sha256:). Used to detect drift — if the agent revises the extension, the digest changes and cached definitions become invalid.") }).catchall(z.any()).describe("Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` — divergent digests in one response indicate producer-side error (e.g., concurrent extension revision mid-render). Buyers encountering divergent digests for the same URI MUST fail closed: treat all references to that URI as unresolved and surface a validation error rather than picking one branch silently. **Across responses**, digest divergence is normal — extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.")).describe("Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\n\n**Collision precedence (normative).** When two or more `platform_extensions[]` entries on the same declaration extend the same target (e.g., both extend `tracking`) with overlapping field names, **array order is authoritative — later entries override earlier ones on a per-field basis** (last-in-array-wins). SDKs MUST surface the overlap via the `errors[]` array on the `get_products` response with a structured code (`FORMAT_DECLARATION_DIVERGENT` is appropriate when the overlap appears across dual-emitted shapes; a producer-self-emitted overlap on a single declaration SHOULD use the same code with `error.details: { collision_kind: \"platform_extension_field\", target, overlapping_fields, winning_extension_uri }`). Producers SHOULD avoid the collision by emitting one extension per target or by partitioning fields across extensions; the deterministic precedence is for last-resort consistency across SDK implementations, not a sanctioned merging strategy.").optional(), "synthesis_nondeterministic": z.boolean().describe("When true, the format's production pipeline is genuinely nondeterministic — the platform cannot guarantee that synthesis from a given input set produces in-spec output. Veo / Sora / Runway-class generative video, and other AI-synthesis flows where output dimensions, duration, or quality vary per run. Implies a different validation contract: predictive `validate_input` is impossible; the platform's own post-synthesis QA loop applies; if the QA loop exhausts without producing a valid artifact, `build_creative` returns task_failed with a synthesis_failed reason. Distinct from `composition_model` (which describes how the surface composes per-slot rendering, not whether synthesis is deterministic). When false or absent, the format's production is predictable enough that `validate_input` can predict output properties from input properties.\n\n**Compatibility with `asset_source` / `item_production_model`**: `synthesis_nondeterministic: true` MAY pair with any of `seller_pre_rendered_from_brief`, `seller_human_designed`, or `agent_synthesized` (the QA loop is concept-level, not source-specific — 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.").default(false), "slots": z.array(z.object({ "asset_group_id": z.string().describe("Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings."), "asset_type": z.enum(["image","video","audio","text","markdown","url","html","css","javascript","vast","daast","webhook","brief","catalog","published_post","zip","card","object","pixel_tracker","vast_tracker","daast_tracker"]).describe("Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `published_post` is an existing-post reference asset, not uploaded media bytes and not a catalog row. `card` is the multi-card carousel element type (see card-asset.json). `pixel_tracker` / `vast_tracker` / `daast_tracker` are the renderer-fired measurement-tracker primitives — see `/schemas/core/assets/pixel-tracker-asset.json` and the VAST / DAAST tracker schemas. `object` is a last-resort fallback for structured non-asset inputs that don't fit any primitive asset_type — prefer specific types whenever possible."), "required": z.boolean().describe("Whether this slot is required for a valid manifest.").default(false), "min": z.number().int().gte(0).describe("Minimum count for repeatable / pool slots.").optional(), "max": z.number().int().gte(1).describe("Maximum count for repeatable / pool slots.").optional(), "max_chars": z.number().int().gte(1).describe("Per-slot character limit. Valid only when `asset_type` is `text`, `markdown`, or `brief`. Mutually exclusive with `max_size_kb` (which applies to binary asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "max_size_kb": z.number().int().gte(1).describe("Per-slot file size limit in kilobytes. Valid only when `asset_type` is `image`, `video`, `audio`, or `zip`. Mutually exclusive with `max_chars` (which applies to text asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Accepted intrinsic-pixel densities for this image-bearing slot. Valid when `asset_type` is `image`, and on a `card` slot where it constrains each card's image media (video media is unaffected). This makes density available to every canonical carrying image assets (native, carousel, responsive, companion images, and image itself), not only `format_kind: image`. When the image canonical also declares top-level `params.pixel_ratios`, the effective set is the intersection; an empty intersection is invalid. One matching asset satisfies the slot unless `required_pixel_ratios` requires rendition coverage.").optional(), "required_pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Required density coverage for an image rendition set. Valid only when `asset_type` is `image` and `pixel_ratios` is also declared. Every value MUST appear in the effective accepted set after intersecting any top-level image `params.pixel_ratios`, and the manifest slot value MUST be an array containing exactly one matching image rendition for each required ratio. Other accepted ratios remain optional. For example, `pixel_ratios: [1, 1.5, 2]` with `required_pixel_ratios: [1, 2]` requires the 1x and 2x renditions while making 1.5x optional. SDKs enforce intersection, subset, coverage, and duplicate-ratio rules because JSON Schema draft-07 cannot express them generically.").optional(), "logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("When `asset_group_id` is `logo`, renderer-facing brand.json logo slots acceptable for this format slot. Producers selecting from brand.json SHOULD prefer `logos[]` entries whose `slots[]` intersects this list, then apply `visual_guidelines.logo_usage_rules[]`.").optional(), "required_logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Subset of `logo_slots` for which this format expects explicit logo coverage. A manifest or brand-derived logo pool SHOULD include at least one usable logo for each required slot; if coverage is missing, builders SHOULD surface a validation warning or approval mapping instead of guessing from prose.").optional(), "description": z.string().describe("Human-readable description of what the slot expects from the buyer.").optional(), "consumed_for_production": z.boolean().describe("Dispatch hint for `build_creative` and v1↔v2 wire translators: when `true`, the slot's value is consumed as INPUT to a production step (host-read script, brief copy fed to generative synthesis, catalog feed driving per-SKU rendering) and is not rendered verbatim. When `false` (default), the slot's value is rendered verbatim on the placement (image bytes, video file, display tag).\n\nMotivates the v1↔v2 dispatch table: pre-v2 buyers shipped production-consumed inputs separately in a `inputs` map on the build_creative request; v2 collapses inputs and rendered assets into a single `assets` map keyed by `asset_group_id`. SDK translators between v1 and v2 use this flag per canonical to know which assets in the v2 manifest map back to v1 `inputs` vs v1 `assets`. Without the per-slot flag the dispatch table lives in adopter code and every SDK gets it slightly different.\n\nProducers SHOULD set this explicitly on slots whose consumption pattern isn't obvious (host-read scripts on `audio_hosted`, briefs on generative `video_hosted`, catalog feeds on `sponsored_placement`). For canonicals where every slot is render-verbatim (`image`, `display_tag`, `video_vast`), the default `false` is sufficient and the flag MAY be omitted.").default(false) }).catchall(z.any()).and(z.intersection(z.intersection(z.any(), z.intersection(z.any(), z.any())), z.intersection(z.any(), z.intersection(z.any(), z.any()))))).describe("Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.").optional(), "required_connections": z.array(z.object({ "provider": z.string().describe("Stable provider or platform namespace, preferably lowercase. Examples: `social.example`, `shortvideo.example`, or a seller-defined namespace. Omit only when the requirement is provider-agnostic, or when an `authorization_url` fully routes the human to the correct provider-specific connection flow.").optional(), "connection_type": z.enum(["advertiser_account","publisher_identity","post_authorization"]).describe("Kind of downstream connection required. `advertiser_account` is the platform account used to buy/manage ads. `publisher_identity` is the creator, page, channel, organization, or profile that owns source posts. `post_authorization` is a post-scoped grant when the platform authorizes individual posts instead of, or in addition to, the owning identity."), "required_for": z.array(z.string().min(1)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Concrete AdCP protocol operation names that require this downstream connection. Sellers SHOULD include this in product declarations when the requirement is known ahead of time, and in AUTHORIZATION_REQUIRED details when it explains the failed operation. Prefer specific operation names such as `list_creatives`, `sync_creatives`, `create_media_buy`, `get_media_buy_delivery`, or `get_creative_delivery` over broad category labels such as `reporting`.").optional(), "scope": z.enum(["account","identity","post","unknown"]).describe("Granularity of the downstream grant.").optional(), "status": z.enum(["connected","missing","pending","expired","revoked","not_required","unknown"]).describe("Current seller-observed state for this downstream connection when known. Product declarations MAY omit status or use `unknown`; AUTHORIZATION_REQUIRED details SHOULD use `missing`, `expired`, or `revoked` for the connection that blocked the call.").optional(), "connection_id": z.string().describe("Seller-defined identifier for an already-created downstream connection. Omit when no connection exists yet or when exposing it would leak platform/account state.").optional(), "resource_ref": z.object({ "platform_account_id": z.string().describe("Provider-native advertiser or business account id, when safe to disclose.").optional(), "identity_id": z.string().describe("Provider-native creator, page, channel, organization, or profile id, when safe to disclose.").optional(), "handle": z.string().describe("Provider-native public handle for the owning identity, when available.").optional(), "profile_url": z.string().url().describe("Public URL for the owning identity, when available.").optional(), "post_id": z.string().describe("Provider-native post id, when the grant is post-scoped or the failed request referenced a specific post.").optional(), "post_url": z.string().url().describe("Public URL for the referenced post, when available.").optional() }).catchall(z.any()).describe("Optional opaque provider-native resource hint, such as a platform account id, profile URL, handle, channel id, post id, or post URL. This is a hint for routing authorization, not proof that authorization exists.").optional(), "authorization_url": z.string().url().describe("Seller-hosted or provider-hosted URL where a human can complete or restore this downstream connection.").optional(), "authorization_instructions": z.string().describe("Human-readable instructions for completing or restoring this downstream connection.").optional(), "expires_at": z.string().datetime().describe("Expiration time for the downstream grant, when known.").optional() }).catchall(z.any()).and(z.any()).describe("A seller/platform-side connection or grant required by a product, format, or request. This is not the AdCP caller credential: the AdCP request is still authenticated once, and the seller uses these stored downstream connections to call a platform or service on the buyer's behalf. Use this shape for platforms that require more than one downstream grant, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.")).describe("Downstream platform connections or grants required to use this format declaration. These are in addition to the single AdCP caller credential. Use this when a platform product requires multiple downstream grants, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.").optional(), "reference_mutability": z.enum(["immutable_snapshot","mutable_requires_reapproval","mutable_auto_recheck"]).describe("Policy for formats whose `slots` accept a `published_post` reference. `immutable_snapshot`: seller snapshots the referenced post at approval and later source changes do not change the served creative. `mutable_requires_reapproval`: the source post may change and material changes require review before continued serving. `mutable_auto_recheck`: the source post may change and the seller continuously or periodically rechecks authorization/policy without requiring buyer resubmission. Omit when the format has no `published_post` slot.").optional(), "production_window_business_days": z.number().int().gte(0).describe("Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative).").optional() }).catchall(z.any()).describe("Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).").describe("Direct video file (mp4/webm/mov) hosted by the buyer. Slot: `video_main` (video asset, file or hosted URL), optional `headline`, `brand_name`, `cta`, `companion_banner`, `landing_page_url`. Tracking model: IAB Open Measurement SDK + external impression/click/quartile pixels via universal_macros. Orientation is a parameter (vertical 9:16 / horizontal 16:9 / square 1:1); slot shape includes optional `brand_name` (typical for vertical short-form) and optional `companion_banner` (typical for horizontal instream). Distinct from `video_vast` (VAST tag, inherent VAST event tracking) — receivers fire impression and click pixels at delivery time.") }), z.object({ "format_kind": z.literal("video_vast"), "params": z.object({ "experimental": z.boolean().describe("When true, this canonical or seller narrowing may not work as declared. Adopters SHOULD preflight it with validate_input or in a sandbox and SHOULD NOT route production budget without testing; experimental status never makes the deprecated v1 path preferable. Drivers include unsettled spec shape, an adopter runtime gap, and custom shapes awaiting promotion. This replaces the earlier status plus runtime_status axes. Sellers SHOULD set experimental whenever a canonical or declaration is not production-ready.").default(false), "deprecated": z.boolean().describe("When true, this canonical (or a seller's specific narrowing of it) is going away. Existing adopters are supported through the deprecation cycle; new adoption is discouraged. Pair with `migration_target_version` to indicate when the canonical is expected to be removed. Distinct from `experimental`: an experimental canonical may stabilize and stop being experimental; a deprecated canonical is on a sunset path.").default(false), "v1_translatable": z.boolean().describe("Whether this canonical has any v1 named-format equivalent. `true` (default) — the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1→v2 projection via `v1-canonical-mapping.json` is meaningful. `false` — the canonical is inherently new in v2 and has no v1 form; v1's `list_creative_formats` couldn't express it because the underlying concept (algorithmic surface composition, AI-surface mentions, retail-media catalog placements, multi-card carousels) didn't exist as a v1 named-format archetype.\n\nLets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The 4 inherently-v2 canonicals at 3.1 GA: `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`.").default(true), "since_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version that introduced this canonical (e.g., '3.1', '3.2'). Lets adopters reason about minimum protocol version requirements when consuming a format declaration. Patch precision is intentionally rejected — canonicals are introduced at minor-version boundaries.").optional(), "migration_target_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version by which the working group expects this canonical to stabilize, surface a breaking revision, or (when `deprecated: true`) be removed. Patch precision is intentionally rejected — canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown').").optional(), "composition_model": z.enum(["deterministic","algorithmic"]).describe("Whether the surface composes deterministically (buyer can predict per-slot rendering — sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing — responsive_creative, agent_placement).").optional(), "provenance_required": z.boolean().describe("When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent.").optional(), "platform_extensions": z.array(z.object({ "uri": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL identifying the extension. `https://` is mandatory — `http://`, `file://`, `data:`, and other schemes are rejected at the schema layer (defense-in-depth on top of the fetch-contract normative rules). The URI base is the owning agent's URL; the path identifies the extension within that agent. Example: 'https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel'. The full fetch contract — SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds — is documented on `product-format-declaration.json#format_schema` and applies to ALL fetches of this reference shape regardless of whether the field is named `format_schema` (load-bearing for validation) or `platform_extensions` (informational); the *transport* rules are identical, only the *consumption* semantics differ."), "digest": z.string().regex(new RegExp("^sha256:[a-f0-9]{64}$")).describe("SHA-256 content digest of the extension definition (sha256:). Used to detect drift — if the agent revises the extension, the digest changes and cached definitions become invalid.") }).catchall(z.any()).describe("Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` — divergent digests in one response indicate producer-side error (e.g., concurrent extension revision mid-render). Buyers encountering divergent digests for the same URI MUST fail closed: treat all references to that URI as unresolved and surface a validation error rather than picking one branch silently. **Across responses**, digest divergence is normal — extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.")).describe("Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\n\n**Collision precedence (normative).** When two or more `platform_extensions[]` entries on the same declaration extend the same target (e.g., both extend `tracking`) with overlapping field names, **array order is authoritative — later entries override earlier ones on a per-field basis** (last-in-array-wins). SDKs MUST surface the overlap via the `errors[]` array on the `get_products` response with a structured code (`FORMAT_DECLARATION_DIVERGENT` is appropriate when the overlap appears across dual-emitted shapes; a producer-self-emitted overlap on a single declaration SHOULD use the same code with `error.details: { collision_kind: \"platform_extension_field\", target, overlapping_fields, winning_extension_uri }`). Producers SHOULD avoid the collision by emitting one extension per target or by partitioning fields across extensions; the deterministic precedence is for last-resort consistency across SDK implementations, not a sanctioned merging strategy.").optional(), "synthesis_nondeterministic": z.boolean().describe("When true, the format's production pipeline is genuinely nondeterministic — the platform cannot guarantee that synthesis from a given input set produces in-spec output. Veo / Sora / Runway-class generative video, and other AI-synthesis flows where output dimensions, duration, or quality vary per run. Implies a different validation contract: predictive `validate_input` is impossible; the platform's own post-synthesis QA loop applies; if the QA loop exhausts without producing a valid artifact, `build_creative` returns task_failed with a synthesis_failed reason. Distinct from `composition_model` (which describes how the surface composes per-slot rendering, not whether synthesis is deterministic). When false or absent, the format's production is predictable enough that `validate_input` can predict output properties from input properties.\n\n**Compatibility with `asset_source` / `item_production_model`**: `synthesis_nondeterministic: true` MAY pair with any of `seller_pre_rendered_from_brief`, `seller_human_designed`, or `agent_synthesized` (the QA loop is concept-level, not source-specific — 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.").default(false), "slots": z.array(z.object({ "asset_group_id": z.string().describe("Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings."), "asset_type": z.enum(["image","video","audio","text","markdown","url","html","css","javascript","vast","daast","webhook","brief","catalog","published_post","zip","card","object","pixel_tracker","vast_tracker","daast_tracker"]).describe("Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `published_post` is an existing-post reference asset, not uploaded media bytes and not a catalog row. `card` is the multi-card carousel element type (see card-asset.json). `pixel_tracker` / `vast_tracker` / `daast_tracker` are the renderer-fired measurement-tracker primitives — see `/schemas/core/assets/pixel-tracker-asset.json` and the VAST / DAAST tracker schemas. `object` is a last-resort fallback for structured non-asset inputs that don't fit any primitive asset_type — prefer specific types whenever possible."), "required": z.boolean().describe("Whether this slot is required for a valid manifest.").default(false), "min": z.number().int().gte(0).describe("Minimum count for repeatable / pool slots.").optional(), "max": z.number().int().gte(1).describe("Maximum count for repeatable / pool slots.").optional(), "max_chars": z.number().int().gte(1).describe("Per-slot character limit. Valid only when `asset_type` is `text`, `markdown`, or `brief`. Mutually exclusive with `max_size_kb` (which applies to binary asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "max_size_kb": z.number().int().gte(1).describe("Per-slot file size limit in kilobytes. Valid only when `asset_type` is `image`, `video`, `audio`, or `zip`. Mutually exclusive with `max_chars` (which applies to text asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Accepted intrinsic-pixel densities for this image-bearing slot. Valid when `asset_type` is `image`, and on a `card` slot where it constrains each card's image media (video media is unaffected). This makes density available to every canonical carrying image assets (native, carousel, responsive, companion images, and image itself), not only `format_kind: image`. When the image canonical also declares top-level `params.pixel_ratios`, the effective set is the intersection; an empty intersection is invalid. One matching asset satisfies the slot unless `required_pixel_ratios` requires rendition coverage.").optional(), "required_pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Required density coverage for an image rendition set. Valid only when `asset_type` is `image` and `pixel_ratios` is also declared. Every value MUST appear in the effective accepted set after intersecting any top-level image `params.pixel_ratios`, and the manifest slot value MUST be an array containing exactly one matching image rendition for each required ratio. Other accepted ratios remain optional. For example, `pixel_ratios: [1, 1.5, 2]` with `required_pixel_ratios: [1, 2]` requires the 1x and 2x renditions while making 1.5x optional. SDKs enforce intersection, subset, coverage, and duplicate-ratio rules because JSON Schema draft-07 cannot express them generically.").optional(), "logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("When `asset_group_id` is `logo`, renderer-facing brand.json logo slots acceptable for this format slot. Producers selecting from brand.json SHOULD prefer `logos[]` entries whose `slots[]` intersects this list, then apply `visual_guidelines.logo_usage_rules[]`.").optional(), "required_logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Subset of `logo_slots` for which this format expects explicit logo coverage. A manifest or brand-derived logo pool SHOULD include at least one usable logo for each required slot; if coverage is missing, builders SHOULD surface a validation warning or approval mapping instead of guessing from prose.").optional(), "description": z.string().describe("Human-readable description of what the slot expects from the buyer.").optional(), "consumed_for_production": z.boolean().describe("Dispatch hint for `build_creative` and v1↔v2 wire translators: when `true`, the slot's value is consumed as INPUT to a production step (host-read script, brief copy fed to generative synthesis, catalog feed driving per-SKU rendering) and is not rendered verbatim. When `false` (default), the slot's value is rendered verbatim on the placement (image bytes, video file, display tag).\n\nMotivates the v1↔v2 dispatch table: pre-v2 buyers shipped production-consumed inputs separately in a `inputs` map on the build_creative request; v2 collapses inputs and rendered assets into a single `assets` map keyed by `asset_group_id`. SDK translators between v1 and v2 use this flag per canonical to know which assets in the v2 manifest map back to v1 `inputs` vs v1 `assets`. Without the per-slot flag the dispatch table lives in adopter code and every SDK gets it slightly different.\n\nProducers SHOULD set this explicitly on slots whose consumption pattern isn't obvious (host-read scripts on `audio_hosted`, briefs on generative `video_hosted`, catalog feeds on `sponsored_placement`). For canonicals where every slot is render-verbatim (`image`, `display_tag`, `video_vast`), the default `false` is sufficient and the flag MAY be omitted.").default(false) }).catchall(z.any()).and(z.intersection(z.intersection(z.any(), z.intersection(z.any(), z.any())), z.intersection(z.any(), z.intersection(z.any(), z.any()))))).describe("Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.").optional(), "required_connections": z.array(z.object({ "provider": z.string().describe("Stable provider or platform namespace, preferably lowercase. Examples: `social.example`, `shortvideo.example`, or a seller-defined namespace. Omit only when the requirement is provider-agnostic, or when an `authorization_url` fully routes the human to the correct provider-specific connection flow.").optional(), "connection_type": z.enum(["advertiser_account","publisher_identity","post_authorization"]).describe("Kind of downstream connection required. `advertiser_account` is the platform account used to buy/manage ads. `publisher_identity` is the creator, page, channel, organization, or profile that owns source posts. `post_authorization` is a post-scoped grant when the platform authorizes individual posts instead of, or in addition to, the owning identity."), "required_for": z.array(z.string().min(1)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Concrete AdCP protocol operation names that require this downstream connection. Sellers SHOULD include this in product declarations when the requirement is known ahead of time, and in AUTHORIZATION_REQUIRED details when it explains the failed operation. Prefer specific operation names such as `list_creatives`, `sync_creatives`, `create_media_buy`, `get_media_buy_delivery`, or `get_creative_delivery` over broad category labels such as `reporting`.").optional(), "scope": z.enum(["account","identity","post","unknown"]).describe("Granularity of the downstream grant.").optional(), "status": z.enum(["connected","missing","pending","expired","revoked","not_required","unknown"]).describe("Current seller-observed state for this downstream connection when known. Product declarations MAY omit status or use `unknown`; AUTHORIZATION_REQUIRED details SHOULD use `missing`, `expired`, or `revoked` for the connection that blocked the call.").optional(), "connection_id": z.string().describe("Seller-defined identifier for an already-created downstream connection. Omit when no connection exists yet or when exposing it would leak platform/account state.").optional(), "resource_ref": z.object({ "platform_account_id": z.string().describe("Provider-native advertiser or business account id, when safe to disclose.").optional(), "identity_id": z.string().describe("Provider-native creator, page, channel, organization, or profile id, when safe to disclose.").optional(), "handle": z.string().describe("Provider-native public handle for the owning identity, when available.").optional(), "profile_url": z.string().url().describe("Public URL for the owning identity, when available.").optional(), "post_id": z.string().describe("Provider-native post id, when the grant is post-scoped or the failed request referenced a specific post.").optional(), "post_url": z.string().url().describe("Public URL for the referenced post, when available.").optional() }).catchall(z.any()).describe("Optional opaque provider-native resource hint, such as a platform account id, profile URL, handle, channel id, post id, or post URL. This is a hint for routing authorization, not proof that authorization exists.").optional(), "authorization_url": z.string().url().describe("Seller-hosted or provider-hosted URL where a human can complete or restore this downstream connection.").optional(), "authorization_instructions": z.string().describe("Human-readable instructions for completing or restoring this downstream connection.").optional(), "expires_at": z.string().datetime().describe("Expiration time for the downstream grant, when known.").optional() }).catchall(z.any()).and(z.any()).describe("A seller/platform-side connection or grant required by a product, format, or request. This is not the AdCP caller credential: the AdCP request is still authenticated once, and the seller uses these stored downstream connections to call a platform or service on the buyer's behalf. Use this shape for platforms that require more than one downstream grant, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.")).describe("Downstream platform connections or grants required to use this format declaration. These are in addition to the single AdCP caller credential. Use this when a platform product requires multiple downstream grants, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.").optional(), "reference_mutability": z.enum(["immutable_snapshot","mutable_requires_reapproval","mutable_auto_recheck"]).describe("Policy for formats whose `slots` accept a `published_post` reference. `immutable_snapshot`: seller snapshots the referenced post at approval and later source changes do not change the served creative. `mutable_requires_reapproval`: the source post may change and material changes require review before continued serving. `mutable_auto_recheck`: the source post may change and the seller continuously or periodically rechecks authorization/policy without requiring buyer resubmission. Omit when the format has no `published_post` slot.").optional(), "production_window_business_days": z.number().int().gte(0).describe("Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative).").optional() }).catchall(z.any()).describe("Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).").describe("VAST-tag-delivered video creative. Slot: `vast_tag` (vast asset, URL or inline XML, VAST 2.x-4.x). Tracking model: VAST events inherent to the spec — `impression`, `firstQuartile`, `midpoint`, `thirdQuartile`, `complete`, `start`, `pause`, `resume`, `mute`, `unmute`, `expand`, `collapse`, `fullscreen`, `creativeView`, `clickTracking`, `error`. VPAID interactivity via `vpaid_enabled: true` flag. SIMID extensions for interactive video supported as VAST extensions. Orientation is a parameter (vertical / horizontal / square). Distinct from `video_hosted` (direct file with external tracking).") }), z.object({ "format_kind": z.literal("audio_hosted"), "params": z.object({ "experimental": z.boolean().describe("When true, this canonical or seller narrowing may not work as declared. Adopters SHOULD preflight it with validate_input or in a sandbox and SHOULD NOT route production budget without testing; experimental status never makes the deprecated v1 path preferable. Drivers include unsettled spec shape, an adopter runtime gap, and custom shapes awaiting promotion. This replaces the earlier status plus runtime_status axes. Sellers SHOULD set experimental whenever a canonical or declaration is not production-ready.").default(false), "deprecated": z.boolean().describe("When true, this canonical (or a seller's specific narrowing of it) is going away. Existing adopters are supported through the deprecation cycle; new adoption is discouraged. Pair with `migration_target_version` to indicate when the canonical is expected to be removed. Distinct from `experimental`: an experimental canonical may stabilize and stop being experimental; a deprecated canonical is on a sunset path.").default(false), "v1_translatable": z.boolean().describe("Whether this canonical has any v1 named-format equivalent. `true` (default) — the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1→v2 projection via `v1-canonical-mapping.json` is meaningful. `false` — the canonical is inherently new in v2 and has no v1 form; v1's `list_creative_formats` couldn't express it because the underlying concept (algorithmic surface composition, AI-surface mentions, retail-media catalog placements, multi-card carousels) didn't exist as a v1 named-format archetype.\n\nLets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The 4 inherently-v2 canonicals at 3.1 GA: `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`.").default(true), "since_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version that introduced this canonical (e.g., '3.1', '3.2'). Lets adopters reason about minimum protocol version requirements when consuming a format declaration. Patch precision is intentionally rejected — canonicals are introduced at minor-version boundaries.").optional(), "migration_target_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version by which the working group expects this canonical to stabilize, surface a breaking revision, or (when `deprecated: true`) be removed. Patch precision is intentionally rejected — canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown').").optional(), "composition_model": z.enum(["deterministic","algorithmic"]).describe("Whether the surface composes deterministically (buyer can predict per-slot rendering — sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing — responsive_creative, agent_placement).").optional(), "provenance_required": z.boolean().describe("When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent.").optional(), "platform_extensions": z.array(z.object({ "uri": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL identifying the extension. `https://` is mandatory — `http://`, `file://`, `data:`, and other schemes are rejected at the schema layer (defense-in-depth on top of the fetch-contract normative rules). The URI base is the owning agent's URL; the path identifies the extension within that agent. Example: 'https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel'. The full fetch contract — SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds — is documented on `product-format-declaration.json#format_schema` and applies to ALL fetches of this reference shape regardless of whether the field is named `format_schema` (load-bearing for validation) or `platform_extensions` (informational); the *transport* rules are identical, only the *consumption* semantics differ."), "digest": z.string().regex(new RegExp("^sha256:[a-f0-9]{64}$")).describe("SHA-256 content digest of the extension definition (sha256:). Used to detect drift — if the agent revises the extension, the digest changes and cached definitions become invalid.") }).catchall(z.any()).describe("Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` — divergent digests in one response indicate producer-side error (e.g., concurrent extension revision mid-render). Buyers encountering divergent digests for the same URI MUST fail closed: treat all references to that URI as unresolved and surface a validation error rather than picking one branch silently. **Across responses**, digest divergence is normal — extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.")).describe("Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\n\n**Collision precedence (normative).** When two or more `platform_extensions[]` entries on the same declaration extend the same target (e.g., both extend `tracking`) with overlapping field names, **array order is authoritative — later entries override earlier ones on a per-field basis** (last-in-array-wins). SDKs MUST surface the overlap via the `errors[]` array on the `get_products` response with a structured code (`FORMAT_DECLARATION_DIVERGENT` is appropriate when the overlap appears across dual-emitted shapes; a producer-self-emitted overlap on a single declaration SHOULD use the same code with `error.details: { collision_kind: \"platform_extension_field\", target, overlapping_fields, winning_extension_uri }`). Producers SHOULD avoid the collision by emitting one extension per target or by partitioning fields across extensions; the deterministic precedence is for last-resort consistency across SDK implementations, not a sanctioned merging strategy.").optional(), "synthesis_nondeterministic": z.boolean().describe("When true, the format's production pipeline is genuinely nondeterministic — the platform cannot guarantee that synthesis from a given input set produces in-spec output. Veo / Sora / Runway-class generative video, and other AI-synthesis flows where output dimensions, duration, or quality vary per run. Implies a different validation contract: predictive `validate_input` is impossible; the platform's own post-synthesis QA loop applies; if the QA loop exhausts without producing a valid artifact, `build_creative` returns task_failed with a synthesis_failed reason. Distinct from `composition_model` (which describes how the surface composes per-slot rendering, not whether synthesis is deterministic). When false or absent, the format's production is predictable enough that `validate_input` can predict output properties from input properties.\n\n**Compatibility with `asset_source` / `item_production_model`**: `synthesis_nondeterministic: true` MAY pair with any of `seller_pre_rendered_from_brief`, `seller_human_designed`, or `agent_synthesized` (the QA loop is concept-level, not source-specific — 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.").default(false), "slots": z.array(z.object({ "asset_group_id": z.string().describe("Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings."), "asset_type": z.enum(["image","video","audio","text","markdown","url","html","css","javascript","vast","daast","webhook","brief","catalog","published_post","zip","card","object","pixel_tracker","vast_tracker","daast_tracker"]).describe("Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `published_post` is an existing-post reference asset, not uploaded media bytes and not a catalog row. `card` is the multi-card carousel element type (see card-asset.json). `pixel_tracker` / `vast_tracker` / `daast_tracker` are the renderer-fired measurement-tracker primitives — see `/schemas/core/assets/pixel-tracker-asset.json` and the VAST / DAAST tracker schemas. `object` is a last-resort fallback for structured non-asset inputs that don't fit any primitive asset_type — prefer specific types whenever possible."), "required": z.boolean().describe("Whether this slot is required for a valid manifest.").default(false), "min": z.number().int().gte(0).describe("Minimum count for repeatable / pool slots.").optional(), "max": z.number().int().gte(1).describe("Maximum count for repeatable / pool slots.").optional(), "max_chars": z.number().int().gte(1).describe("Per-slot character limit. Valid only when `asset_type` is `text`, `markdown`, or `brief`. Mutually exclusive with `max_size_kb` (which applies to binary asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "max_size_kb": z.number().int().gte(1).describe("Per-slot file size limit in kilobytes. Valid only when `asset_type` is `image`, `video`, `audio`, or `zip`. Mutually exclusive with `max_chars` (which applies to text asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Accepted intrinsic-pixel densities for this image-bearing slot. Valid when `asset_type` is `image`, and on a `card` slot where it constrains each card's image media (video media is unaffected). This makes density available to every canonical carrying image assets (native, carousel, responsive, companion images, and image itself), not only `format_kind: image`. When the image canonical also declares top-level `params.pixel_ratios`, the effective set is the intersection; an empty intersection is invalid. One matching asset satisfies the slot unless `required_pixel_ratios` requires rendition coverage.").optional(), "required_pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Required density coverage for an image rendition set. Valid only when `asset_type` is `image` and `pixel_ratios` is also declared. Every value MUST appear in the effective accepted set after intersecting any top-level image `params.pixel_ratios`, and the manifest slot value MUST be an array containing exactly one matching image rendition for each required ratio. Other accepted ratios remain optional. For example, `pixel_ratios: [1, 1.5, 2]` with `required_pixel_ratios: [1, 2]` requires the 1x and 2x renditions while making 1.5x optional. SDKs enforce intersection, subset, coverage, and duplicate-ratio rules because JSON Schema draft-07 cannot express them generically.").optional(), "logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("When `asset_group_id` is `logo`, renderer-facing brand.json logo slots acceptable for this format slot. Producers selecting from brand.json SHOULD prefer `logos[]` entries whose `slots[]` intersects this list, then apply `visual_guidelines.logo_usage_rules[]`.").optional(), "required_logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Subset of `logo_slots` for which this format expects explicit logo coverage. A manifest or brand-derived logo pool SHOULD include at least one usable logo for each required slot; if coverage is missing, builders SHOULD surface a validation warning or approval mapping instead of guessing from prose.").optional(), "description": z.string().describe("Human-readable description of what the slot expects from the buyer.").optional(), "consumed_for_production": z.boolean().describe("Dispatch hint for `build_creative` and v1↔v2 wire translators: when `true`, the slot's value is consumed as INPUT to a production step (host-read script, brief copy fed to generative synthesis, catalog feed driving per-SKU rendering) and is not rendered verbatim. When `false` (default), the slot's value is rendered verbatim on the placement (image bytes, video file, display tag).\n\nMotivates the v1↔v2 dispatch table: pre-v2 buyers shipped production-consumed inputs separately in a `inputs` map on the build_creative request; v2 collapses inputs and rendered assets into a single `assets` map keyed by `asset_group_id`. SDK translators between v1 and v2 use this flag per canonical to know which assets in the v2 manifest map back to v1 `inputs` vs v1 `assets`. Without the per-slot flag the dispatch table lives in adopter code and every SDK gets it slightly different.\n\nProducers SHOULD set this explicitly on slots whose consumption pattern isn't obvious (host-read scripts on `audio_hosted`, briefs on generative `video_hosted`, catalog feeds on `sponsored_placement`). For canonicals where every slot is render-verbatim (`image`, `display_tag`, `video_vast`), the default `false` is sufficient and the flag MAY be omitted.").default(false) }).catchall(z.any()).and(z.intersection(z.intersection(z.any(), z.intersection(z.any(), z.any())), z.intersection(z.any(), z.intersection(z.any(), z.any()))))).describe("Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.").optional(), "required_connections": z.array(z.object({ "provider": z.string().describe("Stable provider or platform namespace, preferably lowercase. Examples: `social.example`, `shortvideo.example`, or a seller-defined namespace. Omit only when the requirement is provider-agnostic, or when an `authorization_url` fully routes the human to the correct provider-specific connection flow.").optional(), "connection_type": z.enum(["advertiser_account","publisher_identity","post_authorization"]).describe("Kind of downstream connection required. `advertiser_account` is the platform account used to buy/manage ads. `publisher_identity` is the creator, page, channel, organization, or profile that owns source posts. `post_authorization` is a post-scoped grant when the platform authorizes individual posts instead of, or in addition to, the owning identity."), "required_for": z.array(z.string().min(1)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Concrete AdCP protocol operation names that require this downstream connection. Sellers SHOULD include this in product declarations when the requirement is known ahead of time, and in AUTHORIZATION_REQUIRED details when it explains the failed operation. Prefer specific operation names such as `list_creatives`, `sync_creatives`, `create_media_buy`, `get_media_buy_delivery`, or `get_creative_delivery` over broad category labels such as `reporting`.").optional(), "scope": z.enum(["account","identity","post","unknown"]).describe("Granularity of the downstream grant.").optional(), "status": z.enum(["connected","missing","pending","expired","revoked","not_required","unknown"]).describe("Current seller-observed state for this downstream connection when known. Product declarations MAY omit status or use `unknown`; AUTHORIZATION_REQUIRED details SHOULD use `missing`, `expired`, or `revoked` for the connection that blocked the call.").optional(), "connection_id": z.string().describe("Seller-defined identifier for an already-created downstream connection. Omit when no connection exists yet or when exposing it would leak platform/account state.").optional(), "resource_ref": z.object({ "platform_account_id": z.string().describe("Provider-native advertiser or business account id, when safe to disclose.").optional(), "identity_id": z.string().describe("Provider-native creator, page, channel, organization, or profile id, when safe to disclose.").optional(), "handle": z.string().describe("Provider-native public handle for the owning identity, when available.").optional(), "profile_url": z.string().url().describe("Public URL for the owning identity, when available.").optional(), "post_id": z.string().describe("Provider-native post id, when the grant is post-scoped or the failed request referenced a specific post.").optional(), "post_url": z.string().url().describe("Public URL for the referenced post, when available.").optional() }).catchall(z.any()).describe("Optional opaque provider-native resource hint, such as a platform account id, profile URL, handle, channel id, post id, or post URL. This is a hint for routing authorization, not proof that authorization exists.").optional(), "authorization_url": z.string().url().describe("Seller-hosted or provider-hosted URL where a human can complete or restore this downstream connection.").optional(), "authorization_instructions": z.string().describe("Human-readable instructions for completing or restoring this downstream connection.").optional(), "expires_at": z.string().datetime().describe("Expiration time for the downstream grant, when known.").optional() }).catchall(z.any()).and(z.any()).describe("A seller/platform-side connection or grant required by a product, format, or request. This is not the AdCP caller credential: the AdCP request is still authenticated once, and the seller uses these stored downstream connections to call a platform or service on the buyer's behalf. Use this shape for platforms that require more than one downstream grant, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.")).describe("Downstream platform connections or grants required to use this format declaration. These are in addition to the single AdCP caller credential. Use this when a platform product requires multiple downstream grants, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.").optional(), "reference_mutability": z.enum(["immutable_snapshot","mutable_requires_reapproval","mutable_auto_recheck"]).describe("Policy for formats whose `slots` accept a `published_post` reference. `immutable_snapshot`: seller snapshots the referenced post at approval and later source changes do not change the served creative. `mutable_requires_reapproval`: the source post may change and material changes require review before continued serving. `mutable_auto_recheck`: the source post may change and the seller continuously or periodically rechecks authorization/policy without requiring buyer resubmission. Omit when the format has no `published_post` slot.").optional(), "production_window_business_days": z.number().int().gte(0).describe("Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative).").optional() }).catchall(z.any()).describe("Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).").describe("Direct audio creative — buyer ships an `audio` asset (mp3/aac/wav) for asset-driven products, or ships a `script` / `creative_brief` text asset for products where the seller produces audio internally (podcast host-reads, TTS synthesis). Optional companion slots: `companion_image`, `brand_name`, `landing_page_url`. Tracking model: standard impression + completion + companion-image-click pixels via universal_macros. Distinct from `audio_daast` (DAAST tag, inherent DAAST event tracking). For host-reads and synthesized audio, the format declares `asset_source: 'publisher_host_recorded'` or `'agent_synthesized'` plus `buyer_asset_acceptance: 'rejected'`; the format's `slots` declaration enumerates which assets the buyer ships (e.g., `script` text asset for host-reads). The seller decides how to consume each asset (render verbatim vs produce audio from text) — there is no separate manifest 'inputs' map; everything the buyer ships goes in `assets`.") }), z.object({ "format_kind": z.literal("audio_daast"), "params": z.object({ "experimental": z.boolean().describe("When true, this canonical or seller narrowing may not work as declared. Adopters SHOULD preflight it with validate_input or in a sandbox and SHOULD NOT route production budget without testing; experimental status never makes the deprecated v1 path preferable. Drivers include unsettled spec shape, an adopter runtime gap, and custom shapes awaiting promotion. This replaces the earlier status plus runtime_status axes. Sellers SHOULD set experimental whenever a canonical or declaration is not production-ready.").default(false), "deprecated": z.boolean().describe("When true, this canonical (or a seller's specific narrowing of it) is going away. Existing adopters are supported through the deprecation cycle; new adoption is discouraged. Pair with `migration_target_version` to indicate when the canonical is expected to be removed. Distinct from `experimental`: an experimental canonical may stabilize and stop being experimental; a deprecated canonical is on a sunset path.").default(false), "v1_translatable": z.boolean().describe("Whether this canonical has any v1 named-format equivalent. `true` (default) — the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1→v2 projection via `v1-canonical-mapping.json` is meaningful. `false` — the canonical is inherently new in v2 and has no v1 form; v1's `list_creative_formats` couldn't express it because the underlying concept (algorithmic surface composition, AI-surface mentions, retail-media catalog placements, multi-card carousels) didn't exist as a v1 named-format archetype.\n\nLets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The 4 inherently-v2 canonicals at 3.1 GA: `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`.").default(true), "since_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version that introduced this canonical (e.g., '3.1', '3.2'). Lets adopters reason about minimum protocol version requirements when consuming a format declaration. Patch precision is intentionally rejected — canonicals are introduced at minor-version boundaries.").optional(), "migration_target_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version by which the working group expects this canonical to stabilize, surface a breaking revision, or (when `deprecated: true`) be removed. Patch precision is intentionally rejected — canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown').").optional(), "composition_model": z.enum(["deterministic","algorithmic"]).describe("Whether the surface composes deterministically (buyer can predict per-slot rendering — sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing — responsive_creative, agent_placement).").optional(), "provenance_required": z.boolean().describe("When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent.").optional(), "platform_extensions": z.array(z.object({ "uri": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL identifying the extension. `https://` is mandatory — `http://`, `file://`, `data:`, and other schemes are rejected at the schema layer (defense-in-depth on top of the fetch-contract normative rules). The URI base is the owning agent's URL; the path identifies the extension within that agent. Example: 'https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel'. The full fetch contract — SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds — is documented on `product-format-declaration.json#format_schema` and applies to ALL fetches of this reference shape regardless of whether the field is named `format_schema` (load-bearing for validation) or `platform_extensions` (informational); the *transport* rules are identical, only the *consumption* semantics differ."), "digest": z.string().regex(new RegExp("^sha256:[a-f0-9]{64}$")).describe("SHA-256 content digest of the extension definition (sha256:). Used to detect drift — if the agent revises the extension, the digest changes and cached definitions become invalid.") }).catchall(z.any()).describe("Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` — divergent digests in one response indicate producer-side error (e.g., concurrent extension revision mid-render). Buyers encountering divergent digests for the same URI MUST fail closed: treat all references to that URI as unresolved and surface a validation error rather than picking one branch silently. **Across responses**, digest divergence is normal — extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.")).describe("Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\n\n**Collision precedence (normative).** When two or more `platform_extensions[]` entries on the same declaration extend the same target (e.g., both extend `tracking`) with overlapping field names, **array order is authoritative — later entries override earlier ones on a per-field basis** (last-in-array-wins). SDKs MUST surface the overlap via the `errors[]` array on the `get_products` response with a structured code (`FORMAT_DECLARATION_DIVERGENT` is appropriate when the overlap appears across dual-emitted shapes; a producer-self-emitted overlap on a single declaration SHOULD use the same code with `error.details: { collision_kind: \"platform_extension_field\", target, overlapping_fields, winning_extension_uri }`). Producers SHOULD avoid the collision by emitting one extension per target or by partitioning fields across extensions; the deterministic precedence is for last-resort consistency across SDK implementations, not a sanctioned merging strategy.").optional(), "synthesis_nondeterministic": z.boolean().describe("When true, the format's production pipeline is genuinely nondeterministic — the platform cannot guarantee that synthesis from a given input set produces in-spec output. Veo / Sora / Runway-class generative video, and other AI-synthesis flows where output dimensions, duration, or quality vary per run. Implies a different validation contract: predictive `validate_input` is impossible; the platform's own post-synthesis QA loop applies; if the QA loop exhausts without producing a valid artifact, `build_creative` returns task_failed with a synthesis_failed reason. Distinct from `composition_model` (which describes how the surface composes per-slot rendering, not whether synthesis is deterministic). When false or absent, the format's production is predictable enough that `validate_input` can predict output properties from input properties.\n\n**Compatibility with `asset_source` / `item_production_model`**: `synthesis_nondeterministic: true` MAY pair with any of `seller_pre_rendered_from_brief`, `seller_human_designed`, or `agent_synthesized` (the QA loop is concept-level, not source-specific — 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.").default(false), "slots": z.array(z.object({ "asset_group_id": z.string().describe("Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings."), "asset_type": z.enum(["image","video","audio","text","markdown","url","html","css","javascript","vast","daast","webhook","brief","catalog","published_post","zip","card","object","pixel_tracker","vast_tracker","daast_tracker"]).describe("Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `published_post` is an existing-post reference asset, not uploaded media bytes and not a catalog row. `card` is the multi-card carousel element type (see card-asset.json). `pixel_tracker` / `vast_tracker` / `daast_tracker` are the renderer-fired measurement-tracker primitives — see `/schemas/core/assets/pixel-tracker-asset.json` and the VAST / DAAST tracker schemas. `object` is a last-resort fallback for structured non-asset inputs that don't fit any primitive asset_type — prefer specific types whenever possible."), "required": z.boolean().describe("Whether this slot is required for a valid manifest.").default(false), "min": z.number().int().gte(0).describe("Minimum count for repeatable / pool slots.").optional(), "max": z.number().int().gte(1).describe("Maximum count for repeatable / pool slots.").optional(), "max_chars": z.number().int().gte(1).describe("Per-slot character limit. Valid only when `asset_type` is `text`, `markdown`, or `brief`. Mutually exclusive with `max_size_kb` (which applies to binary asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "max_size_kb": z.number().int().gte(1).describe("Per-slot file size limit in kilobytes. Valid only when `asset_type` is `image`, `video`, `audio`, or `zip`. Mutually exclusive with `max_chars` (which applies to text asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Accepted intrinsic-pixel densities for this image-bearing slot. Valid when `asset_type` is `image`, and on a `card` slot where it constrains each card's image media (video media is unaffected). This makes density available to every canonical carrying image assets (native, carousel, responsive, companion images, and image itself), not only `format_kind: image`. When the image canonical also declares top-level `params.pixel_ratios`, the effective set is the intersection; an empty intersection is invalid. One matching asset satisfies the slot unless `required_pixel_ratios` requires rendition coverage.").optional(), "required_pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Required density coverage for an image rendition set. Valid only when `asset_type` is `image` and `pixel_ratios` is also declared. Every value MUST appear in the effective accepted set after intersecting any top-level image `params.pixel_ratios`, and the manifest slot value MUST be an array containing exactly one matching image rendition for each required ratio. Other accepted ratios remain optional. For example, `pixel_ratios: [1, 1.5, 2]` with `required_pixel_ratios: [1, 2]` requires the 1x and 2x renditions while making 1.5x optional. SDKs enforce intersection, subset, coverage, and duplicate-ratio rules because JSON Schema draft-07 cannot express them generically.").optional(), "logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("When `asset_group_id` is `logo`, renderer-facing brand.json logo slots acceptable for this format slot. Producers selecting from brand.json SHOULD prefer `logos[]` entries whose `slots[]` intersects this list, then apply `visual_guidelines.logo_usage_rules[]`.").optional(), "required_logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Subset of `logo_slots` for which this format expects explicit logo coverage. A manifest or brand-derived logo pool SHOULD include at least one usable logo for each required slot; if coverage is missing, builders SHOULD surface a validation warning or approval mapping instead of guessing from prose.").optional(), "description": z.string().describe("Human-readable description of what the slot expects from the buyer.").optional(), "consumed_for_production": z.boolean().describe("Dispatch hint for `build_creative` and v1↔v2 wire translators: when `true`, the slot's value is consumed as INPUT to a production step (host-read script, brief copy fed to generative synthesis, catalog feed driving per-SKU rendering) and is not rendered verbatim. When `false` (default), the slot's value is rendered verbatim on the placement (image bytes, video file, display tag).\n\nMotivates the v1↔v2 dispatch table: pre-v2 buyers shipped production-consumed inputs separately in a `inputs` map on the build_creative request; v2 collapses inputs and rendered assets into a single `assets` map keyed by `asset_group_id`. SDK translators between v1 and v2 use this flag per canonical to know which assets in the v2 manifest map back to v1 `inputs` vs v1 `assets`. Without the per-slot flag the dispatch table lives in adopter code and every SDK gets it slightly different.\n\nProducers SHOULD set this explicitly on slots whose consumption pattern isn't obvious (host-read scripts on `audio_hosted`, briefs on generative `video_hosted`, catalog feeds on `sponsored_placement`). For canonicals where every slot is render-verbatim (`image`, `display_tag`, `video_vast`), the default `false` is sufficient and the flag MAY be omitted.").default(false) }).catchall(z.any()).and(z.intersection(z.intersection(z.any(), z.intersection(z.any(), z.any())), z.intersection(z.any(), z.intersection(z.any(), z.any()))))).describe("Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.").optional(), "required_connections": z.array(z.object({ "provider": z.string().describe("Stable provider or platform namespace, preferably lowercase. Examples: `social.example`, `shortvideo.example`, or a seller-defined namespace. Omit only when the requirement is provider-agnostic, or when an `authorization_url` fully routes the human to the correct provider-specific connection flow.").optional(), "connection_type": z.enum(["advertiser_account","publisher_identity","post_authorization"]).describe("Kind of downstream connection required. `advertiser_account` is the platform account used to buy/manage ads. `publisher_identity` is the creator, page, channel, organization, or profile that owns source posts. `post_authorization` is a post-scoped grant when the platform authorizes individual posts instead of, or in addition to, the owning identity."), "required_for": z.array(z.string().min(1)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Concrete AdCP protocol operation names that require this downstream connection. Sellers SHOULD include this in product declarations when the requirement is known ahead of time, and in AUTHORIZATION_REQUIRED details when it explains the failed operation. Prefer specific operation names such as `list_creatives`, `sync_creatives`, `create_media_buy`, `get_media_buy_delivery`, or `get_creative_delivery` over broad category labels such as `reporting`.").optional(), "scope": z.enum(["account","identity","post","unknown"]).describe("Granularity of the downstream grant.").optional(), "status": z.enum(["connected","missing","pending","expired","revoked","not_required","unknown"]).describe("Current seller-observed state for this downstream connection when known. Product declarations MAY omit status or use `unknown`; AUTHORIZATION_REQUIRED details SHOULD use `missing`, `expired`, or `revoked` for the connection that blocked the call.").optional(), "connection_id": z.string().describe("Seller-defined identifier for an already-created downstream connection. Omit when no connection exists yet or when exposing it would leak platform/account state.").optional(), "resource_ref": z.object({ "platform_account_id": z.string().describe("Provider-native advertiser or business account id, when safe to disclose.").optional(), "identity_id": z.string().describe("Provider-native creator, page, channel, organization, or profile id, when safe to disclose.").optional(), "handle": z.string().describe("Provider-native public handle for the owning identity, when available.").optional(), "profile_url": z.string().url().describe("Public URL for the owning identity, when available.").optional(), "post_id": z.string().describe("Provider-native post id, when the grant is post-scoped or the failed request referenced a specific post.").optional(), "post_url": z.string().url().describe("Public URL for the referenced post, when available.").optional() }).catchall(z.any()).describe("Optional opaque provider-native resource hint, such as a platform account id, profile URL, handle, channel id, post id, or post URL. This is a hint for routing authorization, not proof that authorization exists.").optional(), "authorization_url": z.string().url().describe("Seller-hosted or provider-hosted URL where a human can complete or restore this downstream connection.").optional(), "authorization_instructions": z.string().describe("Human-readable instructions for completing or restoring this downstream connection.").optional(), "expires_at": z.string().datetime().describe("Expiration time for the downstream grant, when known.").optional() }).catchall(z.any()).and(z.any()).describe("A seller/platform-side connection or grant required by a product, format, or request. This is not the AdCP caller credential: the AdCP request is still authenticated once, and the seller uses these stored downstream connections to call a platform or service on the buyer's behalf. Use this shape for platforms that require more than one downstream grant, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.")).describe("Downstream platform connections or grants required to use this format declaration. These are in addition to the single AdCP caller credential. Use this when a platform product requires multiple downstream grants, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.").optional(), "reference_mutability": z.enum(["immutable_snapshot","mutable_requires_reapproval","mutable_auto_recheck"]).describe("Policy for formats whose `slots` accept a `published_post` reference. `immutable_snapshot`: seller snapshots the referenced post at approval and later source changes do not change the served creative. `mutable_requires_reapproval`: the source post may change and material changes require review before continued serving. `mutable_auto_recheck`: the source post may change and the seller continuously or periodically rechecks authorization/policy without requiring buyer resubmission. Omit when the format has no `published_post` slot.").optional(), "production_window_business_days": z.number().int().gte(0).describe("Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative).").optional() }).catchall(z.any()).describe("Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).").describe("DAAST-tag-delivered audio creative (audio analog of VAST). Slot: `daast_tag` (daast asset, URL or inline XML). Tracking model: DAAST events inherent to the spec — `impression`, `firstQuartile`, `midpoint`, `thirdQuartile`, `complete`, `start`, `pause`, `resume`, `mute`, `unmute`, `clickTracking`, `error`. Distinct from `audio_hosted` (direct file with external tracking).") }), z.object({ "format_kind": z.literal("sponsored_placement"), "params": z.object({ "experimental": z.boolean().describe("When true, this canonical or seller narrowing may not work as declared. Adopters SHOULD preflight it with validate_input or in a sandbox and SHOULD NOT route production budget without testing; experimental status never makes the deprecated v1 path preferable. Drivers include unsettled spec shape, an adopter runtime gap, and custom shapes awaiting promotion. This replaces the earlier status plus runtime_status axes. Sellers SHOULD set experimental whenever a canonical or declaration is not production-ready.").default(false), "deprecated": z.boolean().describe("When true, this canonical (or a seller's specific narrowing of it) is going away. Existing adopters are supported through the deprecation cycle; new adoption is discouraged. Pair with `migration_target_version` to indicate when the canonical is expected to be removed. Distinct from `experimental`: an experimental canonical may stabilize and stop being experimental; a deprecated canonical is on a sunset path.").default(false), "v1_translatable": z.boolean().describe("Whether this canonical has any v1 named-format equivalent. `true` (default) — the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1→v2 projection via `v1-canonical-mapping.json` is meaningful. `false` — the canonical is inherently new in v2 and has no v1 form; v1's `list_creative_formats` couldn't express it because the underlying concept (algorithmic surface composition, AI-surface mentions, retail-media catalog placements, multi-card carousels) didn't exist as a v1 named-format archetype.\n\nLets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The 4 inherently-v2 canonicals at 3.1 GA: `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`.").default(true), "since_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version that introduced this canonical (e.g., '3.1', '3.2'). Lets adopters reason about minimum protocol version requirements when consuming a format declaration. Patch precision is intentionally rejected — canonicals are introduced at minor-version boundaries.").optional(), "migration_target_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version by which the working group expects this canonical to stabilize, surface a breaking revision, or (when `deprecated: true`) be removed. Patch precision is intentionally rejected — canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown').").optional(), "composition_model": z.enum(["deterministic","algorithmic"]).describe("Whether the surface composes deterministically (buyer can predict per-slot rendering — sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing — responsive_creative, agent_placement).").optional(), "provenance_required": z.boolean().describe("When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent.").optional(), "platform_extensions": z.array(z.object({ "uri": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL identifying the extension. `https://` is mandatory — `http://`, `file://`, `data:`, and other schemes are rejected at the schema layer (defense-in-depth on top of the fetch-contract normative rules). The URI base is the owning agent's URL; the path identifies the extension within that agent. Example: 'https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel'. The full fetch contract — SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds — is documented on `product-format-declaration.json#format_schema` and applies to ALL fetches of this reference shape regardless of whether the field is named `format_schema` (load-bearing for validation) or `platform_extensions` (informational); the *transport* rules are identical, only the *consumption* semantics differ."), "digest": z.string().regex(new RegExp("^sha256:[a-f0-9]{64}$")).describe("SHA-256 content digest of the extension definition (sha256:). Used to detect drift — if the agent revises the extension, the digest changes and cached definitions become invalid.") }).catchall(z.any()).describe("Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` — divergent digests in one response indicate producer-side error (e.g., concurrent extension revision mid-render). Buyers encountering divergent digests for the same URI MUST fail closed: treat all references to that URI as unresolved and surface a validation error rather than picking one branch silently. **Across responses**, digest divergence is normal — extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.")).describe("Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\n\n**Collision precedence (normative).** When two or more `platform_extensions[]` entries on the same declaration extend the same target (e.g., both extend `tracking`) with overlapping field names, **array order is authoritative — later entries override earlier ones on a per-field basis** (last-in-array-wins). SDKs MUST surface the overlap via the `errors[]` array on the `get_products` response with a structured code (`FORMAT_DECLARATION_DIVERGENT` is appropriate when the overlap appears across dual-emitted shapes; a producer-self-emitted overlap on a single declaration SHOULD use the same code with `error.details: { collision_kind: \"platform_extension_field\", target, overlapping_fields, winning_extension_uri }`). Producers SHOULD avoid the collision by emitting one extension per target or by partitioning fields across extensions; the deterministic precedence is for last-resort consistency across SDK implementations, not a sanctioned merging strategy.").optional(), "synthesis_nondeterministic": z.boolean().describe("When true, the format's production pipeline is genuinely nondeterministic — the platform cannot guarantee that synthesis from a given input set produces in-spec output. Veo / Sora / Runway-class generative video, and other AI-synthesis flows where output dimensions, duration, or quality vary per run. Implies a different validation contract: predictive `validate_input` is impossible; the platform's own post-synthesis QA loop applies; if the QA loop exhausts without producing a valid artifact, `build_creative` returns task_failed with a synthesis_failed reason. Distinct from `composition_model` (which describes how the surface composes per-slot rendering, not whether synthesis is deterministic). When false or absent, the format's production is predictable enough that `validate_input` can predict output properties from input properties.\n\n**Compatibility with `asset_source` / `item_production_model`**: `synthesis_nondeterministic: true` MAY pair with any of `seller_pre_rendered_from_brief`, `seller_human_designed`, or `agent_synthesized` (the QA loop is concept-level, not source-specific — 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.").default(false), "slots": z.array(z.object({ "asset_group_id": z.string().describe("Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings."), "asset_type": z.enum(["image","video","audio","text","markdown","url","html","css","javascript","vast","daast","webhook","brief","catalog","published_post","zip","card","object","pixel_tracker","vast_tracker","daast_tracker"]).describe("Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `published_post` is an existing-post reference asset, not uploaded media bytes and not a catalog row. `card` is the multi-card carousel element type (see card-asset.json). `pixel_tracker` / `vast_tracker` / `daast_tracker` are the renderer-fired measurement-tracker primitives — see `/schemas/core/assets/pixel-tracker-asset.json` and the VAST / DAAST tracker schemas. `object` is a last-resort fallback for structured non-asset inputs that don't fit any primitive asset_type — prefer specific types whenever possible."), "required": z.boolean().describe("Whether this slot is required for a valid manifest.").default(false), "min": z.number().int().gte(0).describe("Minimum count for repeatable / pool slots.").optional(), "max": z.number().int().gte(1).describe("Maximum count for repeatable / pool slots.").optional(), "max_chars": z.number().int().gte(1).describe("Per-slot character limit. Valid only when `asset_type` is `text`, `markdown`, or `brief`. Mutually exclusive with `max_size_kb` (which applies to binary asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "max_size_kb": z.number().int().gte(1).describe("Per-slot file size limit in kilobytes. Valid only when `asset_type` is `image`, `video`, `audio`, or `zip`. Mutually exclusive with `max_chars` (which applies to text asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Accepted intrinsic-pixel densities for this image-bearing slot. Valid when `asset_type` is `image`, and on a `card` slot where it constrains each card's image media (video media is unaffected). This makes density available to every canonical carrying image assets (native, carousel, responsive, companion images, and image itself), not only `format_kind: image`. When the image canonical also declares top-level `params.pixel_ratios`, the effective set is the intersection; an empty intersection is invalid. One matching asset satisfies the slot unless `required_pixel_ratios` requires rendition coverage.").optional(), "required_pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Required density coverage for an image rendition set. Valid only when `asset_type` is `image` and `pixel_ratios` is also declared. Every value MUST appear in the effective accepted set after intersecting any top-level image `params.pixel_ratios`, and the manifest slot value MUST be an array containing exactly one matching image rendition for each required ratio. Other accepted ratios remain optional. For example, `pixel_ratios: [1, 1.5, 2]` with `required_pixel_ratios: [1, 2]` requires the 1x and 2x renditions while making 1.5x optional. SDKs enforce intersection, subset, coverage, and duplicate-ratio rules because JSON Schema draft-07 cannot express them generically.").optional(), "logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("When `asset_group_id` is `logo`, renderer-facing brand.json logo slots acceptable for this format slot. Producers selecting from brand.json SHOULD prefer `logos[]` entries whose `slots[]` intersects this list, then apply `visual_guidelines.logo_usage_rules[]`.").optional(), "required_logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Subset of `logo_slots` for which this format expects explicit logo coverage. A manifest or brand-derived logo pool SHOULD include at least one usable logo for each required slot; if coverage is missing, builders SHOULD surface a validation warning or approval mapping instead of guessing from prose.").optional(), "description": z.string().describe("Human-readable description of what the slot expects from the buyer.").optional(), "consumed_for_production": z.boolean().describe("Dispatch hint for `build_creative` and v1↔v2 wire translators: when `true`, the slot's value is consumed as INPUT to a production step (host-read script, brief copy fed to generative synthesis, catalog feed driving per-SKU rendering) and is not rendered verbatim. When `false` (default), the slot's value is rendered verbatim on the placement (image bytes, video file, display tag).\n\nMotivates the v1↔v2 dispatch table: pre-v2 buyers shipped production-consumed inputs separately in a `inputs` map on the build_creative request; v2 collapses inputs and rendered assets into a single `assets` map keyed by `asset_group_id`. SDK translators between v1 and v2 use this flag per canonical to know which assets in the v2 manifest map back to v1 `inputs` vs v1 `assets`. Without the per-slot flag the dispatch table lives in adopter code and every SDK gets it slightly different.\n\nProducers SHOULD set this explicitly on slots whose consumption pattern isn't obvious (host-read scripts on `audio_hosted`, briefs on generative `video_hosted`, catalog feeds on `sponsored_placement`). For canonicals where every slot is render-verbatim (`image`, `display_tag`, `video_vast`), the default `false` is sufficient and the flag MAY be omitted.").default(false) }).catchall(z.any()).and(z.intersection(z.intersection(z.any(), z.intersection(z.any(), z.any())), z.intersection(z.any(), z.intersection(z.any(), z.any()))))).describe("Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.").optional(), "required_connections": z.array(z.object({ "provider": z.string().describe("Stable provider or platform namespace, preferably lowercase. Examples: `social.example`, `shortvideo.example`, or a seller-defined namespace. Omit only when the requirement is provider-agnostic, or when an `authorization_url` fully routes the human to the correct provider-specific connection flow.").optional(), "connection_type": z.enum(["advertiser_account","publisher_identity","post_authorization"]).describe("Kind of downstream connection required. `advertiser_account` is the platform account used to buy/manage ads. `publisher_identity` is the creator, page, channel, organization, or profile that owns source posts. `post_authorization` is a post-scoped grant when the platform authorizes individual posts instead of, or in addition to, the owning identity."), "required_for": z.array(z.string().min(1)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Concrete AdCP protocol operation names that require this downstream connection. Sellers SHOULD include this in product declarations when the requirement is known ahead of time, and in AUTHORIZATION_REQUIRED details when it explains the failed operation. Prefer specific operation names such as `list_creatives`, `sync_creatives`, `create_media_buy`, `get_media_buy_delivery`, or `get_creative_delivery` over broad category labels such as `reporting`.").optional(), "scope": z.enum(["account","identity","post","unknown"]).describe("Granularity of the downstream grant.").optional(), "status": z.enum(["connected","missing","pending","expired","revoked","not_required","unknown"]).describe("Current seller-observed state for this downstream connection when known. Product declarations MAY omit status or use `unknown`; AUTHORIZATION_REQUIRED details SHOULD use `missing`, `expired`, or `revoked` for the connection that blocked the call.").optional(), "connection_id": z.string().describe("Seller-defined identifier for an already-created downstream connection. Omit when no connection exists yet or when exposing it would leak platform/account state.").optional(), "resource_ref": z.object({ "platform_account_id": z.string().describe("Provider-native advertiser or business account id, when safe to disclose.").optional(), "identity_id": z.string().describe("Provider-native creator, page, channel, organization, or profile id, when safe to disclose.").optional(), "handle": z.string().describe("Provider-native public handle for the owning identity, when available.").optional(), "profile_url": z.string().url().describe("Public URL for the owning identity, when available.").optional(), "post_id": z.string().describe("Provider-native post id, when the grant is post-scoped or the failed request referenced a specific post.").optional(), "post_url": z.string().url().describe("Public URL for the referenced post, when available.").optional() }).catchall(z.any()).describe("Optional opaque provider-native resource hint, such as a platform account id, profile URL, handle, channel id, post id, or post URL. This is a hint for routing authorization, not proof that authorization exists.").optional(), "authorization_url": z.string().url().describe("Seller-hosted or provider-hosted URL where a human can complete or restore this downstream connection.").optional(), "authorization_instructions": z.string().describe("Human-readable instructions for completing or restoring this downstream connection.").optional(), "expires_at": z.string().datetime().describe("Expiration time for the downstream grant, when known.").optional() }).catchall(z.any()).and(z.any()).describe("A seller/platform-side connection or grant required by a product, format, or request. This is not the AdCP caller credential: the AdCP request is still authenticated once, and the seller uses these stored downstream connections to call a platform or service on the buyer's behalf. Use this shape for platforms that require more than one downstream grant, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.")).describe("Downstream platform connections or grants required to use this format declaration. These are in addition to the single AdCP caller credential. Use this when a platform product requires multiple downstream grants, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.").optional(), "reference_mutability": z.enum(["immutable_snapshot","mutable_requires_reapproval","mutable_auto_recheck"]).describe("Policy for formats whose `slots` accept a `published_post` reference. `immutable_snapshot`: seller snapshots the referenced post at approval and later source changes do not change the served creative. `mutable_requires_reapproval`: the source post may change and material changes require review before continued serving. `mutable_auto_recheck`: the source post may change and the seller continuously or periodically rechecks authorization/policy without requiring buyer resubmission. Omit when the format has no `published_post` slot.").optional(), "production_window_business_days": z.number().int().gte(0).describe("Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative).").optional() }).catchall(z.any()).describe("Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).").describe("Catalog-driven retail-media format. Slot: `source_catalog` (catalog asset — product/SKU/ASIN/GTIN catalog reference, REQUIRED), optional `hero_asset`, optional `landing_page_url`. Buyer supplies the catalog reference; surface composes per-item or multi-item rendering using its native placement template. **Composition is deterministic** — buyer can predict per-slot rendering from the catalog item structure. Tracking model: per-item impression + click + conversion (catalog-keyed via offering_id/sku/gtin macros). Covers Amazon Sponsored Products, Criteo Sponsored Products, CitrusAd Sponsored Products, Walmart Connect Sponsored Products, Pinterest Collection (catalog-driven mode).\n\n**Scope (normative — buyer-agent routing).** This canonical is the home for catalog-driven retail-media placements ONLY. The defining feature is the `source_catalog` slot — products under this canonical compose their creative *per catalog item* using the buyer-supplied catalog feed. Without a catalog feed there is nothing to render against. Buyer agents reading `format_kind: sponsored_placement` MUST attach a catalog reference; sellers MUST require `source_catalog` in the manifest.\n\n**Not this canonical (route elsewhere):**\n- IAB in-feed native ads, content-recommendation widgets (Taboola, Outbrain, Yahoo Native, AdMob Native, in-feed sponsored cards) — use `native_in_feed` (asset-bundle composition; no catalog).\n- Algorithmic surface that picks from a buyer-supplied asset pool (Google PMax, Meta Advantage+) — use `responsive_creative`.\n- Single-image or single-video creative — use `image` or `video_hosted`.\n\nThe earlier broader framing ('any sponsored placement') was too loose for buyer-agent routing — a buyer reading `sponsored_placement` couldn't disambiguate a catalog-driven Amazon SP from an in-feed Taboola widget. As of 3.1, the canonical is narrowed to catalog-keyed retail-media; native moves to `native_in_feed`. Distinct from `responsive_creative` (algorithmic combinator from buyer pool) and `agent_placement` (text/audio AI-surface composition).") }), z.object({ "format_kind": z.literal("native_in_feed"), "params": z.object({ "experimental": z.boolean().describe("When true, this canonical or seller narrowing may not work as declared. Adopters SHOULD preflight it with validate_input or in a sandbox and SHOULD NOT route production budget without testing; experimental status never makes the deprecated v1 path preferable. Drivers include unsettled spec shape, an adopter runtime gap, and custom shapes awaiting promotion. This replaces the earlier status plus runtime_status axes. Sellers SHOULD set experimental whenever a canonical or declaration is not production-ready.").default(false), "deprecated": z.boolean().describe("When true, this canonical (or a seller's specific narrowing of it) is going away. Existing adopters are supported through the deprecation cycle; new adoption is discouraged. Pair with `migration_target_version` to indicate when the canonical is expected to be removed. Distinct from `experimental`: an experimental canonical may stabilize and stop being experimental; a deprecated canonical is on a sunset path.").default(false), "v1_translatable": z.boolean().describe("Whether this canonical has any v1 named-format equivalent. `true` (default) — the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1→v2 projection via `v1-canonical-mapping.json` is meaningful. `false` — the canonical is inherently new in v2 and has no v1 form; v1's `list_creative_formats` couldn't express it because the underlying concept (algorithmic surface composition, AI-surface mentions, retail-media catalog placements, multi-card carousels) didn't exist as a v1 named-format archetype.\n\nLets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The 4 inherently-v2 canonicals at 3.1 GA: `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`.").default(true), "since_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version that introduced this canonical (e.g., '3.1', '3.2'). Lets adopters reason about minimum protocol version requirements when consuming a format declaration. Patch precision is intentionally rejected — canonicals are introduced at minor-version boundaries.").optional(), "migration_target_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version by which the working group expects this canonical to stabilize, surface a breaking revision, or (when `deprecated: true`) be removed. Patch precision is intentionally rejected — canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown').").optional(), "composition_model": z.enum(["deterministic","algorithmic"]).describe("Whether the surface composes deterministically (buyer can predict per-slot rendering — sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing — responsive_creative, agent_placement).").optional(), "provenance_required": z.boolean().describe("When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent.").optional(), "platform_extensions": z.array(z.object({ "uri": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL identifying the extension. `https://` is mandatory — `http://`, `file://`, `data:`, and other schemes are rejected at the schema layer (defense-in-depth on top of the fetch-contract normative rules). The URI base is the owning agent's URL; the path identifies the extension within that agent. Example: 'https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel'. The full fetch contract — SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds — is documented on `product-format-declaration.json#format_schema` and applies to ALL fetches of this reference shape regardless of whether the field is named `format_schema` (load-bearing for validation) or `platform_extensions` (informational); the *transport* rules are identical, only the *consumption* semantics differ."), "digest": z.string().regex(new RegExp("^sha256:[a-f0-9]{64}$")).describe("SHA-256 content digest of the extension definition (sha256:). Used to detect drift — if the agent revises the extension, the digest changes and cached definitions become invalid.") }).catchall(z.any()).describe("Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` — divergent digests in one response indicate producer-side error (e.g., concurrent extension revision mid-render). Buyers encountering divergent digests for the same URI MUST fail closed: treat all references to that URI as unresolved and surface a validation error rather than picking one branch silently. **Across responses**, digest divergence is normal — extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.")).describe("Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\n\n**Collision precedence (normative).** When two or more `platform_extensions[]` entries on the same declaration extend the same target (e.g., both extend `tracking`) with overlapping field names, **array order is authoritative — later entries override earlier ones on a per-field basis** (last-in-array-wins). SDKs MUST surface the overlap via the `errors[]` array on the `get_products` response with a structured code (`FORMAT_DECLARATION_DIVERGENT` is appropriate when the overlap appears across dual-emitted shapes; a producer-self-emitted overlap on a single declaration SHOULD use the same code with `error.details: { collision_kind: \"platform_extension_field\", target, overlapping_fields, winning_extension_uri }`). Producers SHOULD avoid the collision by emitting one extension per target or by partitioning fields across extensions; the deterministic precedence is for last-resort consistency across SDK implementations, not a sanctioned merging strategy.").optional(), "synthesis_nondeterministic": z.boolean().describe("When true, the format's production pipeline is genuinely nondeterministic — the platform cannot guarantee that synthesis from a given input set produces in-spec output. Veo / Sora / Runway-class generative video, and other AI-synthesis flows where output dimensions, duration, or quality vary per run. Implies a different validation contract: predictive `validate_input` is impossible; the platform's own post-synthesis QA loop applies; if the QA loop exhausts without producing a valid artifact, `build_creative` returns task_failed with a synthesis_failed reason. Distinct from `composition_model` (which describes how the surface composes per-slot rendering, not whether synthesis is deterministic). When false or absent, the format's production is predictable enough that `validate_input` can predict output properties from input properties.\n\n**Compatibility with `asset_source` / `item_production_model`**: `synthesis_nondeterministic: true` MAY pair with any of `seller_pre_rendered_from_brief`, `seller_human_designed`, or `agent_synthesized` (the QA loop is concept-level, not source-specific — 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.").default(false), "slots": z.array(z.object({ "asset_group_id": z.string().describe("Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings."), "asset_type": z.enum(["image","video","audio","text","markdown","url","html","css","javascript","vast","daast","webhook","brief","catalog","published_post","zip","card","object","pixel_tracker","vast_tracker","daast_tracker"]).describe("Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `published_post` is an existing-post reference asset, not uploaded media bytes and not a catalog row. `card` is the multi-card carousel element type (see card-asset.json). `pixel_tracker` / `vast_tracker` / `daast_tracker` are the renderer-fired measurement-tracker primitives — see `/schemas/core/assets/pixel-tracker-asset.json` and the VAST / DAAST tracker schemas. `object` is a last-resort fallback for structured non-asset inputs that don't fit any primitive asset_type — prefer specific types whenever possible."), "required": z.boolean().describe("Whether this slot is required for a valid manifest.").default(false), "min": z.number().int().gte(0).describe("Minimum count for repeatable / pool slots.").optional(), "max": z.number().int().gte(1).describe("Maximum count for repeatable / pool slots.").optional(), "max_chars": z.number().int().gte(1).describe("Per-slot character limit. Valid only when `asset_type` is `text`, `markdown`, or `brief`. Mutually exclusive with `max_size_kb` (which applies to binary asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "max_size_kb": z.number().int().gte(1).describe("Per-slot file size limit in kilobytes. Valid only when `asset_type` is `image`, `video`, `audio`, or `zip`. Mutually exclusive with `max_chars` (which applies to text asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Accepted intrinsic-pixel densities for this image-bearing slot. Valid when `asset_type` is `image`, and on a `card` slot where it constrains each card's image media (video media is unaffected). This makes density available to every canonical carrying image assets (native, carousel, responsive, companion images, and image itself), not only `format_kind: image`. When the image canonical also declares top-level `params.pixel_ratios`, the effective set is the intersection; an empty intersection is invalid. One matching asset satisfies the slot unless `required_pixel_ratios` requires rendition coverage.").optional(), "required_pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Required density coverage for an image rendition set. Valid only when `asset_type` is `image` and `pixel_ratios` is also declared. Every value MUST appear in the effective accepted set after intersecting any top-level image `params.pixel_ratios`, and the manifest slot value MUST be an array containing exactly one matching image rendition for each required ratio. Other accepted ratios remain optional. For example, `pixel_ratios: [1, 1.5, 2]` with `required_pixel_ratios: [1, 2]` requires the 1x and 2x renditions while making 1.5x optional. SDKs enforce intersection, subset, coverage, and duplicate-ratio rules because JSON Schema draft-07 cannot express them generically.").optional(), "logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("When `asset_group_id` is `logo`, renderer-facing brand.json logo slots acceptable for this format slot. Producers selecting from brand.json SHOULD prefer `logos[]` entries whose `slots[]` intersects this list, then apply `visual_guidelines.logo_usage_rules[]`.").optional(), "required_logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Subset of `logo_slots` for which this format expects explicit logo coverage. A manifest or brand-derived logo pool SHOULD include at least one usable logo for each required slot; if coverage is missing, builders SHOULD surface a validation warning or approval mapping instead of guessing from prose.").optional(), "description": z.string().describe("Human-readable description of what the slot expects from the buyer.").optional(), "consumed_for_production": z.boolean().describe("Dispatch hint for `build_creative` and v1↔v2 wire translators: when `true`, the slot's value is consumed as INPUT to a production step (host-read script, brief copy fed to generative synthesis, catalog feed driving per-SKU rendering) and is not rendered verbatim. When `false` (default), the slot's value is rendered verbatim on the placement (image bytes, video file, display tag).\n\nMotivates the v1↔v2 dispatch table: pre-v2 buyers shipped production-consumed inputs separately in a `inputs` map on the build_creative request; v2 collapses inputs and rendered assets into a single `assets` map keyed by `asset_group_id`. SDK translators between v1 and v2 use this flag per canonical to know which assets in the v2 manifest map back to v1 `inputs` vs v1 `assets`. Without the per-slot flag the dispatch table lives in adopter code and every SDK gets it slightly different.\n\nProducers SHOULD set this explicitly on slots whose consumption pattern isn't obvious (host-read scripts on `audio_hosted`, briefs on generative `video_hosted`, catalog feeds on `sponsored_placement`). For canonicals where every slot is render-verbatim (`image`, `display_tag`, `video_vast`), the default `false` is sufficient and the flag MAY be omitted.").default(false) }).catchall(z.any()).and(z.intersection(z.intersection(z.any(), z.intersection(z.any(), z.any())), z.intersection(z.any(), z.intersection(z.any(), z.any()))))).describe("Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.").optional(), "required_connections": z.array(z.object({ "provider": z.string().describe("Stable provider or platform namespace, preferably lowercase. Examples: `social.example`, `shortvideo.example`, or a seller-defined namespace. Omit only when the requirement is provider-agnostic, or when an `authorization_url` fully routes the human to the correct provider-specific connection flow.").optional(), "connection_type": z.enum(["advertiser_account","publisher_identity","post_authorization"]).describe("Kind of downstream connection required. `advertiser_account` is the platform account used to buy/manage ads. `publisher_identity` is the creator, page, channel, organization, or profile that owns source posts. `post_authorization` is a post-scoped grant when the platform authorizes individual posts instead of, or in addition to, the owning identity."), "required_for": z.array(z.string().min(1)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Concrete AdCP protocol operation names that require this downstream connection. Sellers SHOULD include this in product declarations when the requirement is known ahead of time, and in AUTHORIZATION_REQUIRED details when it explains the failed operation. Prefer specific operation names such as `list_creatives`, `sync_creatives`, `create_media_buy`, `get_media_buy_delivery`, or `get_creative_delivery` over broad category labels such as `reporting`.").optional(), "scope": z.enum(["account","identity","post","unknown"]).describe("Granularity of the downstream grant.").optional(), "status": z.enum(["connected","missing","pending","expired","revoked","not_required","unknown"]).describe("Current seller-observed state for this downstream connection when known. Product declarations MAY omit status or use `unknown`; AUTHORIZATION_REQUIRED details SHOULD use `missing`, `expired`, or `revoked` for the connection that blocked the call.").optional(), "connection_id": z.string().describe("Seller-defined identifier for an already-created downstream connection. Omit when no connection exists yet or when exposing it would leak platform/account state.").optional(), "resource_ref": z.object({ "platform_account_id": z.string().describe("Provider-native advertiser or business account id, when safe to disclose.").optional(), "identity_id": z.string().describe("Provider-native creator, page, channel, organization, or profile id, when safe to disclose.").optional(), "handle": z.string().describe("Provider-native public handle for the owning identity, when available.").optional(), "profile_url": z.string().url().describe("Public URL for the owning identity, when available.").optional(), "post_id": z.string().describe("Provider-native post id, when the grant is post-scoped or the failed request referenced a specific post.").optional(), "post_url": z.string().url().describe("Public URL for the referenced post, when available.").optional() }).catchall(z.any()).describe("Optional opaque provider-native resource hint, such as a platform account id, profile URL, handle, channel id, post id, or post URL. This is a hint for routing authorization, not proof that authorization exists.").optional(), "authorization_url": z.string().url().describe("Seller-hosted or provider-hosted URL where a human can complete or restore this downstream connection.").optional(), "authorization_instructions": z.string().describe("Human-readable instructions for completing or restoring this downstream connection.").optional(), "expires_at": z.string().datetime().describe("Expiration time for the downstream grant, when known.").optional() }).catchall(z.any()).and(z.any()).describe("A seller/platform-side connection or grant required by a product, format, or request. This is not the AdCP caller credential: the AdCP request is still authenticated once, and the seller uses these stored downstream connections to call a platform or service on the buyer's behalf. Use this shape for platforms that require more than one downstream grant, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.")).describe("Downstream platform connections or grants required to use this format declaration. These are in addition to the single AdCP caller credential. Use this when a platform product requires multiple downstream grants, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.").optional(), "reference_mutability": z.enum(["immutable_snapshot","mutable_requires_reapproval","mutable_auto_recheck"]).describe("Policy for formats whose `slots` accept a `published_post` reference. `immutable_snapshot`: seller snapshots the referenced post at approval and later source changes do not change the served creative. `mutable_requires_reapproval`: the source post may change and material changes require review before continued serving. `mutable_auto_recheck`: the source post may change and the seller continuously or periodically rechecks authorization/policy without requiring buyer resubmission. Omit when the format has no `published_post` slot.").optional(), "production_window_business_days": z.number().int().gte(0).describe("Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative).").optional() }).catchall(z.any()).describe("Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).").describe("IAB-shaped native creative for in-feed and content-recommendation surfaces. Default slots cover the primary IAB OpenRTB Native 1.2 asset types — `title` (Title Asset), `body_text` (Data Asset type 2), `main_image` (Image Asset main), `icon` (Image Asset icon), `cta` (Data Asset type 12), `advertiser_name` (Data Asset type 1), `sponsored_label` (Title-adjacent), `landing_page_url` (Link Asset), `display_url` (Data Asset type 11 — visible URL/domain, distinct from clickthrough), `rating` (Data Asset type 3 — app/product rating), `price` (Data Asset type 6 — product price), plus renderer-fired `impression_tracker` / `viewability_tracker` / `click_tracker` (`pixel_tracker`). Products MAY use `slots_override` to add other IAB Native data asset types (likes — type 4, downloads — type 5, saleprice — type 7, phone_number — type 8, address — type 9, desc2 — type 10, etc.) or to remove slots the surface doesn't render. The publisher's renderer assembles these into its own look-and-feel — feed card, content-recommendation slot, in-stream native unit. Buyer ships a single asset bundle; the surface chooses presentation.\n\n**Scope (normative — buyer-agent routing).** This canonical is the home for:\n- IAB OpenRTB Native 1.2 in-feed native ads (publisher feeds, app feeds)\n- Content-recommendation widgets (Taboola, Outbrain, Yahoo Recommendations)\n- AdMob Native / Yahoo Native publisher slots\n- In-feed sponsored placements without catalog dependency\n\n**Not this canonical:**\n- Catalog-driven retail-media (Amazon SP, Criteo SP, CitrusAd SP) — use `sponsored_placement` (requires `source_catalog`).\n- Algorithmic surface that picks from a buyer-supplied asset pool (Google PMax, Meta Advantage+) — use `responsive_creative`.\n- Multi-card carousel — use `image_carousel`.\n- Video-first native units where the asset is a hosted video file — use `video_hosted` with `applies_to_channels: [\"native\"]`.\n\nDistinct from `sponsored_placement` along the catalog axis: native_in_feed is asset-bundle composition; sponsored_placement is catalog-row composition. A buyer agent reading `format_kind: native_in_feed` knows to assemble title + image + body + CTA; reading `format_kind: sponsored_placement` knows to attach a catalog feed.") }), z.object({ "format_kind": z.literal("responsive_creative"), "params": z.object({ "experimental": z.boolean().describe("When true, this canonical or seller narrowing may not work as declared. Adopters SHOULD preflight it with validate_input or in a sandbox and SHOULD NOT route production budget without testing; experimental status never makes the deprecated v1 path preferable. Drivers include unsettled spec shape, an adopter runtime gap, and custom shapes awaiting promotion. This replaces the earlier status plus runtime_status axes. Sellers SHOULD set experimental whenever a canonical or declaration is not production-ready.").default(false), "deprecated": z.boolean().describe("When true, this canonical (or a seller's specific narrowing of it) is going away. Existing adopters are supported through the deprecation cycle; new adoption is discouraged. Pair with `migration_target_version` to indicate when the canonical is expected to be removed. Distinct from `experimental`: an experimental canonical may stabilize and stop being experimental; a deprecated canonical is on a sunset path.").default(false), "v1_translatable": z.boolean().describe("Whether this canonical has any v1 named-format equivalent. `true` (default) — the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1→v2 projection via `v1-canonical-mapping.json` is meaningful. `false` — the canonical is inherently new in v2 and has no v1 form; v1's `list_creative_formats` couldn't express it because the underlying concept (algorithmic surface composition, AI-surface mentions, retail-media catalog placements, multi-card carousels) didn't exist as a v1 named-format archetype.\n\nLets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The 4 inherently-v2 canonicals at 3.1 GA: `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`.").default(true), "since_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version that introduced this canonical (e.g., '3.1', '3.2'). Lets adopters reason about minimum protocol version requirements when consuming a format declaration. Patch precision is intentionally rejected — canonicals are introduced at minor-version boundaries.").optional(), "migration_target_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version by which the working group expects this canonical to stabilize, surface a breaking revision, or (when `deprecated: true`) be removed. Patch precision is intentionally rejected — canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown').").optional(), "composition_model": z.enum(["deterministic","algorithmic"]).describe("Whether the surface composes deterministically (buyer can predict per-slot rendering — sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing — responsive_creative, agent_placement).").optional(), "provenance_required": z.boolean().describe("When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent.").optional(), "platform_extensions": z.array(z.object({ "uri": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL identifying the extension. `https://` is mandatory — `http://`, `file://`, `data:`, and other schemes are rejected at the schema layer (defense-in-depth on top of the fetch-contract normative rules). The URI base is the owning agent's URL; the path identifies the extension within that agent. Example: 'https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel'. The full fetch contract — SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds — is documented on `product-format-declaration.json#format_schema` and applies to ALL fetches of this reference shape regardless of whether the field is named `format_schema` (load-bearing for validation) or `platform_extensions` (informational); the *transport* rules are identical, only the *consumption* semantics differ."), "digest": z.string().regex(new RegExp("^sha256:[a-f0-9]{64}$")).describe("SHA-256 content digest of the extension definition (sha256:). Used to detect drift — if the agent revises the extension, the digest changes and cached definitions become invalid.") }).catchall(z.any()).describe("Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` — divergent digests in one response indicate producer-side error (e.g., concurrent extension revision mid-render). Buyers encountering divergent digests for the same URI MUST fail closed: treat all references to that URI as unresolved and surface a validation error rather than picking one branch silently. **Across responses**, digest divergence is normal — extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.")).describe("Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\n\n**Collision precedence (normative).** When two or more `platform_extensions[]` entries on the same declaration extend the same target (e.g., both extend `tracking`) with overlapping field names, **array order is authoritative — later entries override earlier ones on a per-field basis** (last-in-array-wins). SDKs MUST surface the overlap via the `errors[]` array on the `get_products` response with a structured code (`FORMAT_DECLARATION_DIVERGENT` is appropriate when the overlap appears across dual-emitted shapes; a producer-self-emitted overlap on a single declaration SHOULD use the same code with `error.details: { collision_kind: \"platform_extension_field\", target, overlapping_fields, winning_extension_uri }`). Producers SHOULD avoid the collision by emitting one extension per target or by partitioning fields across extensions; the deterministic precedence is for last-resort consistency across SDK implementations, not a sanctioned merging strategy.").optional(), "synthesis_nondeterministic": z.boolean().describe("When true, the format's production pipeline is genuinely nondeterministic — the platform cannot guarantee that synthesis from a given input set produces in-spec output. Veo / Sora / Runway-class generative video, and other AI-synthesis flows where output dimensions, duration, or quality vary per run. Implies a different validation contract: predictive `validate_input` is impossible; the platform's own post-synthesis QA loop applies; if the QA loop exhausts without producing a valid artifact, `build_creative` returns task_failed with a synthesis_failed reason. Distinct from `composition_model` (which describes how the surface composes per-slot rendering, not whether synthesis is deterministic). When false or absent, the format's production is predictable enough that `validate_input` can predict output properties from input properties.\n\n**Compatibility with `asset_source` / `item_production_model`**: `synthesis_nondeterministic: true` MAY pair with any of `seller_pre_rendered_from_brief`, `seller_human_designed`, or `agent_synthesized` (the QA loop is concept-level, not source-specific — 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.").default(false), "slots": z.array(z.object({ "asset_group_id": z.string().describe("Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings."), "asset_type": z.enum(["image","video","audio","text","markdown","url","html","css","javascript","vast","daast","webhook","brief","catalog","published_post","zip","card","object","pixel_tracker","vast_tracker","daast_tracker"]).describe("Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `published_post` is an existing-post reference asset, not uploaded media bytes and not a catalog row. `card` is the multi-card carousel element type (see card-asset.json). `pixel_tracker` / `vast_tracker` / `daast_tracker` are the renderer-fired measurement-tracker primitives — see `/schemas/core/assets/pixel-tracker-asset.json` and the VAST / DAAST tracker schemas. `object` is a last-resort fallback for structured non-asset inputs that don't fit any primitive asset_type — prefer specific types whenever possible."), "required": z.boolean().describe("Whether this slot is required for a valid manifest.").default(false), "min": z.number().int().gte(0).describe("Minimum count for repeatable / pool slots.").optional(), "max": z.number().int().gte(1).describe("Maximum count for repeatable / pool slots.").optional(), "max_chars": z.number().int().gte(1).describe("Per-slot character limit. Valid only when `asset_type` is `text`, `markdown`, or `brief`. Mutually exclusive with `max_size_kb` (which applies to binary asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "max_size_kb": z.number().int().gte(1).describe("Per-slot file size limit in kilobytes. Valid only when `asset_type` is `image`, `video`, `audio`, or `zip`. Mutually exclusive with `max_chars` (which applies to text asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Accepted intrinsic-pixel densities for this image-bearing slot. Valid when `asset_type` is `image`, and on a `card` slot where it constrains each card's image media (video media is unaffected). This makes density available to every canonical carrying image assets (native, carousel, responsive, companion images, and image itself), not only `format_kind: image`. When the image canonical also declares top-level `params.pixel_ratios`, the effective set is the intersection; an empty intersection is invalid. One matching asset satisfies the slot unless `required_pixel_ratios` requires rendition coverage.").optional(), "required_pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Required density coverage for an image rendition set. Valid only when `asset_type` is `image` and `pixel_ratios` is also declared. Every value MUST appear in the effective accepted set after intersecting any top-level image `params.pixel_ratios`, and the manifest slot value MUST be an array containing exactly one matching image rendition for each required ratio. Other accepted ratios remain optional. For example, `pixel_ratios: [1, 1.5, 2]` with `required_pixel_ratios: [1, 2]` requires the 1x and 2x renditions while making 1.5x optional. SDKs enforce intersection, subset, coverage, and duplicate-ratio rules because JSON Schema draft-07 cannot express them generically.").optional(), "logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("When `asset_group_id` is `logo`, renderer-facing brand.json logo slots acceptable for this format slot. Producers selecting from brand.json SHOULD prefer `logos[]` entries whose `slots[]` intersects this list, then apply `visual_guidelines.logo_usage_rules[]`.").optional(), "required_logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Subset of `logo_slots` for which this format expects explicit logo coverage. A manifest or brand-derived logo pool SHOULD include at least one usable logo for each required slot; if coverage is missing, builders SHOULD surface a validation warning or approval mapping instead of guessing from prose.").optional(), "description": z.string().describe("Human-readable description of what the slot expects from the buyer.").optional(), "consumed_for_production": z.boolean().describe("Dispatch hint for `build_creative` and v1↔v2 wire translators: when `true`, the slot's value is consumed as INPUT to a production step (host-read script, brief copy fed to generative synthesis, catalog feed driving per-SKU rendering) and is not rendered verbatim. When `false` (default), the slot's value is rendered verbatim on the placement (image bytes, video file, display tag).\n\nMotivates the v1↔v2 dispatch table: pre-v2 buyers shipped production-consumed inputs separately in a `inputs` map on the build_creative request; v2 collapses inputs and rendered assets into a single `assets` map keyed by `asset_group_id`. SDK translators between v1 and v2 use this flag per canonical to know which assets in the v2 manifest map back to v1 `inputs` vs v1 `assets`. Without the per-slot flag the dispatch table lives in adopter code and every SDK gets it slightly different.\n\nProducers SHOULD set this explicitly on slots whose consumption pattern isn't obvious (host-read scripts on `audio_hosted`, briefs on generative `video_hosted`, catalog feeds on `sponsored_placement`). For canonicals where every slot is render-verbatim (`image`, `display_tag`, `video_vast`), the default `false` is sufficient and the flag MAY be omitted.").default(false) }).catchall(z.any()).and(z.intersection(z.intersection(z.any(), z.intersection(z.any(), z.any())), z.intersection(z.any(), z.intersection(z.any(), z.any()))))).describe("Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.").optional(), "required_connections": z.array(z.object({ "provider": z.string().describe("Stable provider or platform namespace, preferably lowercase. Examples: `social.example`, `shortvideo.example`, or a seller-defined namespace. Omit only when the requirement is provider-agnostic, or when an `authorization_url` fully routes the human to the correct provider-specific connection flow.").optional(), "connection_type": z.enum(["advertiser_account","publisher_identity","post_authorization"]).describe("Kind of downstream connection required. `advertiser_account` is the platform account used to buy/manage ads. `publisher_identity` is the creator, page, channel, organization, or profile that owns source posts. `post_authorization` is a post-scoped grant when the platform authorizes individual posts instead of, or in addition to, the owning identity."), "required_for": z.array(z.string().min(1)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Concrete AdCP protocol operation names that require this downstream connection. Sellers SHOULD include this in product declarations when the requirement is known ahead of time, and in AUTHORIZATION_REQUIRED details when it explains the failed operation. Prefer specific operation names such as `list_creatives`, `sync_creatives`, `create_media_buy`, `get_media_buy_delivery`, or `get_creative_delivery` over broad category labels such as `reporting`.").optional(), "scope": z.enum(["account","identity","post","unknown"]).describe("Granularity of the downstream grant.").optional(), "status": z.enum(["connected","missing","pending","expired","revoked","not_required","unknown"]).describe("Current seller-observed state for this downstream connection when known. Product declarations MAY omit status or use `unknown`; AUTHORIZATION_REQUIRED details SHOULD use `missing`, `expired`, or `revoked` for the connection that blocked the call.").optional(), "connection_id": z.string().describe("Seller-defined identifier for an already-created downstream connection. Omit when no connection exists yet or when exposing it would leak platform/account state.").optional(), "resource_ref": z.object({ "platform_account_id": z.string().describe("Provider-native advertiser or business account id, when safe to disclose.").optional(), "identity_id": z.string().describe("Provider-native creator, page, channel, organization, or profile id, when safe to disclose.").optional(), "handle": z.string().describe("Provider-native public handle for the owning identity, when available.").optional(), "profile_url": z.string().url().describe("Public URL for the owning identity, when available.").optional(), "post_id": z.string().describe("Provider-native post id, when the grant is post-scoped or the failed request referenced a specific post.").optional(), "post_url": z.string().url().describe("Public URL for the referenced post, when available.").optional() }).catchall(z.any()).describe("Optional opaque provider-native resource hint, such as a platform account id, profile URL, handle, channel id, post id, or post URL. This is a hint for routing authorization, not proof that authorization exists.").optional(), "authorization_url": z.string().url().describe("Seller-hosted or provider-hosted URL where a human can complete or restore this downstream connection.").optional(), "authorization_instructions": z.string().describe("Human-readable instructions for completing or restoring this downstream connection.").optional(), "expires_at": z.string().datetime().describe("Expiration time for the downstream grant, when known.").optional() }).catchall(z.any()).and(z.any()).describe("A seller/platform-side connection or grant required by a product, format, or request. This is not the AdCP caller credential: the AdCP request is still authenticated once, and the seller uses these stored downstream connections to call a platform or service on the buyer's behalf. Use this shape for platforms that require more than one downstream grant, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.")).describe("Downstream platform connections or grants required to use this format declaration. These are in addition to the single AdCP caller credential. Use this when a platform product requires multiple downstream grants, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.").optional(), "reference_mutability": z.enum(["immutable_snapshot","mutable_requires_reapproval","mutable_auto_recheck"]).describe("Policy for formats whose `slots` accept a `published_post` reference. `immutable_snapshot`: seller snapshots the referenced post at approval and later source changes do not change the served creative. `mutable_requires_reapproval`: the source post may change and material changes require review before continued serving. `mutable_auto_recheck`: the source post may change and the seller continuously or periodically rechecks authorization/policy without requiring buyer resubmission. Omit when the format has no `published_post` slot.").optional(), "production_window_business_days": z.number().int().gte(0).describe("Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative).").optional() }).catchall(z.any()).describe("Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).").describe("Buyer supplies a pool of typed assets (multiple headlines, descriptions, images, videos, logos); the surface algorithmically composes combinations per placement. **Composition is algorithmic** — surface picks combinations and reports per-asset performance breakdowns. Covers Google Responsive Display Ads (RDA), Responsive Search Ads (RSA), Performance Max (PMax), Demand Gen, and Meta Advantage+ creative. Industry term: \"Responsive\" (Google) / \"Advantage+ creative\" (Meta) / \"Dynamic Creative\" (older Meta term). Distinct from `sponsored_placement` (catalog-driven, deterministic) and `agent_placement` (AI-surface composition). The structured `slots` field below enumerates expected canonical asset_group_id slots; per-slot count/length narrowing lives in flat parameters (`headlines_min`, `headline_max_chars`, etc.).") }), z.object({ "format_kind": z.literal("agent_placement"), "params": z.object({ "experimental": z.boolean().describe("When true, this canonical or seller narrowing may not work as declared. Adopters SHOULD preflight it with validate_input or in a sandbox and SHOULD NOT route production budget without testing; experimental status never makes the deprecated v1 path preferable. Drivers include unsettled spec shape, an adopter runtime gap, and custom shapes awaiting promotion. This replaces the earlier status plus runtime_status axes. Sellers SHOULD set experimental whenever a canonical or declaration is not production-ready.").default(false), "deprecated": z.boolean().describe("When true, this canonical (or a seller's specific narrowing of it) is going away. Existing adopters are supported through the deprecation cycle; new adoption is discouraged. Pair with `migration_target_version` to indicate when the canonical is expected to be removed. Distinct from `experimental`: an experimental canonical may stabilize and stop being experimental; a deprecated canonical is on a sunset path.").default(false), "v1_translatable": z.boolean().describe("Whether this canonical has any v1 named-format equivalent. `true` (default) — the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1→v2 projection via `v1-canonical-mapping.json` is meaningful. `false` — the canonical is inherently new in v2 and has no v1 form; v1's `list_creative_formats` couldn't express it because the underlying concept (algorithmic surface composition, AI-surface mentions, retail-media catalog placements, multi-card carousels) didn't exist as a v1 named-format archetype.\n\nLets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The 4 inherently-v2 canonicals at 3.1 GA: `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`.").default(true), "since_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version that introduced this canonical (e.g., '3.1', '3.2'). Lets adopters reason about minimum protocol version requirements when consuming a format declaration. Patch precision is intentionally rejected — canonicals are introduced at minor-version boundaries.").optional(), "migration_target_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version by which the working group expects this canonical to stabilize, surface a breaking revision, or (when `deprecated: true`) be removed. Patch precision is intentionally rejected — canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown').").optional(), "composition_model": z.enum(["deterministic","algorithmic"]).describe("Whether the surface composes deterministically (buyer can predict per-slot rendering — sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing — responsive_creative, agent_placement).").optional(), "provenance_required": z.boolean().describe("When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent.").optional(), "platform_extensions": z.array(z.object({ "uri": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL identifying the extension. `https://` is mandatory — `http://`, `file://`, `data:`, and other schemes are rejected at the schema layer (defense-in-depth on top of the fetch-contract normative rules). The URI base is the owning agent's URL; the path identifies the extension within that agent. Example: 'https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel'. The full fetch contract — SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds — is documented on `product-format-declaration.json#format_schema` and applies to ALL fetches of this reference shape regardless of whether the field is named `format_schema` (load-bearing for validation) or `platform_extensions` (informational); the *transport* rules are identical, only the *consumption* semantics differ."), "digest": z.string().regex(new RegExp("^sha256:[a-f0-9]{64}$")).describe("SHA-256 content digest of the extension definition (sha256:). Used to detect drift — if the agent revises the extension, the digest changes and cached definitions become invalid.") }).catchall(z.any()).describe("Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` — divergent digests in one response indicate producer-side error (e.g., concurrent extension revision mid-render). Buyers encountering divergent digests for the same URI MUST fail closed: treat all references to that URI as unresolved and surface a validation error rather than picking one branch silently. **Across responses**, digest divergence is normal — extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.")).describe("Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\n\n**Collision precedence (normative).** When two or more `platform_extensions[]` entries on the same declaration extend the same target (e.g., both extend `tracking`) with overlapping field names, **array order is authoritative — later entries override earlier ones on a per-field basis** (last-in-array-wins). SDKs MUST surface the overlap via the `errors[]` array on the `get_products` response with a structured code (`FORMAT_DECLARATION_DIVERGENT` is appropriate when the overlap appears across dual-emitted shapes; a producer-self-emitted overlap on a single declaration SHOULD use the same code with `error.details: { collision_kind: \"platform_extension_field\", target, overlapping_fields, winning_extension_uri }`). Producers SHOULD avoid the collision by emitting one extension per target or by partitioning fields across extensions; the deterministic precedence is for last-resort consistency across SDK implementations, not a sanctioned merging strategy.").optional(), "synthesis_nondeterministic": z.boolean().describe("When true, the format's production pipeline is genuinely nondeterministic — the platform cannot guarantee that synthesis from a given input set produces in-spec output. Veo / Sora / Runway-class generative video, and other AI-synthesis flows where output dimensions, duration, or quality vary per run. Implies a different validation contract: predictive `validate_input` is impossible; the platform's own post-synthesis QA loop applies; if the QA loop exhausts without producing a valid artifact, `build_creative` returns task_failed with a synthesis_failed reason. Distinct from `composition_model` (which describes how the surface composes per-slot rendering, not whether synthesis is deterministic). When false or absent, the format's production is predictable enough that `validate_input` can predict output properties from input properties.\n\n**Compatibility with `asset_source` / `item_production_model`**: `synthesis_nondeterministic: true` MAY pair with any of `seller_pre_rendered_from_brief`, `seller_human_designed`, or `agent_synthesized` (the QA loop is concept-level, not source-specific — 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.").default(false), "slots": z.array(z.object({ "asset_group_id": z.string().describe("Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings."), "asset_type": z.enum(["image","video","audio","text","markdown","url","html","css","javascript","vast","daast","webhook","brief","catalog","published_post","zip","card","object","pixel_tracker","vast_tracker","daast_tracker"]).describe("Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `published_post` is an existing-post reference asset, not uploaded media bytes and not a catalog row. `card` is the multi-card carousel element type (see card-asset.json). `pixel_tracker` / `vast_tracker` / `daast_tracker` are the renderer-fired measurement-tracker primitives — see `/schemas/core/assets/pixel-tracker-asset.json` and the VAST / DAAST tracker schemas. `object` is a last-resort fallback for structured non-asset inputs that don't fit any primitive asset_type — prefer specific types whenever possible."), "required": z.boolean().describe("Whether this slot is required for a valid manifest.").default(false), "min": z.number().int().gte(0).describe("Minimum count for repeatable / pool slots.").optional(), "max": z.number().int().gte(1).describe("Maximum count for repeatable / pool slots.").optional(), "max_chars": z.number().int().gte(1).describe("Per-slot character limit. Valid only when `asset_type` is `text`, `markdown`, or `brief`. Mutually exclusive with `max_size_kb` (which applies to binary asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "max_size_kb": z.number().int().gte(1).describe("Per-slot file size limit in kilobytes. Valid only when `asset_type` is `image`, `video`, `audio`, or `zip`. Mutually exclusive with `max_chars` (which applies to text asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Accepted intrinsic-pixel densities for this image-bearing slot. Valid when `asset_type` is `image`, and on a `card` slot where it constrains each card's image media (video media is unaffected). This makes density available to every canonical carrying image assets (native, carousel, responsive, companion images, and image itself), not only `format_kind: image`. When the image canonical also declares top-level `params.pixel_ratios`, the effective set is the intersection; an empty intersection is invalid. One matching asset satisfies the slot unless `required_pixel_ratios` requires rendition coverage.").optional(), "required_pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Required density coverage for an image rendition set. Valid only when `asset_type` is `image` and `pixel_ratios` is also declared. Every value MUST appear in the effective accepted set after intersecting any top-level image `params.pixel_ratios`, and the manifest slot value MUST be an array containing exactly one matching image rendition for each required ratio. Other accepted ratios remain optional. For example, `pixel_ratios: [1, 1.5, 2]` with `required_pixel_ratios: [1, 2]` requires the 1x and 2x renditions while making 1.5x optional. SDKs enforce intersection, subset, coverage, and duplicate-ratio rules because JSON Schema draft-07 cannot express them generically.").optional(), "logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("When `asset_group_id` is `logo`, renderer-facing brand.json logo slots acceptable for this format slot. Producers selecting from brand.json SHOULD prefer `logos[]` entries whose `slots[]` intersects this list, then apply `visual_guidelines.logo_usage_rules[]`.").optional(), "required_logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Subset of `logo_slots` for which this format expects explicit logo coverage. A manifest or brand-derived logo pool SHOULD include at least one usable logo for each required slot; if coverage is missing, builders SHOULD surface a validation warning or approval mapping instead of guessing from prose.").optional(), "description": z.string().describe("Human-readable description of what the slot expects from the buyer.").optional(), "consumed_for_production": z.boolean().describe("Dispatch hint for `build_creative` and v1↔v2 wire translators: when `true`, the slot's value is consumed as INPUT to a production step (host-read script, brief copy fed to generative synthesis, catalog feed driving per-SKU rendering) and is not rendered verbatim. When `false` (default), the slot's value is rendered verbatim on the placement (image bytes, video file, display tag).\n\nMotivates the v1↔v2 dispatch table: pre-v2 buyers shipped production-consumed inputs separately in a `inputs` map on the build_creative request; v2 collapses inputs and rendered assets into a single `assets` map keyed by `asset_group_id`. SDK translators between v1 and v2 use this flag per canonical to know which assets in the v2 manifest map back to v1 `inputs` vs v1 `assets`. Without the per-slot flag the dispatch table lives in adopter code and every SDK gets it slightly different.\n\nProducers SHOULD set this explicitly on slots whose consumption pattern isn't obvious (host-read scripts on `audio_hosted`, briefs on generative `video_hosted`, catalog feeds on `sponsored_placement`). For canonicals where every slot is render-verbatim (`image`, `display_tag`, `video_vast`), the default `false` is sufficient and the flag MAY be omitted.").default(false) }).catchall(z.any()).and(z.intersection(z.intersection(z.any(), z.intersection(z.any(), z.any())), z.intersection(z.any(), z.intersection(z.any(), z.any()))))).describe("Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.").optional(), "required_connections": z.array(z.object({ "provider": z.string().describe("Stable provider or platform namespace, preferably lowercase. Examples: `social.example`, `shortvideo.example`, or a seller-defined namespace. Omit only when the requirement is provider-agnostic, or when an `authorization_url` fully routes the human to the correct provider-specific connection flow.").optional(), "connection_type": z.enum(["advertiser_account","publisher_identity","post_authorization"]).describe("Kind of downstream connection required. `advertiser_account` is the platform account used to buy/manage ads. `publisher_identity` is the creator, page, channel, organization, or profile that owns source posts. `post_authorization` is a post-scoped grant when the platform authorizes individual posts instead of, or in addition to, the owning identity."), "required_for": z.array(z.string().min(1)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Concrete AdCP protocol operation names that require this downstream connection. Sellers SHOULD include this in product declarations when the requirement is known ahead of time, and in AUTHORIZATION_REQUIRED details when it explains the failed operation. Prefer specific operation names such as `list_creatives`, `sync_creatives`, `create_media_buy`, `get_media_buy_delivery`, or `get_creative_delivery` over broad category labels such as `reporting`.").optional(), "scope": z.enum(["account","identity","post","unknown"]).describe("Granularity of the downstream grant.").optional(), "status": z.enum(["connected","missing","pending","expired","revoked","not_required","unknown"]).describe("Current seller-observed state for this downstream connection when known. Product declarations MAY omit status or use `unknown`; AUTHORIZATION_REQUIRED details SHOULD use `missing`, `expired`, or `revoked` for the connection that blocked the call.").optional(), "connection_id": z.string().describe("Seller-defined identifier for an already-created downstream connection. Omit when no connection exists yet or when exposing it would leak platform/account state.").optional(), "resource_ref": z.object({ "platform_account_id": z.string().describe("Provider-native advertiser or business account id, when safe to disclose.").optional(), "identity_id": z.string().describe("Provider-native creator, page, channel, organization, or profile id, when safe to disclose.").optional(), "handle": z.string().describe("Provider-native public handle for the owning identity, when available.").optional(), "profile_url": z.string().url().describe("Public URL for the owning identity, when available.").optional(), "post_id": z.string().describe("Provider-native post id, when the grant is post-scoped or the failed request referenced a specific post.").optional(), "post_url": z.string().url().describe("Public URL for the referenced post, when available.").optional() }).catchall(z.any()).describe("Optional opaque provider-native resource hint, such as a platform account id, profile URL, handle, channel id, post id, or post URL. This is a hint for routing authorization, not proof that authorization exists.").optional(), "authorization_url": z.string().url().describe("Seller-hosted or provider-hosted URL where a human can complete or restore this downstream connection.").optional(), "authorization_instructions": z.string().describe("Human-readable instructions for completing or restoring this downstream connection.").optional(), "expires_at": z.string().datetime().describe("Expiration time for the downstream grant, when known.").optional() }).catchall(z.any()).and(z.any()).describe("A seller/platform-side connection or grant required by a product, format, or request. This is not the AdCP caller credential: the AdCP request is still authenticated once, and the seller uses these stored downstream connections to call a platform or service on the buyer's behalf. Use this shape for platforms that require more than one downstream grant, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.")).describe("Downstream platform connections or grants required to use this format declaration. These are in addition to the single AdCP caller credential. Use this when a platform product requires multiple downstream grants, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.").optional(), "reference_mutability": z.enum(["immutable_snapshot","mutable_requires_reapproval","mutable_auto_recheck"]).describe("Policy for formats whose `slots` accept a `published_post` reference. `immutable_snapshot`: seller snapshots the referenced post at approval and later source changes do not change the served creative. `mutable_requires_reapproval`: the source post may change and material changes require review before continued serving. `mutable_auto_recheck`: the source post may change and the seller continuously or periodically rechecks authorization/policy without requiring buyer resubmission. Omit when the format has no `published_post` slot.").optional(), "production_window_business_days": z.number().int().gte(0).describe("Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative).").optional() }).catchall(z.any()).describe("Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).").describe("**3.2-track canonical.** The structural shape (algorithmic composition + brand-context input + optional offering/landing_page) is captured here so adopters can declare against it in 3.1 catalogs, but the **mention-level tracking contract is intentionally underspecified for 3.1**: no normative macro vocabulary, no postback shape, no cross-surface dedup model. Adopters claiming `agent_placement` in 3.1 ship private tracking integrations and SHOULD leave `experimental: true` on the product declaration that references this canonical; buyer agents MUST treat agent_placement attribution as adapter-defined until the 3.2 tracking-macro spec lands. The canonical promotes to a normatively-buyer-callable surface in 3.2 (or later) once the tracking contract is specified.\n\nSponsored placement integrated into an AI-surface's response to a user. Buyer supplies a `BrandRef` (resolving brand.json for context), an optional `offering_ref` to focus the mention on a specific offering, and an optional `landing_page_url` the surface MAY attach as a citation. The surface (LLM, voice assistant, sponsored-search ranker) composes a natural-language mention, sponsored card, or audio snippet within its response to a user query. **Composition is algorithmic** — the agent chooses phrasing and presentation. Output asset_type varies by surface: `text` for chat UIs and sponsored search snippets; `audio` (synthesized) for voice assistants; `card` for structured AI-surface result cards. Tracking model: mention-level impression + attribution events; per-mention id keys back to brand and offering — but see the 3.2-track note above; the wire shape of these events is not yet specified. Distinct from `si_chat` (which is the user-converses-with-brand's-agent pattern — brand owns the conversational surface) and from `sponsored_placement` (retail-media catalog-driven). Parallels `sponsored_placement` structurally: both are surface-composed placements; agent_placement is for AI/agentic surfaces, sponsored_placement is for retail media.") }), z.object({ "format_kind": z.literal("custom"), "params": z.record(z.string(), z.any()).describe("Custom shape's params. Validated against the schema fetched from `format_schema.uri` at the cached `format_schema.digest`.") }).describe("Adopter-defined shape that doesn't fit the 12 canonicals. Requires `format_shape` (vocabulary-registered global pattern) and `format_schema` (URI+digest reference to a fetchable schema describing the actual params/slots). `params` shape is governed by the fetched schema rather than baked into AdCP — kept as `type: object` here with `additionalProperties: true` because the canonical schema validates dynamically post-fetch.")])).and(z.intersection(z.intersection(z.any(), z.union([z.union([z.any(), z.any()]), z.any().refine((value) => !z.union([z.any(), z.any()]).safeParse(value).success, "Invalid input: Should NOT be valid against schema")]).superRefine((value,ctx) => { +})), z.intersection(z.any().refine((value) => !z.intersection(z.any(), z.any()).safeParse(value).success, "Invalid input: Should NOT be valid against schema"), z.intersection(z.any().refine((value) => !z.any().safeParse(value).success, "Invalid input: Should NOT be valid against schema"), z.any())))).describe("Inline 3.1+ canonical format-option declaration. Use when the placement narrows a format in a way that's not worth a top-level catalog entry.")])).describe("Optional 3.1+ canonical format-option declarations supported by this placement. This `adagents.json` placement surface supports two entry shapes: (1) reference an entry in the same file's top-level `formats[]` by `format_option_id` only — buyers resolve the full declaration from `formats[]` by matching `format_option_id` (recommended; avoids duplication). Top-level formats may be publisher-owned custom formats or narrowed canonical formats; their `format_kind` is the canonical anchor that the placement reference inherits. (2) carry an inline `ProductFormatDeclaration` directly — for placement-specific canonical narrowing that doesn't fit a reusable catalog entry. This bare-reference shape is placement-catalog specific; Product `format_options[]` entries are always full `ProductFormatDeclaration` objects with required `format_kind` and `params`.\n\nProduct-level formats remain the upper bound for a sellable product. Catalog placement formats describe placement support; when a product references the placement and also declares product-level formats, buyers use the intersection for that product placement. A catalog placement format that is absent from the product-level declaration is not accepted for that product unless the product explicitly includes it. When the product locale policy is absent, placement locale_policy may introduce any concrete narrowing; when both are present, every placement accepted_language_range must be contained by a product range under RFC 4647 Basic Filtering. An effective placement policy is canonical-only: its resolved catalog and matching product declarations cannot project through legacy format_ids.\n\n**Format-option reference shape.** A format-option reference entry SHOULD carry ONLY `format_option_id` — extra fields are allowed (`additionalProperties: true`) so adopters who want to attach a placement-local override like `display_name` or a narrower `locale_policy` don't get rejected by the branch boundary, but buyer SDKs MUST resolve the format from the top-level `formats[]` by `format_option_id` and apply additional fields on the entry as placement-level overrides (NOT as a partial inline declaration). If a publisher needs to materially narrow the format at the placement, use the inline-declaration form instead.\n\n**Resolution scope is same-file only.** `format_option_id` resolves only within this file's top-level `formats[]`; cross-file references are not supported by design because same-file resolution keeps validation bounded and prevents a file from squatting on or narrowing another publisher's format_option_id. When `format_options[]` references a `format_option_id` not declared in the file's top-level `formats[]`, validators MUST surface this as `FORMAT_OPTION_UNRESOLVED` on the response `errors[]`. Buyers MUST fail closed for that placement (drop the format from the placement's accepted set) rather than silently dropping the placement or guessing intent.").optional(), "video_placement_types": z.array(z.enum(["instream","accompanying_content","interstitial","standalone"]).describe("Declared video placement classification for OLV and other video inventory, using the IAB Tech Lab/OpenRTB 2.6 video.plcmt definitions with AdCP-native value names. This is seller-declared discovery metadata, not independent verification of inventory quality or delivery context.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Declared video placement types for this publisher placement, using IAB Tech Lab/OpenRTB 2.6 video.plcmt definitions with AdCP-native names. Most concrete placements SHOULD declare a single value; aggregate placements MAY declare multiple values. Product-level placement declarations may narrow this set but SHOULD NOT broaden it. This is seller-declared discovery metadata, not independent verification of inventory quality or delivery context.").optional(), "audio_distribution_types": z.array(z.enum(["music_streaming_service","fm_am_broadcast","podcast","catch_up_radio","web_radio","video_game","text_to_speech"]).describe("Declared audio distribution classification for radio, streaming-audio, podcast, and other audio inventory, using IAB Tech Lab/OpenRTB 2.6 audio.feed definitions with AdCP-native value names. This is seller-declared discovery metadata, not independent verification of inventory quality or delivery context.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Declared audio distribution types for this publisher placement, using IAB Tech Lab/OpenRTB 2.6 audio.feed definitions with AdCP-native names. Most concrete placements SHOULD declare a single value; aggregate placements MAY declare multiple values. Product-level placement declarations may narrow this set but SHOULD NOT broaden it. This is seller-declared discovery metadata, not independent verification of inventory quality or delivery context.").optional(), "sponsored_placement_types": z.array(z.enum(["sponsored_search","sponsored_display","sponsored_native"]).describe("Declared sponsored-placement classification for catalog-driven retail-media inventory, distinguishing where the sponsored placement renders on the retailer surface. This is seller-declared discovery metadata, not independent verification of inventory quality or delivery context.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Declared sponsored-placement types for this publisher placement, distinguishing where the catalog-driven retail-media placement renders on the retailer surface. Most concrete placements SHOULD declare a single value; aggregate placements MAY declare multiple values. Product-level placement declarations may narrow this set but SHOULD NOT broaden it. This is seller-declared discovery metadata, not independent verification of inventory quality or delivery context.").optional(), "social_placement_surfaces": z.array(z.enum(["feed","stories","short_video","explore","search"]).describe("Declared social-placement surface classification for social inventory, distinguishing the in-app surface where a social placement renders. Values name the semantic surface, not platform brands. This is seller-declared discovery metadata, not independent verification of inventory quality or delivery context.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Declared social-placement surfaces for this publisher placement, distinguishing the in-app surface where the social placement renders. Most concrete placements SHOULD declare a single value; aggregate placements MAY declare multiple values. Product-level placement declarations may narrow this set but SHOULD NOT broaden it. This is seller-declared discovery metadata, not independent verification of inventory quality or delivery context.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension object for platform-specific, vendor-namespaced parameters. Extensions are always optional and must be namespaced under a vendor/platform key (e.g., ext.gam, ext.roku). Used for custom capabilities, partner-specific configuration, and features being proposed for standardization.").optional() }).catchall(z.any()).and(z.union([z.any(), z.any()])).and(z.any().refine((value) => !z.union([z.any(), z.any(), z.any(), z.any(), z.any()]).safeParse(value).success, "Invalid input: Should NOT be valid against schema")).describe("Canonical placement definition published in a publisher's adagents.json. Defines stable placement IDs that products can reuse and that authorization rules can reference. When a product reuses a registered placement_id, it is referring to this same semantic placement, not inventing a new one with the same ID.")).describe("Canonical placement definitions for properties in this file. Products SHOULD reuse these placement_id values when exposing inventory in get_products, and authorized agents can scope authorization to these placement IDs.").optional(), "formats": z.array(z.intersection(z.object({ "format_option_id": z.string().describe("Stable identifier for this declaration within its namespace. REQUIRED when a product contains multiple declarations with the same format_kind and SHOULD be set on every entry. Publisher-backed options pair it with publisher_domain; product-local options omit publisher_domain. When a single declaration has a unique format_kind and no ID, buyers author canonically with format_kind plus params; they MUST NOT fall back to deprecated format_ids merely because this optional ID is absent. Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'.").optional(), "publisher_domain": z.string().regex(new RegExp("^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$")).describe("Namespace for `format_option_id` when this declaration references or narrows a publisher-declared format option from that publisher's adagents.json top-level `formats[]`. Product-local options omit this field and are selected by `format_option_id` within the target product.").optional(), "display_name": z.string().describe("Optional seller-controlled human-readable label for this format declaration. Used by buyer dashboards, catalog UIs, and reporting surfaces to show a seller's own naming ('Homepage Takeover', 'Branded Canvas', 'Reels Premium Video') rather than the raw `format_kind` or `format_option_id`. Has no machine semantics — buyer agents route on `format_kind` and `format_option_id`; `display_name` is purely for human presentation. Freeform; no enumeration. Sellers SHOULD keep it stable once published to avoid dashboard churn.").optional(), "sample_render_url": z.string().url().regex(new RegExp("^https://")).describe("Optional public HTTPS page where a human can inspect a sample render of this declaration using assets chosen by the party publishing the enclosing declaration. Consumers MUST identify that source correctly: publisher or community mirror for `adagents.json` `formats[]`, seller for product or inline-placement declarations, and creative agent for `creative.supported_formats`. Informational only: this is not a renderer endpoint, buyer-asset preview, validation result, creative approval, proof of publisher acceptance, or guarantee of live delivery. Declaring parties SHOULD keep the URL stable while the declaration is active.").optional(), "applies_to_channels": z.array(z.enum(["display","olv","social","search","ctv","linear_tv","radio","streaming_audio","podcast","dooh","ooh","print","cinema","email","gaming","retail_media","influencer","affiliate","product_placement","sponsored_intelligence"]).describe("Standardized advertising media channels describing how buyers allocate budget. Channels are planning abstractions, not technical substrates. See the Media Channel Taxonomy specification for detailed definitions.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Optional subset of the parent product's `channels` to which this declaration applies. When omitted, the declaration applies to ALL channels declared on the product. Lets a multi-channel product (e.g., `channels: ['display', 'video']`) carry distinct format_options per channel — `format_options: [{format_kind: 'image', applies_to_channels: ['display']}, {format_kind: 'video_hosted', applies_to_channels: ['video']}]`. Buyers ship channel-appropriate manifests per `applies_to_channels`.").optional(), "seller_preference": z.enum(["preferred","accepted","discouraged"]).describe("Optional soft routing hint *within* a product's accepted set of formats — NOT an enforcement axis. `preferred` — seller actively recommends this format (often because of measurement, viewability, or render-quality differences); `accepted` — supported on equal footing with other format_options (default when omitted); `discouraged` — supported but suboptimal (e.g., legacy 3p-tag where the seller would prefer html5 for OM-SDK coverage). Buyer agents picking between format_options SHOULD respect seller preferences when their own constraints don't override.\n\n**Not an enforcement axis (normative).** `seller_preference` does NOT carry the meaning of 'this format won't work / required-only'. That case is structural: `format_options[]` IS the closed set of accepted formats; anything outside the list is rejected at `create_media_buy` regardless of preference. A seller that accepts only one format lists exactly that one entry — the structural fact does the enforcement work, no enum value needed. There is intentionally no `required` value; preference is bounded to *ranking within the already-accepted set*, not gating into it.").optional(), "locale_policy": z.object({ "accepted_language_ranges": z.array(z.string().regex(new RegExp("^(?:[a-z]{2,8}(?:-[A-Z][a-z]{3})?(?:-(?:[A-Z]{2}|[0-9]{3}))?(?:-(?:[a-z0-9]{5,8}|[0-9][a-z0-9]{3}))*(?:-[0-9a-wy-z](?:-[a-z0-9]{2,8})+)*(?:-x(?:-[a-z0-9]{1,8})+)?|x(?:-[a-z0-9]{1,8})+)$")).min(2).max(63).describe("A well-formed BCP 47 language tag used by AdCP only as language identity. Script and region may refine that identity; other valid BCP 47 subtags remain part of tag matching but do not make this a general locale-settings object. It does not determine currency, time zone, number/date formatting, market, or legal jurisdiction. The AdCP canonical wire profile requires lower-case language and variants, title-case script, and upper-case region (for example `en-US`, `zh-Hant-TW`, or `x-private`). RFC 5646 comparisons are case-insensitive and its case regularization is optional; AdCP intentionally requires this stricter single wire spelling and receivers MUST reject differently cased tags rather than silently normalizing them. The schema pattern enforces the AdCP casing profile and extension structure for commonly used tags; conforming receivers additionally validate the complete RFC 5646 grammar and registry rules. Every new AdCP field carrying BCP 47 language identity or a concrete language range MUST reference this schema instead of declaring independent string constraints.")).max(50).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Concrete canonical BCP 47 language ranges accepted by this format option. RFC 4647 Basic Filtering is directional: seller range fr accepts variant fr-CA, but seller range fr-CA does not accept variant fr or fr-FR. Use zxx explicitly for language-neutral creative; und means unknown and is not a wildcard.") }).catchall(z.any()).describe("Optional seller-enforced creative-locale constraint for this format option. This is product/placement eligibility, not a new format kind or synthetic locale-specific format ID. Because legacy format_ids cannot preserve this constraint, declarations carrying locale_policy MUST set canonical_formats_only to true and MUST NOT carry v1_format_ref.").optional(), "canonical_formats_only": z.boolean().describe("When true, this format declaration has no clean v1 projection and SDKs MUST NOT synthesize a v1 `format_id` for it. Buyers reading the product on the v1 wire path see this declaration absent from `format_ids`; only v2-aware buyers (reading `format_options`) discover it. Set explicitly for `format_kind: \"custom\"` declarations (no canonical exists in v1 to project onto) and for declarations whose canonical/parameter shape cannot round-trip through a v1 named format without semantic loss. The protocol does NOT mint synthetic v1 format_ids for unmappable declarations — the alternative (an `aao-synth/*` namespace populated automatically) was considered and rejected because adopters would index on synthetic IDs that have no stable identity. Producers SHOULD set `canonical_formats_only: true` rather than omit the declaration from `format_options` — explicit v2-only is more useful than silent absence.").default(false), "experimental": z.boolean().describe("When true, this seller's specific canonical declaration may not work as declared even if the underlying canonical is stable. Buyers SHOULD preflight it with validate_input or in a sandbox before routing production budget and SHOULD filter it from default views unless the caller opts in. Experimental status never makes the deprecated named-format path preferable. This field is independent of the canonical's own experimental flag and replaces the earlier runtime_status enum.").default(false), "format_shape": z.string().describe("REQUIRED when `format_kind: \"custom\"`; otherwise MUST be absent. Recognized global pattern this custom shape is an instance of, drawn from the [format-shape vocabulary registry](/schemas/core/format-shape-vocabulary.json) (`branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, …). Non-canonical values are valid (validators MAY soft-warn) — adopters CAN ship a shape that isn't yet in the registry. Adding entries is a vocabulary PR. Once a `format_shape` entry sees 2+ adopters with substantively similar `format_schema` content for 90+ days, the working group may promote it to a first-class canonical. Retired vocabulary entries `multi_state_display` and `multi_placement_takeover` remain temporarily recognizable for migration; new declarations MUST use their promoted canonicals and validators SHOULD emit `FORMAT_SHAPE_PROMOTED`. `roadblock` remains an inventory/exclusivity classifier and is not a promoted creative format.").optional(), "v1_format_ref": z.array(z.object({ "agent_url": z.string().url().describe("URL of the agent that defines this format (e.g., 'https://creative.adcontextprotocol.org' for standard formats, or 'https://publisher.com/.well-known/adcp/sales' for custom formats). Callers comparing two `format-id` values MUST canonicalize `agent_url` per the AdCP URL canonicalization rules before treating two formats as the same. See docs/reference/url-canonicalization."), "id": z.string().regex(new RegExp("^[a-zA-Z0-9_-]+$")).describe("Format identifier within the agent's namespace (e.g., 'display_static', 'video_hosted', 'audio_standard'). When used alone, references a template format. When combined with dimension/duration fields, creates a parameterized format ID for a specific variant."), "width": z.number().int().gte(1).describe("Width in pixels for visual formats. When specified, height must also be specified. Both fields together create a parameterized format ID for dimension-specific variants.").optional(), "height": z.number().int().gte(1).describe("Height in pixels for visual formats. When specified, width must also be specified. Both fields together create a parameterized format ID for dimension-specific variants.").optional(), "duration_ms": z.number().gte(1).describe("Duration in milliseconds for time-based formats (video, audio). When specified, creates a parameterized format ID. Omit to reference a template format without parameters.").optional(), "pixel_ratio": z.number().gt(0).describe("Required intrinsic-pixel density for a parameterized visual format, expressed as intrinsic pixels per logical pixel. Requires `width` and `height`. Example: `{id: \"display_image\", width: 300, height: 250, pixel_ratio: 2}` identifies a 300×250 logical render supplied by a 600×500 image. Omit for the backward-compatible 1x variant.").optional() }).catchall(z.any()).describe("**DEPRECATED in 3.2.** Legacy named-format reference retained for 3.x compatibility. This JSON object — never a plain string — identifies a format by its declaring agent and local slug, and may carry legacy logical dimensions, pixel ratio, or duration parameters. New products and manifests use canonical `format_options`, `format_kind`, and `format_option_ref`; creative-agent build routing uses `creative.supported_formats[].capability_id`.")).describe("Authoritative v2 → v1 link, expressed as an array of one or more v1 `format_id` ({agent_url, id}) values. Each entry asserts that this canonical-formats declaration IS the same underlying format as the referenced v1 named format. Always an array (single-ref is `[{...}]`) so the multi-size case below has a clean wire shape — adopters surveyed in the SDK implementor review pushed for this over the lossy single-ref form.\n\nThe v2 declaration's `params` MUST narrow (be compatible with) each referenced v1 format's `requirements` — see the 'Narrows — formal definition' section in canonical-formats.mdx. SDKs comparing dual-emitted shapes (`Product.format_ids[]` ⊇ entries from `v1_format_ref` AND `Product.format_options[]` carrying this declaration) treat the link as the authoritative pairing and run the narrowing check between this declaration and EACH referenced v1 format file's `requirements`.\n\n**Multi-size fan-out (normative).** When the declaration carries `params.sizes: [{w,h}, ...]` (multi-size flexible slot), sellers SHOULD carry one `v1_format_ref[]` entry per size, each pointing at the per-size v1 named format in the AAO catalog. Example: a multi-size image declaration with `sizes: [300x250, 728x90, 970x250]` SHOULD carry `v1_format_ref: [{aao, display_300x250_image}, {aao, display_728x90_image}, {aao, display_970x250_image}]`. v1-only buyers then see the product on all three sizes via the `format_ids[]` dual-emission. When `v1_format_ref[]` count < `sizes[]` count, SDKs MUST emit `FORMAT_DECLARATION_V1_LOSSY_MULTI_SIZE` on the response `errors[]` (advisory, alongside the partial-coverage v1 emit — NOT in place of it). SDKs MAY (non-normative) fan out automatically by catalog lookup when `v1_format_ref[]` has length 1 and `sizes[]` has length N — opt-in, requires catalog access; sellers asserting refs is the source of truth.\n\nMutually exclusive with `canonical_formats_only: true` — a declaration can EITHER assert no v1 projection (`canonical_formats_only: true`) OR link to v1 named formats (`v1_format_ref[]`), never both. When neither is present, SDKs fall back to the resolution order in `v1-canonical-mapping.json` (seller's explicit `canonical` field on the v1 file → registry glob → structural match → fail-closed).\n\nThis is the v2-side authoritative replacement for the v1-side `canonical_parameters` field on `format.json` (which is deprecated for 3.1, removed at 4.0). Sellers SHOULD prefer authoring v2 declarations with `v1_format_ref[]` over mirroring the v2 shape onto v1 files via `canonical_parameters`; the directional link (v2 declaration → v1 identifiers) is the same fact without the parallel-shape drift surface.\n\n**AAO-hosted convention (normative).** For IAB-standard formats (image dimensions, VAST/DAAST tags, standard third-party tags, HTML5 banner bundles), sellers SHOULD point each `v1_format_ref[].agent_url` at the AAO-hosted canonical agent URL `https://creative.adcontextprotocol.org` and use the registry-published id (e.g., `display_300x250_image`, `video_vast_30s`, `audio_standard_30s`, `display_300x250_html`, `display_js`). This converges the v1-wire namespace: every seller's IAB MREC points at the same `{agent_url, id}` pair, so v1-only buyers' allowlists work uniformly. Without this convention, every publisher's 300x250 ships with a different `v1_format_ref` (theirs vs nytimes.example vs cnn.example vs …) and the v1 wire fragments into per-publisher namespaces — exactly what canonical-formats was designed to eliminate.\n\nFor platform-specific formats (Meta Reels, TikTok Spark, Snap Spotlight, etc.), each `v1_format_ref[].agent_url` SHOULD point at the platform's own agent_url when the platform has adopted AdCP and publishes its own `adagents.json` with `formats[]`. When the platform has NOT adopted AdCP, sellers SHOULD point at the AAO community-registry mirror — `https://creative.adcontextprotocol.org/translated/` + `id: ` (e.g., `https://creative.adcontextprotocol.org/translated/meta` + `id: meta_reels`). This keeps the v1 namespace converged across all sellers selling that platform's inventory until the platform owns its own adagents.json.\n\n**Platform-adoption cutover (normative).** When a platform adopts AdCP and publishes its own adagents.json, sellers MUST update `v1_format_ref[].agent_url` to the platform's adopted agent_url in the same minor release as the AAO mirror entry's `superseded_by` field goes live (see `static/schemas/source/adagents.json#superseded_by`). The AAO mirror entry SHOULD continue serving for ≥1 minor release after `superseded_by` is set, returning an advisory 'superseded' marker so v1 buyer allowlists keyed on the mirror URL get an explicit signal rather than a silent break. **Identity-confusion note**: the mirror URL is *format-shape namespace*, NOT seller identity. Inventory authorization always flows from `authorized_agents[]` + publisher signing keys; a buyer matching `v1_format_ref[].agent_url` against an allowlist is matching format-shape provenance, not seller identity.\n\n**Mirror domain migration (3.1).** Earlier drafts used `https://mirror.adcontextprotocol.org/translated/`. As of this release, the convention is `https://creative.adcontextprotocol.org/translated/` — sibling content under the AAO catalog domain we already host. Adopters who hardcoded the earlier mirror URL MUST migrate to the new path; the canonical-formats.mdx migration section documents the move. No transitional redirect is currently published (the earlier subdomain was never provisioned).\n\nFor seller-bespoke formats (a publisher's `acme_homepage_takeover` that doesn't fit IAB conventions), each `v1_format_ref[].agent_url` is the seller's own agent_url and the id is seller-namespaced. These won't appear in `v1-canonical-mapping.json`'s registry; they're seller-asserted only.").optional(), "format_schema": z.object({ "uri": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL identifying the extension. `https://` is mandatory — `http://`, `file://`, `data:`, and other schemes are rejected at the schema layer (defense-in-depth on top of the fetch-contract normative rules). The URI base is the owning agent's URL; the path identifies the extension within that agent. Example: 'https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel'. The full fetch contract — SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds — is documented on `product-format-declaration.json#format_schema` and applies to ALL fetches of this reference shape regardless of whether the field is named `format_schema` (load-bearing for validation) or `platform_extensions` (informational); the *transport* rules are identical, only the *consumption* semantics differ."), "digest": z.string().regex(new RegExp("^sha256:[a-f0-9]{64}$")).describe("SHA-256 content digest of the extension definition (sha256:). Used to detect drift — if the agent revises the extension, the digest changes and cached definitions become invalid.") }).catchall(z.any()).describe("REQUIRED when `format_kind: \"custom\"`; otherwise MUST be absent. URI+digest reference to a fetchable schema describing this custom shape's actual `params` and `slots`. Same hosting model as `platform_extensions`: open-ecosystem publishers host the artifact at the canonical URI on their subdomain; closed-platform / walled-garden shapes resolve through the AAO mirror at `https://creative.adcontextprotocol.org/translated/...`. Buyer agents fetch by `uri@digest` (immutable per digest, aggressive caching, `Cache-Control: public, max-age=31536000, immutable`), validate `params` and `slots` against the fetched schema, and reason about manifests structurally — same mechanic as platform_extensions but at the format-structure level. Without `format_schema`, custom shapes would be opaque to buyer agents and the protocol would regress to per-seller integration code; that's why the schema is required, not optional.\n\n**Fetch contract (normative)** — `format_schema` is load-bearing for validation (unlike `platform_extensions`, which is informational on the *consumption* side). The *transport* rules below apply identically to BOTH fields — any SDK fetching a `platform-extension-ref.json` URI MUST apply this contract regardless of whether the field name is `format_schema` or `platform_extensions`. A shared SDK fetch path that drops to the weakest bar undermines `format_schema`'s hardening. The consumption distinction (load-bearing vs informational) is about *what the body means*; the transport distinction is `https`-and-allowlisted regardless.\n\n- **Transport**: `https` only. Buyers MUST reject `http://`, `file://`, `data:`, and any non-`https` scheme. The URI MUST resolve to a JSON document that is itself a valid JSON Schema (Draft 07 or 2020-12; producers MUST declare `$schema`).\n- **SSRF protection**: buyers MUST resolve the URI hostname and reject if any resolved address is in RFC 1918 private space (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`), loopback (`127.0.0.0/8`, `::1`), link-local (`169.254.0.0/16`, `fe80::/10`), CGNAT (`100.64.0.0/10`), or any RFC 6761 special-use name (`.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. Buyers MUST pin the connection to the resolved IP (or re-resolve and re-validate the allowlist per request) to defeat DNS rebinding.\n- **HTTP redirects**: MUST be disabled. If a follow is implemented at all, the redirect target MUST pass the same scheme + SSRF + allowlist checks; otherwise the fetch hard-fails. Open redirects on same-origin paths are otherwise a free SSRF primitive.\n- **Response size cap**: response body MUST be capped at 1 MiB. Enforce during streaming, not after full buffering. Over-cap hard-fails identically to digest mismatch.\n- **Timeout**: SDKs SHOULD apply a fetch timeout ≤5 seconds. Timeout SHOULD be treated identically to an HTTP 5xx response (transient — retry policy at the SDK's discretion; on persistent failure surface as unresolved and skip the declaration for this session).\n- **Digest verification**: SHA-256 of the response body MUST equal `digest`. **Digest mismatch is a hard fail** — the buyer MUST treat the format declaration as unresolvable and MUST NOT validate manifests against the mismatched body. A divergent digest is either a malicious substitution or producer error; either way, falling back to the un-verified body breaks the trust model. Digest format: `sha256:` prefix + 64 lowercase hex characters. Cache key is `uri@digest`; digest mismatch MUST NOT be cached as a negative result keyed on `uri` alone (defeats CDN-flap recovery), and MUST be distinguishable in telemetry from network 5xx / 404 (sustained mismatch is a substitution-attack signal, not a flap).\n- **Sandboxing of `$ref`**: fetched schemas MAY use `$ref`. Buyers MUST resolve `$ref` only to URIs that are (a) same-origin as the parent `format_schema.uri` after RFC 3986 §6 normalization (lowercase scheme + host, strip default port, normalize path dot-segments, no userinfo component), OR (b) hosted under the AAO catalog domain (`https://creative.adcontextprotocol.org/...`), OR (c) intra-document JSON Pointer refs (`#/...`) bounded to the parent document's parsed tree. Cross-origin `$ref` to arbitrary URIs MUST be rejected. `$ref: file://...` MUST be rejected unconditionally. Transitive `$ref` chains MUST be bounded at depth ≤8 AND `$ref` count ≤256 across the resolved tree (depth 8 with breadth 100 per level is 10^16 nodes — depth alone is not enough). Publishers SHOULD inline rather than $ref where possible.\n- **Schema-compile bounds (DoS protection)**: validators MUST bound CPU/memory on fetched schemas. Recommended: compiled-schema keyword count ≤10 000, `pattern` regexes evaluated with a non-backtracking engine (re2) OR under a per-pattern timeout, per-manifest validation budget ≤250 ms (exceeded budget → treat manifest as invalid, surface telemetry signal). Without these, a 'valid' schema with catastrophic regex backtracking or exponential `allOf`/`anyOf` expansion pins a CPU forever.\n- **Cache**: buyers cache fetched schemas by `uri@digest` and treat them as immutable (the same hosting contract as `platform_extensions`). On `404`, network partition, or persistent fetch failure, buyers SHOULD degrade gracefully (treat the declaration as unresolved, skip it for the current `get_products` response, surface via `errors[]` with the relevant code) rather than failing the entire session.\n- **Schema-not-valid handling**: if the fetched body parses as JSON but is not a valid JSON Schema, the buyer MUST treat the declaration as unresolvable (same as digest mismatch) and surface via `errors[]`. Validators MUST NOT attempt partial validation against an invalid schema.\n- **AAO catalog trust**: `https://creative.adcontextprotocol.org/*` is a single trust anchor in the same-origin allowlist; compromise of the catalog domain or its CA compromises every buyer agent. Catalog-served bodies MUST be digest-pinned identically to origin fetches (the digest is on the *parent* `format_schema.uri@digest`, not on the catalog response). Future hardening (signed bodies, transparency log) is tracked separately.").optional() }).and(z.union([z.object({ "format_kind": z.literal("image"), "params": z.intersection(z.intersection(z.object({ "experimental": z.boolean().describe("When true, this canonical or seller narrowing may not work as declared. Adopters SHOULD preflight it with validate_input or in a sandbox and SHOULD NOT route production budget without testing; experimental status never makes the deprecated v1 path preferable. Drivers include unsettled spec shape, an adopter runtime gap, and custom shapes awaiting promotion. This replaces the earlier status plus runtime_status axes. Sellers SHOULD set experimental whenever a canonical or declaration is not production-ready.").default(false), "deprecated": z.boolean().describe("When true, this canonical (or a seller's specific narrowing of it) is going away. Existing adopters are supported through the deprecation cycle; new adoption is discouraged. Pair with `migration_target_version` to indicate when the canonical is expected to be removed. Distinct from `experimental`: an experimental canonical may stabilize and stop being experimental; a deprecated canonical is on a sunset path.").default(false), "v1_translatable": z.boolean().describe("Whether this canonical has any v1 named-format equivalent. `true` (default) — the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1→v2 projection via `v1-canonical-mapping.json` is meaningful. `false` — the canonical is inherently new in v2 and has no v1 form; v1's `list_creative_formats` couldn't express it because the underlying concept (algorithmic surface composition, AI-surface mentions, retail-media catalog placements, multi-card carousels) didn't exist as a v1 named-format archetype.\n\nLets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The six inherently-v2 canonicals in 3.2 are `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`, `seller_rendered_stateful_display`, and `coordinated_placements`.").default(true), "since_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version that introduced this canonical (e.g., '3.1', '3.2'). Lets adopters reason about minimum protocol version requirements when consuming a format declaration. Patch precision is intentionally rejected — canonicals are introduced at minor-version boundaries.").optional(), "migration_target_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version by which the working group expects this canonical to stabilize, surface a breaking revision, or (when `deprecated: true`) be removed. Patch precision is intentionally rejected — canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown').").optional(), "composition_model": z.enum(["deterministic","algorithmic"]).describe("Whether the surface composes deterministically (buyer can predict per-slot rendering — sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing — responsive_creative, agent_placement).").optional(), "provenance_required": z.boolean().describe("When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent.").optional(), "platform_extensions": z.array(z.object({ "uri": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL identifying the extension. `https://` is mandatory — `http://`, `file://`, `data:`, and other schemes are rejected at the schema layer (defense-in-depth on top of the fetch-contract normative rules). The URI base is the owning agent's URL; the path identifies the extension within that agent. Example: 'https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel'. The full fetch contract — SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds — is documented on `product-format-declaration.json#format_schema` and applies to ALL fetches of this reference shape regardless of whether the field is named `format_schema` (load-bearing for validation) or `platform_extensions` (informational); the *transport* rules are identical, only the *consumption* semantics differ."), "digest": z.string().regex(new RegExp("^sha256:[a-f0-9]{64}$")).describe("SHA-256 content digest of the extension definition (sha256:). Used to detect drift — if the agent revises the extension, the digest changes and cached definitions become invalid.") }).catchall(z.any()).describe("Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` — divergent digests in one response indicate producer-side error (e.g., concurrent extension revision mid-render). Buyers encountering divergent digests for the same URI MUST fail closed: treat all references to that URI as unresolved and surface a validation error rather than picking one branch silently. **Across responses**, digest divergence is normal — extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.")).describe("Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\n\n**Collision precedence (normative).** When two or more `platform_extensions[]` entries on the same declaration extend the same target (e.g., both extend `tracking`) with overlapping field names, **array order is authoritative — later entries override earlier ones on a per-field basis** (last-in-array-wins). SDKs MUST surface the overlap via the `errors[]` array on the `get_products` response with a structured code (`FORMAT_DECLARATION_DIVERGENT` is appropriate when the overlap appears across dual-emitted shapes; a producer-self-emitted overlap on a single declaration SHOULD use the same code with `error.details: { collision_kind: \"platform_extension_field\", target, overlapping_fields, winning_extension_uri }`). Producers SHOULD avoid the collision by emitting one extension per target or by partitioning fields across extensions; the deterministic precedence is for last-resort consistency across SDK implementations, not a sanctioned merging strategy.").optional(), "synthesis_nondeterministic": z.boolean().describe("When true, the format's production pipeline is genuinely nondeterministic — the platform cannot guarantee that synthesis from a given input set produces in-spec output. Veo / Sora / Runway-class generative video, and other AI-synthesis flows where output dimensions, duration, or quality vary per run. Implies a different validation contract: predictive `validate_input` is impossible; the platform's own post-synthesis QA loop applies; if the QA loop exhausts without producing a valid artifact, `build_creative` returns task_failed with a synthesis_failed reason. Distinct from `composition_model` (which describes how the surface composes per-slot rendering, not whether synthesis is deterministic). When false or absent, the format's production is predictable enough that `validate_input` can predict output properties from input properties.\n\n**Compatibility with `asset_source` / `item_production_model`**: `synthesis_nondeterministic: true` MAY pair with any of `seller_pre_rendered_from_brief`, `seller_human_designed`, or `agent_synthesized` (the QA loop is concept-level, not source-specific — 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.").default(false), "slots": z.array(z.object({ "asset_group_id": z.string().describe("Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings."), "asset_type": z.enum(["image","video","audio","text","markdown","url","html","css","javascript","vast","daast","webhook","brief","catalog","published_post","zip","card","object","pixel_tracker","vast_tracker","daast_tracker"]).describe("Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `published_post` is an existing-post reference asset, not uploaded media bytes and not a catalog row. `card` is the multi-card carousel element type (see card-asset.json). `pixel_tracker` / `vast_tracker` / `daast_tracker` are the renderer-fired measurement-tracker primitives — see `/schemas/core/assets/pixel-tracker-asset.json` and the VAST / DAAST tracker schemas. `object` is a last-resort fallback for structured non-asset inputs that don't fit any primitive asset_type — prefer specific types whenever possible."), "required": z.boolean().describe("Whether this slot is required for a valid manifest.").default(false), "min": z.number().int().gte(0).describe("Minimum count for repeatable / pool slots.").optional(), "max": z.number().int().gte(1).describe("Maximum count for repeatable / pool slots.").optional(), "max_chars": z.number().int().gte(1).describe("Per-slot character limit. Valid only when `asset_type` is `text`, `markdown`, or `brief`. Mutually exclusive with `max_size_kb` (which applies to binary asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "max_size_kb": z.number().int().gte(1).describe("Per-slot file size limit in kilobytes. Valid only when `asset_type` is `image`, `video`, `audio`, or `zip`. Mutually exclusive with `max_chars` (which applies to text asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Accepted intrinsic-pixel densities for this image-bearing slot. Valid when `asset_type` is `image`, and on a `card` slot where it constrains each card's image media (video media is unaffected). This makes density available to every canonical carrying image assets (native, carousel, responsive, companion images, and image itself), not only `format_kind: image`. When the image canonical also declares top-level `params.pixel_ratios`, the effective set is the intersection; an empty intersection is invalid. One matching asset satisfies the slot unless `required_pixel_ratios` requires rendition coverage.").optional(), "required_pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Required density coverage for an image rendition set. Valid only when `asset_type` is `image` and `pixel_ratios` is also declared. Every value MUST appear in the effective accepted set after intersecting any top-level image `params.pixel_ratios`, and the manifest slot value MUST be an array containing exactly one matching image rendition for each required ratio. Other accepted ratios remain optional. For example, `pixel_ratios: [1, 1.5, 2]` with `required_pixel_ratios: [1, 2]` requires the 1x and 2x renditions while making 1.5x optional. SDKs enforce intersection, subset, coverage, and duplicate-ratio rules because JSON Schema draft-07 cannot express them generically.").optional(), "logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("When `asset_group_id` is `logo`, renderer-facing brand.json logo slots acceptable for this format slot. Producers selecting from brand.json SHOULD prefer `logos[]` entries whose `slots[]` intersects this list, then apply `visual_guidelines.logo_usage_rules[]`.").optional(), "required_logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Subset of `logo_slots` for which this format expects explicit logo coverage. A manifest or brand-derived logo pool SHOULD include at least one usable logo for each required slot; if coverage is missing, builders SHOULD surface a validation warning or approval mapping instead of guessing from prose.").optional(), "description": z.string().describe("Human-readable description of what the slot expects from the buyer.").optional(), "consumed_for_production": z.boolean().describe("Dispatch hint for `build_creative` and v1↔v2 wire translators: when `true`, the slot's value is consumed as INPUT to a production step (host-read script, brief copy fed to generative synthesis, catalog feed driving per-SKU rendering) and is not rendered verbatim. When `false` (default), the slot's value is rendered verbatim on the placement (image bytes, video file, display tag).\n\nMotivates the v1↔v2 dispatch table: pre-v2 buyers shipped production-consumed inputs separately in a `inputs` map on the build_creative request; v2 collapses inputs and rendered assets into a single `assets` map keyed by `asset_group_id`. SDK translators between v1 and v2 use this flag per canonical to know which assets in the v2 manifest map back to v1 `inputs` vs v1 `assets`. Without the per-slot flag the dispatch table lives in adopter code and every SDK gets it slightly different.\n\nProducers SHOULD set this explicitly on slots whose consumption pattern isn't obvious (host-read scripts on `audio_hosted`, briefs on generative `video_hosted`, catalog feeds on `sponsored_placement`). For canonicals where every slot is render-verbatim (`image`, `display_tag`, `video_vast`), the default `false` is sufficient and the flag MAY be omitted.").default(false) }).catchall(z.any()).and(z.intersection(z.intersection(z.any(), z.intersection(z.any(), z.any())), z.intersection(z.any(), z.intersection(z.any(), z.any()))))).describe("Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.").optional(), "required_connections": z.array(z.object({ "provider": z.string().describe("Stable provider or platform namespace, preferably lowercase. Examples: `social.example`, `shortvideo.example`, or a seller-defined namespace. Omit only when the requirement is provider-agnostic, or when an `authorization_url` fully routes the human to the correct provider-specific connection flow.").optional(), "connection_type": z.enum(["advertiser_account","publisher_identity","post_authorization"]).describe("Kind of downstream connection required. `advertiser_account` is the platform account used to buy/manage ads. `publisher_identity` is the creator, page, channel, organization, or profile that owns source posts. `post_authorization` is a post-scoped grant when the platform authorizes individual posts instead of, or in addition to, the owning identity."), "required_for": z.array(z.string().min(1)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Concrete AdCP protocol operation names that require this downstream connection. Sellers SHOULD include this in product declarations when the requirement is known ahead of time, and in AUTHORIZATION_REQUIRED details when it explains the failed operation. Prefer specific operation names such as `list_creatives`, `sync_creatives`, `create_media_buy`, `get_media_buy_delivery`, or `get_creative_delivery` over broad category labels such as `reporting`.").optional(), "scope": z.enum(["account","identity","post","unknown"]).describe("Granularity of the downstream grant.").optional(), "status": z.enum(["connected","missing","pending","expired","revoked","not_required","unknown"]).describe("Current seller-observed state for this downstream connection when known. Product declarations MAY omit status or use `unknown`; AUTHORIZATION_REQUIRED details SHOULD use `missing`, `expired`, or `revoked` for the connection that blocked the call.").optional(), "connection_id": z.string().describe("Seller-defined identifier for an already-created downstream connection. Omit when no connection exists yet or when exposing it would leak platform/account state.").optional(), "resource_ref": z.object({ "platform_account_id": z.string().describe("Provider-native advertiser or business account id, when safe to disclose.").optional(), "identity_id": z.string().describe("Provider-native creator, page, channel, organization, or profile id, when safe to disclose.").optional(), "handle": z.string().describe("Provider-native public handle for the owning identity, when available.").optional(), "profile_url": z.string().url().describe("Public URL for the owning identity, when available.").optional(), "post_id": z.string().describe("Provider-native post id, when the grant is post-scoped or the failed request referenced a specific post.").optional(), "post_url": z.string().url().describe("Public URL for the referenced post, when available.").optional() }).catchall(z.any()).describe("Optional opaque provider-native resource hint, such as a platform account id, profile URL, handle, channel id, post id, or post URL. This is a hint for routing authorization, not proof that authorization exists.").optional(), "authorization_url": z.string().url().describe("Seller-hosted or provider-hosted URL where a human can complete or restore this downstream connection.").optional(), "authorization_instructions": z.string().describe("Human-readable instructions for completing or restoring this downstream connection.").optional(), "expires_at": z.string().datetime().describe("Expiration time for the downstream grant, when known.").optional() }).catchall(z.any()).and(z.any()).describe("A seller/platform-side connection or grant required by a product, format, or request. This is not the AdCP caller credential: the AdCP request is still authenticated once, and the seller uses these stored downstream connections to call a platform or service on the buyer's behalf. Use this shape for platforms that require more than one downstream grant, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.")).describe("Downstream platform connections or grants required to use this format declaration. These are in addition to the single AdCP caller credential. Use this when a platform product requires multiple downstream grants, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.").optional(), "reference_mutability": z.enum(["immutable_snapshot","mutable_requires_reapproval","mutable_auto_recheck"]).describe("Policy for formats whose `slots` accept a `published_post` reference. `immutable_snapshot`: seller snapshots the referenced post at approval and later source changes do not change the served creative. `mutable_requires_reapproval`: the source post may change and material changes require review before continued serving. `mutable_auto_recheck`: the source post may change and the seller continuously or periodically rechecks authorization/policy without requiring buyer resubmission. Omit when the format has no `published_post` slot.").optional(), "production_window_business_days": z.number().int().gte(0).describe("Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative).").optional() }).catchall(z.any()).describe("Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints)."), z.union([z.any().refine((value) => !z.union([z.any(), z.any(), z.any(), z.any(), z.any()]).safeParse(value).success, "Invalid input: Should NOT be valid against schema"), z.any().refine((value) => !z.union([z.any(), z.any(), z.any(), z.any(), z.any(), z.any()]).safeParse(value).success, "Invalid input: Should NOT be valid against schema"), z.union([z.any(), z.any(), z.any(), z.any()]), z.any().refine((value) => !z.union([z.any(), z.any(), z.any(), z.any(), z.any(), z.any(), z.any()]).safeParse(value).success, "Invalid input: Should NOT be valid against schema")]).describe("Exactly one of: (a) fixed (`width` + `height` both set), (b) multi-size (`sizes` set), (c) responsive (any of `min_width`/`max_width`/`min_height`/`max_height` set), (d) none (no size constraint declared — accepts any dimensions). Combining modes (e.g., `width` + `sizes`) is rejected at schema layer; same rule on `html5` and `display_tag` canonicals.")), z.intersection(z.any(), z.any())).describe("Static image creative format. Slots: `image_main` (image asset, file or hosted URL), optional `headline` (text), `body_text` (text), `cta` (text/enum), `landing_page_url` (url). Tracking model: impression pixel + click URL via universal_macros, with optional viewability pixel. Distinct from `html5` (interactive bundles) and `display_tag` (third-party served). AR/dimensions narrow to specific sizes via product parameters — covers IAB display sizes (300x250, 728x90, 970x250, etc.) without a separate iab_size enum.") }), z.object({ "format_kind": z.literal("html5"), "params": z.intersection(z.object({ "experimental": z.boolean().describe("When true, this canonical or seller narrowing may not work as declared. Adopters SHOULD preflight it with validate_input or in a sandbox and SHOULD NOT route production budget without testing; experimental status never makes the deprecated v1 path preferable. Drivers include unsettled spec shape, an adopter runtime gap, and custom shapes awaiting promotion. This replaces the earlier status plus runtime_status axes. Sellers SHOULD set experimental whenever a canonical or declaration is not production-ready.").default(false), "deprecated": z.boolean().describe("When true, this canonical (or a seller's specific narrowing of it) is going away. Existing adopters are supported through the deprecation cycle; new adoption is discouraged. Pair with `migration_target_version` to indicate when the canonical is expected to be removed. Distinct from `experimental`: an experimental canonical may stabilize and stop being experimental; a deprecated canonical is on a sunset path.").default(false), "v1_translatable": z.boolean().describe("Whether this canonical has any v1 named-format equivalent. `true` (default) — the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1→v2 projection via `v1-canonical-mapping.json` is meaningful. `false` — the canonical is inherently new in v2 and has no v1 form; v1's `list_creative_formats` couldn't express it because the underlying concept (algorithmic surface composition, AI-surface mentions, retail-media catalog placements, multi-card carousels) didn't exist as a v1 named-format archetype.\n\nLets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The six inherently-v2 canonicals in 3.2 are `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`, `seller_rendered_stateful_display`, and `coordinated_placements`.").default(true), "since_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version that introduced this canonical (e.g., '3.1', '3.2'). Lets adopters reason about minimum protocol version requirements when consuming a format declaration. Patch precision is intentionally rejected — canonicals are introduced at minor-version boundaries.").optional(), "migration_target_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version by which the working group expects this canonical to stabilize, surface a breaking revision, or (when `deprecated: true`) be removed. Patch precision is intentionally rejected — canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown').").optional(), "composition_model": z.enum(["deterministic","algorithmic"]).describe("Whether the surface composes deterministically (buyer can predict per-slot rendering — sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing — responsive_creative, agent_placement).").optional(), "provenance_required": z.boolean().describe("When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent.").optional(), "platform_extensions": z.array(z.object({ "uri": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL identifying the extension. `https://` is mandatory — `http://`, `file://`, `data:`, and other schemes are rejected at the schema layer (defense-in-depth on top of the fetch-contract normative rules). The URI base is the owning agent's URL; the path identifies the extension within that agent. Example: 'https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel'. The full fetch contract — SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds — is documented on `product-format-declaration.json#format_schema` and applies to ALL fetches of this reference shape regardless of whether the field is named `format_schema` (load-bearing for validation) or `platform_extensions` (informational); the *transport* rules are identical, only the *consumption* semantics differ."), "digest": z.string().regex(new RegExp("^sha256:[a-f0-9]{64}$")).describe("SHA-256 content digest of the extension definition (sha256:). Used to detect drift — if the agent revises the extension, the digest changes and cached definitions become invalid.") }).catchall(z.any()).describe("Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` — divergent digests in one response indicate producer-side error (e.g., concurrent extension revision mid-render). Buyers encountering divergent digests for the same URI MUST fail closed: treat all references to that URI as unresolved and surface a validation error rather than picking one branch silently. **Across responses**, digest divergence is normal — extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.")).describe("Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\n\n**Collision precedence (normative).** When two or more `platform_extensions[]` entries on the same declaration extend the same target (e.g., both extend `tracking`) with overlapping field names, **array order is authoritative — later entries override earlier ones on a per-field basis** (last-in-array-wins). SDKs MUST surface the overlap via the `errors[]` array on the `get_products` response with a structured code (`FORMAT_DECLARATION_DIVERGENT` is appropriate when the overlap appears across dual-emitted shapes; a producer-self-emitted overlap on a single declaration SHOULD use the same code with `error.details: { collision_kind: \"platform_extension_field\", target, overlapping_fields, winning_extension_uri }`). Producers SHOULD avoid the collision by emitting one extension per target or by partitioning fields across extensions; the deterministic precedence is for last-resort consistency across SDK implementations, not a sanctioned merging strategy.").optional(), "synthesis_nondeterministic": z.boolean().describe("When true, the format's production pipeline is genuinely nondeterministic — the platform cannot guarantee that synthesis from a given input set produces in-spec output. Veo / Sora / Runway-class generative video, and other AI-synthesis flows where output dimensions, duration, or quality vary per run. Implies a different validation contract: predictive `validate_input` is impossible; the platform's own post-synthesis QA loop applies; if the QA loop exhausts without producing a valid artifact, `build_creative` returns task_failed with a synthesis_failed reason. Distinct from `composition_model` (which describes how the surface composes per-slot rendering, not whether synthesis is deterministic). When false or absent, the format's production is predictable enough that `validate_input` can predict output properties from input properties.\n\n**Compatibility with `asset_source` / `item_production_model`**: `synthesis_nondeterministic: true` MAY pair with any of `seller_pre_rendered_from_brief`, `seller_human_designed`, or `agent_synthesized` (the QA loop is concept-level, not source-specific — 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.").default(false), "slots": z.array(z.object({ "asset_group_id": z.string().describe("Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings."), "asset_type": z.enum(["image","video","audio","text","markdown","url","html","css","javascript","vast","daast","webhook","brief","catalog","published_post","zip","card","object","pixel_tracker","vast_tracker","daast_tracker"]).describe("Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `published_post` is an existing-post reference asset, not uploaded media bytes and not a catalog row. `card` is the multi-card carousel element type (see card-asset.json). `pixel_tracker` / `vast_tracker` / `daast_tracker` are the renderer-fired measurement-tracker primitives — see `/schemas/core/assets/pixel-tracker-asset.json` and the VAST / DAAST tracker schemas. `object` is a last-resort fallback for structured non-asset inputs that don't fit any primitive asset_type — prefer specific types whenever possible."), "required": z.boolean().describe("Whether this slot is required for a valid manifest.").default(false), "min": z.number().int().gte(0).describe("Minimum count for repeatable / pool slots.").optional(), "max": z.number().int().gte(1).describe("Maximum count for repeatable / pool slots.").optional(), "max_chars": z.number().int().gte(1).describe("Per-slot character limit. Valid only when `asset_type` is `text`, `markdown`, or `brief`. Mutually exclusive with `max_size_kb` (which applies to binary asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "max_size_kb": z.number().int().gte(1).describe("Per-slot file size limit in kilobytes. Valid only when `asset_type` is `image`, `video`, `audio`, or `zip`. Mutually exclusive with `max_chars` (which applies to text asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Accepted intrinsic-pixel densities for this image-bearing slot. Valid when `asset_type` is `image`, and on a `card` slot where it constrains each card's image media (video media is unaffected). This makes density available to every canonical carrying image assets (native, carousel, responsive, companion images, and image itself), not only `format_kind: image`. When the image canonical also declares top-level `params.pixel_ratios`, the effective set is the intersection; an empty intersection is invalid. One matching asset satisfies the slot unless `required_pixel_ratios` requires rendition coverage.").optional(), "required_pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Required density coverage for an image rendition set. Valid only when `asset_type` is `image` and `pixel_ratios` is also declared. Every value MUST appear in the effective accepted set after intersecting any top-level image `params.pixel_ratios`, and the manifest slot value MUST be an array containing exactly one matching image rendition for each required ratio. Other accepted ratios remain optional. For example, `pixel_ratios: [1, 1.5, 2]` with `required_pixel_ratios: [1, 2]` requires the 1x and 2x renditions while making 1.5x optional. SDKs enforce intersection, subset, coverage, and duplicate-ratio rules because JSON Schema draft-07 cannot express them generically.").optional(), "logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("When `asset_group_id` is `logo`, renderer-facing brand.json logo slots acceptable for this format slot. Producers selecting from brand.json SHOULD prefer `logos[]` entries whose `slots[]` intersects this list, then apply `visual_guidelines.logo_usage_rules[]`.").optional(), "required_logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Subset of `logo_slots` for which this format expects explicit logo coverage. A manifest or brand-derived logo pool SHOULD include at least one usable logo for each required slot; if coverage is missing, builders SHOULD surface a validation warning or approval mapping instead of guessing from prose.").optional(), "description": z.string().describe("Human-readable description of what the slot expects from the buyer.").optional(), "consumed_for_production": z.boolean().describe("Dispatch hint for `build_creative` and v1↔v2 wire translators: when `true`, the slot's value is consumed as INPUT to a production step (host-read script, brief copy fed to generative synthesis, catalog feed driving per-SKU rendering) and is not rendered verbatim. When `false` (default), the slot's value is rendered verbatim on the placement (image bytes, video file, display tag).\n\nMotivates the v1↔v2 dispatch table: pre-v2 buyers shipped production-consumed inputs separately in a `inputs` map on the build_creative request; v2 collapses inputs and rendered assets into a single `assets` map keyed by `asset_group_id`. SDK translators between v1 and v2 use this flag per canonical to know which assets in the v2 manifest map back to v1 `inputs` vs v1 `assets`. Without the per-slot flag the dispatch table lives in adopter code and every SDK gets it slightly different.\n\nProducers SHOULD set this explicitly on slots whose consumption pattern isn't obvious (host-read scripts on `audio_hosted`, briefs on generative `video_hosted`, catalog feeds on `sponsored_placement`). For canonicals where every slot is render-verbatim (`image`, `display_tag`, `video_vast`), the default `false` is sufficient and the flag MAY be omitted.").default(false) }).catchall(z.any()).and(z.intersection(z.intersection(z.any(), z.intersection(z.any(), z.any())), z.intersection(z.any(), z.intersection(z.any(), z.any()))))).describe("Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.").optional(), "required_connections": z.array(z.object({ "provider": z.string().describe("Stable provider or platform namespace, preferably lowercase. Examples: `social.example`, `shortvideo.example`, or a seller-defined namespace. Omit only when the requirement is provider-agnostic, or when an `authorization_url` fully routes the human to the correct provider-specific connection flow.").optional(), "connection_type": z.enum(["advertiser_account","publisher_identity","post_authorization"]).describe("Kind of downstream connection required. `advertiser_account` is the platform account used to buy/manage ads. `publisher_identity` is the creator, page, channel, organization, or profile that owns source posts. `post_authorization` is a post-scoped grant when the platform authorizes individual posts instead of, or in addition to, the owning identity."), "required_for": z.array(z.string().min(1)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Concrete AdCP protocol operation names that require this downstream connection. Sellers SHOULD include this in product declarations when the requirement is known ahead of time, and in AUTHORIZATION_REQUIRED details when it explains the failed operation. Prefer specific operation names such as `list_creatives`, `sync_creatives`, `create_media_buy`, `get_media_buy_delivery`, or `get_creative_delivery` over broad category labels such as `reporting`.").optional(), "scope": z.enum(["account","identity","post","unknown"]).describe("Granularity of the downstream grant.").optional(), "status": z.enum(["connected","missing","pending","expired","revoked","not_required","unknown"]).describe("Current seller-observed state for this downstream connection when known. Product declarations MAY omit status or use `unknown`; AUTHORIZATION_REQUIRED details SHOULD use `missing`, `expired`, or `revoked` for the connection that blocked the call.").optional(), "connection_id": z.string().describe("Seller-defined identifier for an already-created downstream connection. Omit when no connection exists yet or when exposing it would leak platform/account state.").optional(), "resource_ref": z.object({ "platform_account_id": z.string().describe("Provider-native advertiser or business account id, when safe to disclose.").optional(), "identity_id": z.string().describe("Provider-native creator, page, channel, organization, or profile id, when safe to disclose.").optional(), "handle": z.string().describe("Provider-native public handle for the owning identity, when available.").optional(), "profile_url": z.string().url().describe("Public URL for the owning identity, when available.").optional(), "post_id": z.string().describe("Provider-native post id, when the grant is post-scoped or the failed request referenced a specific post.").optional(), "post_url": z.string().url().describe("Public URL for the referenced post, when available.").optional() }).catchall(z.any()).describe("Optional opaque provider-native resource hint, such as a platform account id, profile URL, handle, channel id, post id, or post URL. This is a hint for routing authorization, not proof that authorization exists.").optional(), "authorization_url": z.string().url().describe("Seller-hosted or provider-hosted URL where a human can complete or restore this downstream connection.").optional(), "authorization_instructions": z.string().describe("Human-readable instructions for completing or restoring this downstream connection.").optional(), "expires_at": z.string().datetime().describe("Expiration time for the downstream grant, when known.").optional() }).catchall(z.any()).and(z.any()).describe("A seller/platform-side connection or grant required by a product, format, or request. This is not the AdCP caller credential: the AdCP request is still authenticated once, and the seller uses these stored downstream connections to call a platform or service on the buyer's behalf. Use this shape for platforms that require more than one downstream grant, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.")).describe("Downstream platform connections or grants required to use this format declaration. These are in addition to the single AdCP caller credential. Use this when a platform product requires multiple downstream grants, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.").optional(), "reference_mutability": z.enum(["immutable_snapshot","mutable_requires_reapproval","mutable_auto_recheck"]).describe("Policy for formats whose `slots` accept a `published_post` reference. `immutable_snapshot`: seller snapshots the referenced post at approval and later source changes do not change the served creative. `mutable_requires_reapproval`: the source post may change and material changes require review before continued serving. `mutable_auto_recheck`: the source post may change and the seller continuously or periodically rechecks authorization/policy without requiring buyer resubmission. Omit when the format has no `published_post` slot.").optional(), "production_window_business_days": z.number().int().gte(0).describe("Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative).").optional() }).catchall(z.any()).describe("Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints)."), z.union([z.any().refine((value) => !z.union([z.any(), z.any(), z.any(), z.any(), z.any()]).safeParse(value).success, "Invalid input: Should NOT be valid against schema"), z.any().refine((value) => !z.union([z.any(), z.any(), z.any(), z.any(), z.any(), z.any()]).safeParse(value).success, "Invalid input: Should NOT be valid against schema"), z.union([z.any(), z.any(), z.any(), z.any()]), z.any().refine((value) => !z.union([z.any(), z.any(), z.any(), z.any(), z.any(), z.any(), z.any()]).safeParse(value).success, "Invalid input: Should NOT be valid against schema")]).describe("Exactly one of: (a) fixed (`width` + `height` both set), (b) multi-size (`sizes` set), (c) responsive (any of `min_width`/`max_width`/`min_height`/`max_height` set), (d) none (no size constraint declared — accepts any dimensions). Combining modes is rejected at schema layer.")).describe("Interactive HTML5 banner delivered as a zip archive. Slot: `html5_bundle` (zip asset). Tracking model: MRAID + IAB Open Measurement (OM-SDK) + click-tag macro substitution + backup image fallback. Receivers unpack the zip, validate internal structure, and serve from CDN. Distinct from `image` (static, non-interactive) and `display_tag` (third-party served). The zip's entry point is typically `index.html`; click handling uses `clickTag` (or `clickTAG`) macro substitution.") }), z.object({ "format_kind": z.literal("display_tag"), "params": z.intersection(z.object({ "experimental": z.boolean().describe("When true, this canonical or seller narrowing may not work as declared. Adopters SHOULD preflight it with validate_input or in a sandbox and SHOULD NOT route production budget without testing; experimental status never makes the deprecated v1 path preferable. Drivers include unsettled spec shape, an adopter runtime gap, and custom shapes awaiting promotion. This replaces the earlier status plus runtime_status axes. Sellers SHOULD set experimental whenever a canonical or declaration is not production-ready.").default(false), "deprecated": z.boolean().describe("When true, this canonical (or a seller's specific narrowing of it) is going away. Existing adopters are supported through the deprecation cycle; new adoption is discouraged. Pair with `migration_target_version` to indicate when the canonical is expected to be removed. Distinct from `experimental`: an experimental canonical may stabilize and stop being experimental; a deprecated canonical is on a sunset path.").default(false), "v1_translatable": z.boolean().describe("Whether this canonical has any v1 named-format equivalent. `true` (default) — the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1→v2 projection via `v1-canonical-mapping.json` is meaningful. `false` — the canonical is inherently new in v2 and has no v1 form; v1's `list_creative_formats` couldn't express it because the underlying concept (algorithmic surface composition, AI-surface mentions, retail-media catalog placements, multi-card carousels) didn't exist as a v1 named-format archetype.\n\nLets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The six inherently-v2 canonicals in 3.2 are `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`, `seller_rendered_stateful_display`, and `coordinated_placements`.").default(true), "since_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version that introduced this canonical (e.g., '3.1', '3.2'). Lets adopters reason about minimum protocol version requirements when consuming a format declaration. Patch precision is intentionally rejected — canonicals are introduced at minor-version boundaries.").optional(), "migration_target_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version by which the working group expects this canonical to stabilize, surface a breaking revision, or (when `deprecated: true`) be removed. Patch precision is intentionally rejected — canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown').").optional(), "composition_model": z.enum(["deterministic","algorithmic"]).describe("Whether the surface composes deterministically (buyer can predict per-slot rendering — sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing — responsive_creative, agent_placement).").optional(), "provenance_required": z.boolean().describe("When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent.").optional(), "platform_extensions": z.array(z.object({ "uri": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL identifying the extension. `https://` is mandatory — `http://`, `file://`, `data:`, and other schemes are rejected at the schema layer (defense-in-depth on top of the fetch-contract normative rules). The URI base is the owning agent's URL; the path identifies the extension within that agent. Example: 'https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel'. The full fetch contract — SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds — is documented on `product-format-declaration.json#format_schema` and applies to ALL fetches of this reference shape regardless of whether the field is named `format_schema` (load-bearing for validation) or `platform_extensions` (informational); the *transport* rules are identical, only the *consumption* semantics differ."), "digest": z.string().regex(new RegExp("^sha256:[a-f0-9]{64}$")).describe("SHA-256 content digest of the extension definition (sha256:). Used to detect drift — if the agent revises the extension, the digest changes and cached definitions become invalid.") }).catchall(z.any()).describe("Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` — divergent digests in one response indicate producer-side error (e.g., concurrent extension revision mid-render). Buyers encountering divergent digests for the same URI MUST fail closed: treat all references to that URI as unresolved and surface a validation error rather than picking one branch silently. **Across responses**, digest divergence is normal — extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.")).describe("Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\n\n**Collision precedence (normative).** When two or more `platform_extensions[]` entries on the same declaration extend the same target (e.g., both extend `tracking`) with overlapping field names, **array order is authoritative — later entries override earlier ones on a per-field basis** (last-in-array-wins). SDKs MUST surface the overlap via the `errors[]` array on the `get_products` response with a structured code (`FORMAT_DECLARATION_DIVERGENT` is appropriate when the overlap appears across dual-emitted shapes; a producer-self-emitted overlap on a single declaration SHOULD use the same code with `error.details: { collision_kind: \"platform_extension_field\", target, overlapping_fields, winning_extension_uri }`). Producers SHOULD avoid the collision by emitting one extension per target or by partitioning fields across extensions; the deterministic precedence is for last-resort consistency across SDK implementations, not a sanctioned merging strategy.").optional(), "synthesis_nondeterministic": z.boolean().describe("When true, the format's production pipeline is genuinely nondeterministic — the platform cannot guarantee that synthesis from a given input set produces in-spec output. Veo / Sora / Runway-class generative video, and other AI-synthesis flows where output dimensions, duration, or quality vary per run. Implies a different validation contract: predictive `validate_input` is impossible; the platform's own post-synthesis QA loop applies; if the QA loop exhausts without producing a valid artifact, `build_creative` returns task_failed with a synthesis_failed reason. Distinct from `composition_model` (which describes how the surface composes per-slot rendering, not whether synthesis is deterministic). When false or absent, the format's production is predictable enough that `validate_input` can predict output properties from input properties.\n\n**Compatibility with `asset_source` / `item_production_model`**: `synthesis_nondeterministic: true` MAY pair with any of `seller_pre_rendered_from_brief`, `seller_human_designed`, or `agent_synthesized` (the QA loop is concept-level, not source-specific — 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.").default(false), "slots": z.array(z.object({ "asset_group_id": z.string().describe("Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings."), "asset_type": z.enum(["image","video","audio","text","markdown","url","html","css","javascript","vast","daast","webhook","brief","catalog","published_post","zip","card","object","pixel_tracker","vast_tracker","daast_tracker"]).describe("Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `published_post` is an existing-post reference asset, not uploaded media bytes and not a catalog row. `card` is the multi-card carousel element type (see card-asset.json). `pixel_tracker` / `vast_tracker` / `daast_tracker` are the renderer-fired measurement-tracker primitives — see `/schemas/core/assets/pixel-tracker-asset.json` and the VAST / DAAST tracker schemas. `object` is a last-resort fallback for structured non-asset inputs that don't fit any primitive asset_type — prefer specific types whenever possible."), "required": z.boolean().describe("Whether this slot is required for a valid manifest.").default(false), "min": z.number().int().gte(0).describe("Minimum count for repeatable / pool slots.").optional(), "max": z.number().int().gte(1).describe("Maximum count for repeatable / pool slots.").optional(), "max_chars": z.number().int().gte(1).describe("Per-slot character limit. Valid only when `asset_type` is `text`, `markdown`, or `brief`. Mutually exclusive with `max_size_kb` (which applies to binary asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "max_size_kb": z.number().int().gte(1).describe("Per-slot file size limit in kilobytes. Valid only when `asset_type` is `image`, `video`, `audio`, or `zip`. Mutually exclusive with `max_chars` (which applies to text asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Accepted intrinsic-pixel densities for this image-bearing slot. Valid when `asset_type` is `image`, and on a `card` slot where it constrains each card's image media (video media is unaffected). This makes density available to every canonical carrying image assets (native, carousel, responsive, companion images, and image itself), not only `format_kind: image`. When the image canonical also declares top-level `params.pixel_ratios`, the effective set is the intersection; an empty intersection is invalid. One matching asset satisfies the slot unless `required_pixel_ratios` requires rendition coverage.").optional(), "required_pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Required density coverage for an image rendition set. Valid only when `asset_type` is `image` and `pixel_ratios` is also declared. Every value MUST appear in the effective accepted set after intersecting any top-level image `params.pixel_ratios`, and the manifest slot value MUST be an array containing exactly one matching image rendition for each required ratio. Other accepted ratios remain optional. For example, `pixel_ratios: [1, 1.5, 2]` with `required_pixel_ratios: [1, 2]` requires the 1x and 2x renditions while making 1.5x optional. SDKs enforce intersection, subset, coverage, and duplicate-ratio rules because JSON Schema draft-07 cannot express them generically.").optional(), "logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("When `asset_group_id` is `logo`, renderer-facing brand.json logo slots acceptable for this format slot. Producers selecting from brand.json SHOULD prefer `logos[]` entries whose `slots[]` intersects this list, then apply `visual_guidelines.logo_usage_rules[]`.").optional(), "required_logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Subset of `logo_slots` for which this format expects explicit logo coverage. A manifest or brand-derived logo pool SHOULD include at least one usable logo for each required slot; if coverage is missing, builders SHOULD surface a validation warning or approval mapping instead of guessing from prose.").optional(), "description": z.string().describe("Human-readable description of what the slot expects from the buyer.").optional(), "consumed_for_production": z.boolean().describe("Dispatch hint for `build_creative` and v1↔v2 wire translators: when `true`, the slot's value is consumed as INPUT to a production step (host-read script, brief copy fed to generative synthesis, catalog feed driving per-SKU rendering) and is not rendered verbatim. When `false` (default), the slot's value is rendered verbatim on the placement (image bytes, video file, display tag).\n\nMotivates the v1↔v2 dispatch table: pre-v2 buyers shipped production-consumed inputs separately in a `inputs` map on the build_creative request; v2 collapses inputs and rendered assets into a single `assets` map keyed by `asset_group_id`. SDK translators between v1 and v2 use this flag per canonical to know which assets in the v2 manifest map back to v1 `inputs` vs v1 `assets`. Without the per-slot flag the dispatch table lives in adopter code and every SDK gets it slightly different.\n\nProducers SHOULD set this explicitly on slots whose consumption pattern isn't obvious (host-read scripts on `audio_hosted`, briefs on generative `video_hosted`, catalog feeds on `sponsored_placement`). For canonicals where every slot is render-verbatim (`image`, `display_tag`, `video_vast`), the default `false` is sufficient and the flag MAY be omitted.").default(false) }).catchall(z.any()).and(z.intersection(z.intersection(z.any(), z.intersection(z.any(), z.any())), z.intersection(z.any(), z.intersection(z.any(), z.any()))))).describe("Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.").optional(), "required_connections": z.array(z.object({ "provider": z.string().describe("Stable provider or platform namespace, preferably lowercase. Examples: `social.example`, `shortvideo.example`, or a seller-defined namespace. Omit only when the requirement is provider-agnostic, or when an `authorization_url` fully routes the human to the correct provider-specific connection flow.").optional(), "connection_type": z.enum(["advertiser_account","publisher_identity","post_authorization"]).describe("Kind of downstream connection required. `advertiser_account` is the platform account used to buy/manage ads. `publisher_identity` is the creator, page, channel, organization, or profile that owns source posts. `post_authorization` is a post-scoped grant when the platform authorizes individual posts instead of, or in addition to, the owning identity."), "required_for": z.array(z.string().min(1)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Concrete AdCP protocol operation names that require this downstream connection. Sellers SHOULD include this in product declarations when the requirement is known ahead of time, and in AUTHORIZATION_REQUIRED details when it explains the failed operation. Prefer specific operation names such as `list_creatives`, `sync_creatives`, `create_media_buy`, `get_media_buy_delivery`, or `get_creative_delivery` over broad category labels such as `reporting`.").optional(), "scope": z.enum(["account","identity","post","unknown"]).describe("Granularity of the downstream grant.").optional(), "status": z.enum(["connected","missing","pending","expired","revoked","not_required","unknown"]).describe("Current seller-observed state for this downstream connection when known. Product declarations MAY omit status or use `unknown`; AUTHORIZATION_REQUIRED details SHOULD use `missing`, `expired`, or `revoked` for the connection that blocked the call.").optional(), "connection_id": z.string().describe("Seller-defined identifier for an already-created downstream connection. Omit when no connection exists yet or when exposing it would leak platform/account state.").optional(), "resource_ref": z.object({ "platform_account_id": z.string().describe("Provider-native advertiser or business account id, when safe to disclose.").optional(), "identity_id": z.string().describe("Provider-native creator, page, channel, organization, or profile id, when safe to disclose.").optional(), "handle": z.string().describe("Provider-native public handle for the owning identity, when available.").optional(), "profile_url": z.string().url().describe("Public URL for the owning identity, when available.").optional(), "post_id": z.string().describe("Provider-native post id, when the grant is post-scoped or the failed request referenced a specific post.").optional(), "post_url": z.string().url().describe("Public URL for the referenced post, when available.").optional() }).catchall(z.any()).describe("Optional opaque provider-native resource hint, such as a platform account id, profile URL, handle, channel id, post id, or post URL. This is a hint for routing authorization, not proof that authorization exists.").optional(), "authorization_url": z.string().url().describe("Seller-hosted or provider-hosted URL where a human can complete or restore this downstream connection.").optional(), "authorization_instructions": z.string().describe("Human-readable instructions for completing or restoring this downstream connection.").optional(), "expires_at": z.string().datetime().describe("Expiration time for the downstream grant, when known.").optional() }).catchall(z.any()).and(z.any()).describe("A seller/platform-side connection or grant required by a product, format, or request. This is not the AdCP caller credential: the AdCP request is still authenticated once, and the seller uses these stored downstream connections to call a platform or service on the buyer's behalf. Use this shape for platforms that require more than one downstream grant, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.")).describe("Downstream platform connections or grants required to use this format declaration. These are in addition to the single AdCP caller credential. Use this when a platform product requires multiple downstream grants, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.").optional(), "reference_mutability": z.enum(["immutable_snapshot","mutable_requires_reapproval","mutable_auto_recheck"]).describe("Policy for formats whose `slots` accept a `published_post` reference. `immutable_snapshot`: seller snapshots the referenced post at approval and later source changes do not change the served creative. `mutable_requires_reapproval`: the source post may change and material changes require review before continued serving. `mutable_auto_recheck`: the source post may change and the seller continuously or periodically rechecks authorization/policy without requiring buyer resubmission. Omit when the format has no `published_post` slot.").optional(), "production_window_business_days": z.number().int().gte(0).describe("Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative).").optional() }).catchall(z.any()).describe("Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints)."), z.union([z.any().refine((value) => !z.union([z.any(), z.any(), z.any(), z.any(), z.any()]).safeParse(value).success, "Invalid input: Should NOT be valid against schema"), z.any().refine((value) => !z.union([z.any(), z.any(), z.any(), z.any(), z.any(), z.any()]).safeParse(value).success, "Invalid input: Should NOT be valid against schema"), z.union([z.any(), z.any(), z.any(), z.any()]), z.any().refine((value) => !z.union([z.any(), z.any(), z.any(), z.any(), z.any(), z.any(), z.any()]).safeParse(value).success, "Invalid input: Should NOT be valid against schema")]).describe("Exactly one of: (a) fixed (`width` + `height` both set), (b) multi-size (`sizes` set), (c) responsive (any of `min_width`/`max_width`/`min_height`/`max_height` set), (d) none (no size constraint declared — accepts any dimensions). Combining modes is rejected at schema layer.")).describe("Third-party-served display tag (JS, iframe, or 1×1 redirect). The buyer's adserver hosts the creative; the seller calls the tag URL at impression time. Slot: `tag_url` (url asset with appropriate `url_type`). Tracking model: opaque to seller — third party serves and measures. Click tracking via redirect URL substitution using universal_macros. Distinct from `image` (static asset hosted by seller) and `html5` (zip bundle hosted by seller).") }), z.object({ "format_kind": z.literal("image_carousel"), "params": z.object({ "experimental": z.boolean().describe("When true, this canonical or seller narrowing may not work as declared. Adopters SHOULD preflight it with validate_input or in a sandbox and SHOULD NOT route production budget without testing; experimental status never makes the deprecated v1 path preferable. Drivers include unsettled spec shape, an adopter runtime gap, and custom shapes awaiting promotion. This replaces the earlier status plus runtime_status axes. Sellers SHOULD set experimental whenever a canonical or declaration is not production-ready.").default(false), "deprecated": z.boolean().describe("When true, this canonical (or a seller's specific narrowing of it) is going away. Existing adopters are supported through the deprecation cycle; new adoption is discouraged. Pair with `migration_target_version` to indicate when the canonical is expected to be removed. Distinct from `experimental`: an experimental canonical may stabilize and stop being experimental; a deprecated canonical is on a sunset path.").default(false), "v1_translatable": z.boolean().describe("Whether this canonical has any v1 named-format equivalent. `true` (default) — the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1→v2 projection via `v1-canonical-mapping.json` is meaningful. `false` — the canonical is inherently new in v2 and has no v1 form; v1's `list_creative_formats` couldn't express it because the underlying concept (algorithmic surface composition, AI-surface mentions, retail-media catalog placements, multi-card carousels) didn't exist as a v1 named-format archetype.\n\nLets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The six inherently-v2 canonicals in 3.2 are `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`, `seller_rendered_stateful_display`, and `coordinated_placements`.").default(true), "since_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version that introduced this canonical (e.g., '3.1', '3.2'). Lets adopters reason about minimum protocol version requirements when consuming a format declaration. Patch precision is intentionally rejected — canonicals are introduced at minor-version boundaries.").optional(), "migration_target_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version by which the working group expects this canonical to stabilize, surface a breaking revision, or (when `deprecated: true`) be removed. Patch precision is intentionally rejected — canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown').").optional(), "composition_model": z.enum(["deterministic","algorithmic"]).describe("Whether the surface composes deterministically (buyer can predict per-slot rendering — sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing — responsive_creative, agent_placement).").optional(), "provenance_required": z.boolean().describe("When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent.").optional(), "platform_extensions": z.array(z.object({ "uri": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL identifying the extension. `https://` is mandatory — `http://`, `file://`, `data:`, and other schemes are rejected at the schema layer (defense-in-depth on top of the fetch-contract normative rules). The URI base is the owning agent's URL; the path identifies the extension within that agent. Example: 'https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel'. The full fetch contract — SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds — is documented on `product-format-declaration.json#format_schema` and applies to ALL fetches of this reference shape regardless of whether the field is named `format_schema` (load-bearing for validation) or `platform_extensions` (informational); the *transport* rules are identical, only the *consumption* semantics differ."), "digest": z.string().regex(new RegExp("^sha256:[a-f0-9]{64}$")).describe("SHA-256 content digest of the extension definition (sha256:). Used to detect drift — if the agent revises the extension, the digest changes and cached definitions become invalid.") }).catchall(z.any()).describe("Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` — divergent digests in one response indicate producer-side error (e.g., concurrent extension revision mid-render). Buyers encountering divergent digests for the same URI MUST fail closed: treat all references to that URI as unresolved and surface a validation error rather than picking one branch silently. **Across responses**, digest divergence is normal — extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.")).describe("Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\n\n**Collision precedence (normative).** When two or more `platform_extensions[]` entries on the same declaration extend the same target (e.g., both extend `tracking`) with overlapping field names, **array order is authoritative — later entries override earlier ones on a per-field basis** (last-in-array-wins). SDKs MUST surface the overlap via the `errors[]` array on the `get_products` response with a structured code (`FORMAT_DECLARATION_DIVERGENT` is appropriate when the overlap appears across dual-emitted shapes; a producer-self-emitted overlap on a single declaration SHOULD use the same code with `error.details: { collision_kind: \"platform_extension_field\", target, overlapping_fields, winning_extension_uri }`). Producers SHOULD avoid the collision by emitting one extension per target or by partitioning fields across extensions; the deterministic precedence is for last-resort consistency across SDK implementations, not a sanctioned merging strategy.").optional(), "synthesis_nondeterministic": z.boolean().describe("When true, the format's production pipeline is genuinely nondeterministic — the platform cannot guarantee that synthesis from a given input set produces in-spec output. Veo / Sora / Runway-class generative video, and other AI-synthesis flows where output dimensions, duration, or quality vary per run. Implies a different validation contract: predictive `validate_input` is impossible; the platform's own post-synthesis QA loop applies; if the QA loop exhausts without producing a valid artifact, `build_creative` returns task_failed with a synthesis_failed reason. Distinct from `composition_model` (which describes how the surface composes per-slot rendering, not whether synthesis is deterministic). When false or absent, the format's production is predictable enough that `validate_input` can predict output properties from input properties.\n\n**Compatibility with `asset_source` / `item_production_model`**: `synthesis_nondeterministic: true` MAY pair with any of `seller_pre_rendered_from_brief`, `seller_human_designed`, or `agent_synthesized` (the QA loop is concept-level, not source-specific — 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.").default(false), "slots": z.array(z.object({ "asset_group_id": z.string().describe("Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings."), "asset_type": z.enum(["image","video","audio","text","markdown","url","html","css","javascript","vast","daast","webhook","brief","catalog","published_post","zip","card","object","pixel_tracker","vast_tracker","daast_tracker"]).describe("Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `published_post` is an existing-post reference asset, not uploaded media bytes and not a catalog row. `card` is the multi-card carousel element type (see card-asset.json). `pixel_tracker` / `vast_tracker` / `daast_tracker` are the renderer-fired measurement-tracker primitives — see `/schemas/core/assets/pixel-tracker-asset.json` and the VAST / DAAST tracker schemas. `object` is a last-resort fallback for structured non-asset inputs that don't fit any primitive asset_type — prefer specific types whenever possible."), "required": z.boolean().describe("Whether this slot is required for a valid manifest.").default(false), "min": z.number().int().gte(0).describe("Minimum count for repeatable / pool slots.").optional(), "max": z.number().int().gte(1).describe("Maximum count for repeatable / pool slots.").optional(), "max_chars": z.number().int().gte(1).describe("Per-slot character limit. Valid only when `asset_type` is `text`, `markdown`, or `brief`. Mutually exclusive with `max_size_kb` (which applies to binary asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "max_size_kb": z.number().int().gte(1).describe("Per-slot file size limit in kilobytes. Valid only when `asset_type` is `image`, `video`, `audio`, or `zip`. Mutually exclusive with `max_chars` (which applies to text asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Accepted intrinsic-pixel densities for this image-bearing slot. Valid when `asset_type` is `image`, and on a `card` slot where it constrains each card's image media (video media is unaffected). This makes density available to every canonical carrying image assets (native, carousel, responsive, companion images, and image itself), not only `format_kind: image`. When the image canonical also declares top-level `params.pixel_ratios`, the effective set is the intersection; an empty intersection is invalid. One matching asset satisfies the slot unless `required_pixel_ratios` requires rendition coverage.").optional(), "required_pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Required density coverage for an image rendition set. Valid only when `asset_type` is `image` and `pixel_ratios` is also declared. Every value MUST appear in the effective accepted set after intersecting any top-level image `params.pixel_ratios`, and the manifest slot value MUST be an array containing exactly one matching image rendition for each required ratio. Other accepted ratios remain optional. For example, `pixel_ratios: [1, 1.5, 2]` with `required_pixel_ratios: [1, 2]` requires the 1x and 2x renditions while making 1.5x optional. SDKs enforce intersection, subset, coverage, and duplicate-ratio rules because JSON Schema draft-07 cannot express them generically.").optional(), "logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("When `asset_group_id` is `logo`, renderer-facing brand.json logo slots acceptable for this format slot. Producers selecting from brand.json SHOULD prefer `logos[]` entries whose `slots[]` intersects this list, then apply `visual_guidelines.logo_usage_rules[]`.").optional(), "required_logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Subset of `logo_slots` for which this format expects explicit logo coverage. A manifest or brand-derived logo pool SHOULD include at least one usable logo for each required slot; if coverage is missing, builders SHOULD surface a validation warning or approval mapping instead of guessing from prose.").optional(), "description": z.string().describe("Human-readable description of what the slot expects from the buyer.").optional(), "consumed_for_production": z.boolean().describe("Dispatch hint for `build_creative` and v1↔v2 wire translators: when `true`, the slot's value is consumed as INPUT to a production step (host-read script, brief copy fed to generative synthesis, catalog feed driving per-SKU rendering) and is not rendered verbatim. When `false` (default), the slot's value is rendered verbatim on the placement (image bytes, video file, display tag).\n\nMotivates the v1↔v2 dispatch table: pre-v2 buyers shipped production-consumed inputs separately in a `inputs` map on the build_creative request; v2 collapses inputs and rendered assets into a single `assets` map keyed by `asset_group_id`. SDK translators between v1 and v2 use this flag per canonical to know which assets in the v2 manifest map back to v1 `inputs` vs v1 `assets`. Without the per-slot flag the dispatch table lives in adopter code and every SDK gets it slightly different.\n\nProducers SHOULD set this explicitly on slots whose consumption pattern isn't obvious (host-read scripts on `audio_hosted`, briefs on generative `video_hosted`, catalog feeds on `sponsored_placement`). For canonicals where every slot is render-verbatim (`image`, `display_tag`, `video_vast`), the default `false` is sufficient and the flag MAY be omitted.").default(false) }).catchall(z.any()).and(z.intersection(z.intersection(z.any(), z.intersection(z.any(), z.any())), z.intersection(z.any(), z.intersection(z.any(), z.any()))))).describe("Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.").optional(), "required_connections": z.array(z.object({ "provider": z.string().describe("Stable provider or platform namespace, preferably lowercase. Examples: `social.example`, `shortvideo.example`, or a seller-defined namespace. Omit only when the requirement is provider-agnostic, or when an `authorization_url` fully routes the human to the correct provider-specific connection flow.").optional(), "connection_type": z.enum(["advertiser_account","publisher_identity","post_authorization"]).describe("Kind of downstream connection required. `advertiser_account` is the platform account used to buy/manage ads. `publisher_identity` is the creator, page, channel, organization, or profile that owns source posts. `post_authorization` is a post-scoped grant when the platform authorizes individual posts instead of, or in addition to, the owning identity."), "required_for": z.array(z.string().min(1)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Concrete AdCP protocol operation names that require this downstream connection. Sellers SHOULD include this in product declarations when the requirement is known ahead of time, and in AUTHORIZATION_REQUIRED details when it explains the failed operation. Prefer specific operation names such as `list_creatives`, `sync_creatives`, `create_media_buy`, `get_media_buy_delivery`, or `get_creative_delivery` over broad category labels such as `reporting`.").optional(), "scope": z.enum(["account","identity","post","unknown"]).describe("Granularity of the downstream grant.").optional(), "status": z.enum(["connected","missing","pending","expired","revoked","not_required","unknown"]).describe("Current seller-observed state for this downstream connection when known. Product declarations MAY omit status or use `unknown`; AUTHORIZATION_REQUIRED details SHOULD use `missing`, `expired`, or `revoked` for the connection that blocked the call.").optional(), "connection_id": z.string().describe("Seller-defined identifier for an already-created downstream connection. Omit when no connection exists yet or when exposing it would leak platform/account state.").optional(), "resource_ref": z.object({ "platform_account_id": z.string().describe("Provider-native advertiser or business account id, when safe to disclose.").optional(), "identity_id": z.string().describe("Provider-native creator, page, channel, organization, or profile id, when safe to disclose.").optional(), "handle": z.string().describe("Provider-native public handle for the owning identity, when available.").optional(), "profile_url": z.string().url().describe("Public URL for the owning identity, when available.").optional(), "post_id": z.string().describe("Provider-native post id, when the grant is post-scoped or the failed request referenced a specific post.").optional(), "post_url": z.string().url().describe("Public URL for the referenced post, when available.").optional() }).catchall(z.any()).describe("Optional opaque provider-native resource hint, such as a platform account id, profile URL, handle, channel id, post id, or post URL. This is a hint for routing authorization, not proof that authorization exists.").optional(), "authorization_url": z.string().url().describe("Seller-hosted or provider-hosted URL where a human can complete or restore this downstream connection.").optional(), "authorization_instructions": z.string().describe("Human-readable instructions for completing or restoring this downstream connection.").optional(), "expires_at": z.string().datetime().describe("Expiration time for the downstream grant, when known.").optional() }).catchall(z.any()).and(z.any()).describe("A seller/platform-side connection or grant required by a product, format, or request. This is not the AdCP caller credential: the AdCP request is still authenticated once, and the seller uses these stored downstream connections to call a platform or service on the buyer's behalf. Use this shape for platforms that require more than one downstream grant, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.")).describe("Downstream platform connections or grants required to use this format declaration. These are in addition to the single AdCP caller credential. Use this when a platform product requires multiple downstream grants, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.").optional(), "reference_mutability": z.enum(["immutable_snapshot","mutable_requires_reapproval","mutable_auto_recheck"]).describe("Policy for formats whose `slots` accept a `published_post` reference. `immutable_snapshot`: seller snapshots the referenced post at approval and later source changes do not change the served creative. `mutable_requires_reapproval`: the source post may change and material changes require review before continued serving. `mutable_auto_recheck`: the source post may change and the seller continuously or periodically rechecks authorization/policy without requiring buyer resubmission. Omit when the format has no `published_post` slot.").optional(), "production_window_business_days": z.number().int().gte(0).describe("Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative).").optional() }).catchall(z.any()).describe("Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).").describe("Multi-card swipeable carousel. The buyer ships a `cards` slot whose value is an **array** of [card-asset](/schemas/core/assets/card-asset.json) objects (a single key with an array value — NOT one key per card, NOT dotted/bracketed paths). Each card-asset carries: `asset_type: \"card\"`, `media` (an image or video asset), optional `headline` (text), optional `landing_page_url` (url asset). Per-card structure is the same across all cards; mixed orientations not allowed within a single carousel. Tracking model: per-card impression and engagement pixels + carousel-level engagement (swipe, view-time). Allowed asset types for a card's `media` field: `image` and `video` (Meta-style mixed-media); platforms can narrow to image-only or video-only via `allowed_card_media_asset_types`.\n\nThe manifest's `assets.cards` value is an array of card-asset objects. Example: `\"cards\": [{\"asset_type\": \"card\", \"media\": {\"asset_type\": \"image\", \"url\": \"...\"}, \"headline\": \"Buy now\", \"landing_page_url\": {\"asset_type\": \"url\", \"url_type\": \"clickthrough\", \"url\": \"...\"}}, ...]`. Each card-asset validates against the card schema; per-card platform extensions attach via the card's `platform_extensions` field, never via inline non-canonical keys.") }), z.object({ "format_kind": z.literal("video_hosted"), "params": z.intersection(z.object({ "experimental": z.boolean().describe("When true, this canonical or seller narrowing may not work as declared. Adopters SHOULD preflight it with validate_input or in a sandbox and SHOULD NOT route production budget without testing; experimental status never makes the deprecated v1 path preferable. Drivers include unsettled spec shape, an adopter runtime gap, and custom shapes awaiting promotion. This replaces the earlier status plus runtime_status axes. Sellers SHOULD set experimental whenever a canonical or declaration is not production-ready.").default(false), "deprecated": z.boolean().describe("When true, this canonical (or a seller's specific narrowing of it) is going away. Existing adopters are supported through the deprecation cycle; new adoption is discouraged. Pair with `migration_target_version` to indicate when the canonical is expected to be removed. Distinct from `experimental`: an experimental canonical may stabilize and stop being experimental; a deprecated canonical is on a sunset path.").default(false), "v1_translatable": z.boolean().describe("Whether this canonical has any v1 named-format equivalent. `true` (default) — the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1→v2 projection via `v1-canonical-mapping.json` is meaningful. `false` — the canonical is inherently new in v2 and has no v1 form; v1's `list_creative_formats` couldn't express it because the underlying concept (algorithmic surface composition, AI-surface mentions, retail-media catalog placements, multi-card carousels) didn't exist as a v1 named-format archetype.\n\nLets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The six inherently-v2 canonicals in 3.2 are `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`, `seller_rendered_stateful_display`, and `coordinated_placements`.").default(true), "since_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version that introduced this canonical (e.g., '3.1', '3.2'). Lets adopters reason about minimum protocol version requirements when consuming a format declaration. Patch precision is intentionally rejected — canonicals are introduced at minor-version boundaries.").optional(), "migration_target_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version by which the working group expects this canonical to stabilize, surface a breaking revision, or (when `deprecated: true`) be removed. Patch precision is intentionally rejected — canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown').").optional(), "composition_model": z.enum(["deterministic","algorithmic"]).describe("Whether the surface composes deterministically (buyer can predict per-slot rendering — sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing — responsive_creative, agent_placement).").optional(), "provenance_required": z.boolean().describe("When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent.").optional(), "platform_extensions": z.array(z.object({ "uri": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL identifying the extension. `https://` is mandatory — `http://`, `file://`, `data:`, and other schemes are rejected at the schema layer (defense-in-depth on top of the fetch-contract normative rules). The URI base is the owning agent's URL; the path identifies the extension within that agent. Example: 'https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel'. The full fetch contract — SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds — is documented on `product-format-declaration.json#format_schema` and applies to ALL fetches of this reference shape regardless of whether the field is named `format_schema` (load-bearing for validation) or `platform_extensions` (informational); the *transport* rules are identical, only the *consumption* semantics differ."), "digest": z.string().regex(new RegExp("^sha256:[a-f0-9]{64}$")).describe("SHA-256 content digest of the extension definition (sha256:). Used to detect drift — if the agent revises the extension, the digest changes and cached definitions become invalid.") }).catchall(z.any()).describe("Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` — divergent digests in one response indicate producer-side error (e.g., concurrent extension revision mid-render). Buyers encountering divergent digests for the same URI MUST fail closed: treat all references to that URI as unresolved and surface a validation error rather than picking one branch silently. **Across responses**, digest divergence is normal — extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.")).describe("Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\n\n**Collision precedence (normative).** When two or more `platform_extensions[]` entries on the same declaration extend the same target (e.g., both extend `tracking`) with overlapping field names, **array order is authoritative — later entries override earlier ones on a per-field basis** (last-in-array-wins). SDKs MUST surface the overlap via the `errors[]` array on the `get_products` response with a structured code (`FORMAT_DECLARATION_DIVERGENT` is appropriate when the overlap appears across dual-emitted shapes; a producer-self-emitted overlap on a single declaration SHOULD use the same code with `error.details: { collision_kind: \"platform_extension_field\", target, overlapping_fields, winning_extension_uri }`). Producers SHOULD avoid the collision by emitting one extension per target or by partitioning fields across extensions; the deterministic precedence is for last-resort consistency across SDK implementations, not a sanctioned merging strategy.").optional(), "synthesis_nondeterministic": z.boolean().describe("When true, the format's production pipeline is genuinely nondeterministic — the platform cannot guarantee that synthesis from a given input set produces in-spec output. Veo / Sora / Runway-class generative video, and other AI-synthesis flows where output dimensions, duration, or quality vary per run. Implies a different validation contract: predictive `validate_input` is impossible; the platform's own post-synthesis QA loop applies; if the QA loop exhausts without producing a valid artifact, `build_creative` returns task_failed with a synthesis_failed reason. Distinct from `composition_model` (which describes how the surface composes per-slot rendering, not whether synthesis is deterministic). When false or absent, the format's production is predictable enough that `validate_input` can predict output properties from input properties.\n\n**Compatibility with `asset_source` / `item_production_model`**: `synthesis_nondeterministic: true` MAY pair with any of `seller_pre_rendered_from_brief`, `seller_human_designed`, or `agent_synthesized` (the QA loop is concept-level, not source-specific — 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.").default(false), "slots": z.array(z.object({ "asset_group_id": z.string().describe("Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings."), "asset_type": z.enum(["image","video","audio","text","markdown","url","html","css","javascript","vast","daast","webhook","brief","catalog","published_post","zip","card","object","pixel_tracker","vast_tracker","daast_tracker"]).describe("Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `published_post` is an existing-post reference asset, not uploaded media bytes and not a catalog row. `card` is the multi-card carousel element type (see card-asset.json). `pixel_tracker` / `vast_tracker` / `daast_tracker` are the renderer-fired measurement-tracker primitives — see `/schemas/core/assets/pixel-tracker-asset.json` and the VAST / DAAST tracker schemas. `object` is a last-resort fallback for structured non-asset inputs that don't fit any primitive asset_type — prefer specific types whenever possible."), "required": z.boolean().describe("Whether this slot is required for a valid manifest.").default(false), "min": z.number().int().gte(0).describe("Minimum count for repeatable / pool slots.").optional(), "max": z.number().int().gte(1).describe("Maximum count for repeatable / pool slots.").optional(), "max_chars": z.number().int().gte(1).describe("Per-slot character limit. Valid only when `asset_type` is `text`, `markdown`, or `brief`. Mutually exclusive with `max_size_kb` (which applies to binary asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "max_size_kb": z.number().int().gte(1).describe("Per-slot file size limit in kilobytes. Valid only when `asset_type` is `image`, `video`, `audio`, or `zip`. Mutually exclusive with `max_chars` (which applies to text asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Accepted intrinsic-pixel densities for this image-bearing slot. Valid when `asset_type` is `image`, and on a `card` slot where it constrains each card's image media (video media is unaffected). This makes density available to every canonical carrying image assets (native, carousel, responsive, companion images, and image itself), not only `format_kind: image`. When the image canonical also declares top-level `params.pixel_ratios`, the effective set is the intersection; an empty intersection is invalid. One matching asset satisfies the slot unless `required_pixel_ratios` requires rendition coverage.").optional(), "required_pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Required density coverage for an image rendition set. Valid only when `asset_type` is `image` and `pixel_ratios` is also declared. Every value MUST appear in the effective accepted set after intersecting any top-level image `params.pixel_ratios`, and the manifest slot value MUST be an array containing exactly one matching image rendition for each required ratio. Other accepted ratios remain optional. For example, `pixel_ratios: [1, 1.5, 2]` with `required_pixel_ratios: [1, 2]` requires the 1x and 2x renditions while making 1.5x optional. SDKs enforce intersection, subset, coverage, and duplicate-ratio rules because JSON Schema draft-07 cannot express them generically.").optional(), "logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("When `asset_group_id` is `logo`, renderer-facing brand.json logo slots acceptable for this format slot. Producers selecting from brand.json SHOULD prefer `logos[]` entries whose `slots[]` intersects this list, then apply `visual_guidelines.logo_usage_rules[]`.").optional(), "required_logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Subset of `logo_slots` for which this format expects explicit logo coverage. A manifest or brand-derived logo pool SHOULD include at least one usable logo for each required slot; if coverage is missing, builders SHOULD surface a validation warning or approval mapping instead of guessing from prose.").optional(), "description": z.string().describe("Human-readable description of what the slot expects from the buyer.").optional(), "consumed_for_production": z.boolean().describe("Dispatch hint for `build_creative` and v1↔v2 wire translators: when `true`, the slot's value is consumed as INPUT to a production step (host-read script, brief copy fed to generative synthesis, catalog feed driving per-SKU rendering) and is not rendered verbatim. When `false` (default), the slot's value is rendered verbatim on the placement (image bytes, video file, display tag).\n\nMotivates the v1↔v2 dispatch table: pre-v2 buyers shipped production-consumed inputs separately in a `inputs` map on the build_creative request; v2 collapses inputs and rendered assets into a single `assets` map keyed by `asset_group_id`. SDK translators between v1 and v2 use this flag per canonical to know which assets in the v2 manifest map back to v1 `inputs` vs v1 `assets`. Without the per-slot flag the dispatch table lives in adopter code and every SDK gets it slightly different.\n\nProducers SHOULD set this explicitly on slots whose consumption pattern isn't obvious (host-read scripts on `audio_hosted`, briefs on generative `video_hosted`, catalog feeds on `sponsored_placement`). For canonicals where every slot is render-verbatim (`image`, `display_tag`, `video_vast`), the default `false` is sufficient and the flag MAY be omitted.").default(false) }).catchall(z.any()).and(z.intersection(z.intersection(z.any(), z.intersection(z.any(), z.any())), z.intersection(z.any(), z.intersection(z.any(), z.any()))))).describe("Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.").optional(), "required_connections": z.array(z.object({ "provider": z.string().describe("Stable provider or platform namespace, preferably lowercase. Examples: `social.example`, `shortvideo.example`, or a seller-defined namespace. Omit only when the requirement is provider-agnostic, or when an `authorization_url` fully routes the human to the correct provider-specific connection flow.").optional(), "connection_type": z.enum(["advertiser_account","publisher_identity","post_authorization"]).describe("Kind of downstream connection required. `advertiser_account` is the platform account used to buy/manage ads. `publisher_identity` is the creator, page, channel, organization, or profile that owns source posts. `post_authorization` is a post-scoped grant when the platform authorizes individual posts instead of, or in addition to, the owning identity."), "required_for": z.array(z.string().min(1)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Concrete AdCP protocol operation names that require this downstream connection. Sellers SHOULD include this in product declarations when the requirement is known ahead of time, and in AUTHORIZATION_REQUIRED details when it explains the failed operation. Prefer specific operation names such as `list_creatives`, `sync_creatives`, `create_media_buy`, `get_media_buy_delivery`, or `get_creative_delivery` over broad category labels such as `reporting`.").optional(), "scope": z.enum(["account","identity","post","unknown"]).describe("Granularity of the downstream grant.").optional(), "status": z.enum(["connected","missing","pending","expired","revoked","not_required","unknown"]).describe("Current seller-observed state for this downstream connection when known. Product declarations MAY omit status or use `unknown`; AUTHORIZATION_REQUIRED details SHOULD use `missing`, `expired`, or `revoked` for the connection that blocked the call.").optional(), "connection_id": z.string().describe("Seller-defined identifier for an already-created downstream connection. Omit when no connection exists yet or when exposing it would leak platform/account state.").optional(), "resource_ref": z.object({ "platform_account_id": z.string().describe("Provider-native advertiser or business account id, when safe to disclose.").optional(), "identity_id": z.string().describe("Provider-native creator, page, channel, organization, or profile id, when safe to disclose.").optional(), "handle": z.string().describe("Provider-native public handle for the owning identity, when available.").optional(), "profile_url": z.string().url().describe("Public URL for the owning identity, when available.").optional(), "post_id": z.string().describe("Provider-native post id, when the grant is post-scoped or the failed request referenced a specific post.").optional(), "post_url": z.string().url().describe("Public URL for the referenced post, when available.").optional() }).catchall(z.any()).describe("Optional opaque provider-native resource hint, such as a platform account id, profile URL, handle, channel id, post id, or post URL. This is a hint for routing authorization, not proof that authorization exists.").optional(), "authorization_url": z.string().url().describe("Seller-hosted or provider-hosted URL where a human can complete or restore this downstream connection.").optional(), "authorization_instructions": z.string().describe("Human-readable instructions for completing or restoring this downstream connection.").optional(), "expires_at": z.string().datetime().describe("Expiration time for the downstream grant, when known.").optional() }).catchall(z.any()).and(z.any()).describe("A seller/platform-side connection or grant required by a product, format, or request. This is not the AdCP caller credential: the AdCP request is still authenticated once, and the seller uses these stored downstream connections to call a platform or service on the buyer's behalf. Use this shape for platforms that require more than one downstream grant, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.")).describe("Downstream platform connections or grants required to use this format declaration. These are in addition to the single AdCP caller credential. Use this when a platform product requires multiple downstream grants, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.").optional(), "reference_mutability": z.enum(["immutable_snapshot","mutable_requires_reapproval","mutable_auto_recheck"]).describe("Policy for formats whose `slots` accept a `published_post` reference. `immutable_snapshot`: seller snapshots the referenced post at approval and later source changes do not change the served creative. `mutable_requires_reapproval`: the source post may change and material changes require review before continued serving. `mutable_auto_recheck`: the source post may change and the seller continuously or periodically rechecks authorization/policy without requiring buyer resubmission. Omit when the format has no `published_post` slot.").optional(), "production_window_business_days": z.number().int().gte(0).describe("Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative).").optional() }).catchall(z.any()).describe("Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints)."), z.any()).describe("Direct video file (mp4/webm/mov) hosted by the buyer. Slot: `video_main` (video asset, file or hosted URL), optional `headline`, `brand_name`, `cta`, `companion_banner`, `landing_page_url`. Tracking model: IAB Open Measurement SDK + external impression/click/quartile pixels via universal_macros. Orientation is a parameter (vertical 9:16 / horizontal 16:9 / square 1:1); slot shape includes optional `brand_name` (typical for vertical short-form) and optional `companion_banner` (typical for horizontal instream). Distinct from `video_vast` (VAST tag, inherent VAST event tracking) — receivers fire impression and click pixels at delivery time.") }), z.object({ "format_kind": z.literal("video_vast"), "params": z.intersection(z.object({ "experimental": z.boolean().describe("When true, this canonical or seller narrowing may not work as declared. Adopters SHOULD preflight it with validate_input or in a sandbox and SHOULD NOT route production budget without testing; experimental status never makes the deprecated v1 path preferable. Drivers include unsettled spec shape, an adopter runtime gap, and custom shapes awaiting promotion. This replaces the earlier status plus runtime_status axes. Sellers SHOULD set experimental whenever a canonical or declaration is not production-ready.").default(false), "deprecated": z.boolean().describe("When true, this canonical (or a seller's specific narrowing of it) is going away. Existing adopters are supported through the deprecation cycle; new adoption is discouraged. Pair with `migration_target_version` to indicate when the canonical is expected to be removed. Distinct from `experimental`: an experimental canonical may stabilize and stop being experimental; a deprecated canonical is on a sunset path.").default(false), "v1_translatable": z.boolean().describe("Whether this canonical has any v1 named-format equivalent. `true` (default) — the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1→v2 projection via `v1-canonical-mapping.json` is meaningful. `false` — the canonical is inherently new in v2 and has no v1 form; v1's `list_creative_formats` couldn't express it because the underlying concept (algorithmic surface composition, AI-surface mentions, retail-media catalog placements, multi-card carousels) didn't exist as a v1 named-format archetype.\n\nLets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The six inherently-v2 canonicals in 3.2 are `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`, `seller_rendered_stateful_display`, and `coordinated_placements`.").default(true), "since_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version that introduced this canonical (e.g., '3.1', '3.2'). Lets adopters reason about minimum protocol version requirements when consuming a format declaration. Patch precision is intentionally rejected — canonicals are introduced at minor-version boundaries.").optional(), "migration_target_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version by which the working group expects this canonical to stabilize, surface a breaking revision, or (when `deprecated: true`) be removed. Patch precision is intentionally rejected — canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown').").optional(), "composition_model": z.enum(["deterministic","algorithmic"]).describe("Whether the surface composes deterministically (buyer can predict per-slot rendering — sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing — responsive_creative, agent_placement).").optional(), "provenance_required": z.boolean().describe("When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent.").optional(), "platform_extensions": z.array(z.object({ "uri": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL identifying the extension. `https://` is mandatory — `http://`, `file://`, `data:`, and other schemes are rejected at the schema layer (defense-in-depth on top of the fetch-contract normative rules). The URI base is the owning agent's URL; the path identifies the extension within that agent. Example: 'https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel'. The full fetch contract — SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds — is documented on `product-format-declaration.json#format_schema` and applies to ALL fetches of this reference shape regardless of whether the field is named `format_schema` (load-bearing for validation) or `platform_extensions` (informational); the *transport* rules are identical, only the *consumption* semantics differ."), "digest": z.string().regex(new RegExp("^sha256:[a-f0-9]{64}$")).describe("SHA-256 content digest of the extension definition (sha256:). Used to detect drift — if the agent revises the extension, the digest changes and cached definitions become invalid.") }).catchall(z.any()).describe("Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` — divergent digests in one response indicate producer-side error (e.g., concurrent extension revision mid-render). Buyers encountering divergent digests for the same URI MUST fail closed: treat all references to that URI as unresolved and surface a validation error rather than picking one branch silently. **Across responses**, digest divergence is normal — extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.")).describe("Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\n\n**Collision precedence (normative).** When two or more `platform_extensions[]` entries on the same declaration extend the same target (e.g., both extend `tracking`) with overlapping field names, **array order is authoritative — later entries override earlier ones on a per-field basis** (last-in-array-wins). SDKs MUST surface the overlap via the `errors[]` array on the `get_products` response with a structured code (`FORMAT_DECLARATION_DIVERGENT` is appropriate when the overlap appears across dual-emitted shapes; a producer-self-emitted overlap on a single declaration SHOULD use the same code with `error.details: { collision_kind: \"platform_extension_field\", target, overlapping_fields, winning_extension_uri }`). Producers SHOULD avoid the collision by emitting one extension per target or by partitioning fields across extensions; the deterministic precedence is for last-resort consistency across SDK implementations, not a sanctioned merging strategy.").optional(), "synthesis_nondeterministic": z.boolean().describe("When true, the format's production pipeline is genuinely nondeterministic — the platform cannot guarantee that synthesis from a given input set produces in-spec output. Veo / Sora / Runway-class generative video, and other AI-synthesis flows where output dimensions, duration, or quality vary per run. Implies a different validation contract: predictive `validate_input` is impossible; the platform's own post-synthesis QA loop applies; if the QA loop exhausts without producing a valid artifact, `build_creative` returns task_failed with a synthesis_failed reason. Distinct from `composition_model` (which describes how the surface composes per-slot rendering, not whether synthesis is deterministic). When false or absent, the format's production is predictable enough that `validate_input` can predict output properties from input properties.\n\n**Compatibility with `asset_source` / `item_production_model`**: `synthesis_nondeterministic: true` MAY pair with any of `seller_pre_rendered_from_brief`, `seller_human_designed`, or `agent_synthesized` (the QA loop is concept-level, not source-specific — 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.").default(false), "slots": z.array(z.object({ "asset_group_id": z.string().describe("Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings."), "asset_type": z.enum(["image","video","audio","text","markdown","url","html","css","javascript","vast","daast","webhook","brief","catalog","published_post","zip","card","object","pixel_tracker","vast_tracker","daast_tracker"]).describe("Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `published_post` is an existing-post reference asset, not uploaded media bytes and not a catalog row. `card` is the multi-card carousel element type (see card-asset.json). `pixel_tracker` / `vast_tracker` / `daast_tracker` are the renderer-fired measurement-tracker primitives — see `/schemas/core/assets/pixel-tracker-asset.json` and the VAST / DAAST tracker schemas. `object` is a last-resort fallback for structured non-asset inputs that don't fit any primitive asset_type — prefer specific types whenever possible."), "required": z.boolean().describe("Whether this slot is required for a valid manifest.").default(false), "min": z.number().int().gte(0).describe("Minimum count for repeatable / pool slots.").optional(), "max": z.number().int().gte(1).describe("Maximum count for repeatable / pool slots.").optional(), "max_chars": z.number().int().gte(1).describe("Per-slot character limit. Valid only when `asset_type` is `text`, `markdown`, or `brief`. Mutually exclusive with `max_size_kb` (which applies to binary asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "max_size_kb": z.number().int().gte(1).describe("Per-slot file size limit in kilobytes. Valid only when `asset_type` is `image`, `video`, `audio`, or `zip`. Mutually exclusive with `max_chars` (which applies to text asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Accepted intrinsic-pixel densities for this image-bearing slot. Valid when `asset_type` is `image`, and on a `card` slot where it constrains each card's image media (video media is unaffected). This makes density available to every canonical carrying image assets (native, carousel, responsive, companion images, and image itself), not only `format_kind: image`. When the image canonical also declares top-level `params.pixel_ratios`, the effective set is the intersection; an empty intersection is invalid. One matching asset satisfies the slot unless `required_pixel_ratios` requires rendition coverage.").optional(), "required_pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Required density coverage for an image rendition set. Valid only when `asset_type` is `image` and `pixel_ratios` is also declared. Every value MUST appear in the effective accepted set after intersecting any top-level image `params.pixel_ratios`, and the manifest slot value MUST be an array containing exactly one matching image rendition for each required ratio. Other accepted ratios remain optional. For example, `pixel_ratios: [1, 1.5, 2]` with `required_pixel_ratios: [1, 2]` requires the 1x and 2x renditions while making 1.5x optional. SDKs enforce intersection, subset, coverage, and duplicate-ratio rules because JSON Schema draft-07 cannot express them generically.").optional(), "logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("When `asset_group_id` is `logo`, renderer-facing brand.json logo slots acceptable for this format slot. Producers selecting from brand.json SHOULD prefer `logos[]` entries whose `slots[]` intersects this list, then apply `visual_guidelines.logo_usage_rules[]`.").optional(), "required_logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Subset of `logo_slots` for which this format expects explicit logo coverage. A manifest or brand-derived logo pool SHOULD include at least one usable logo for each required slot; if coverage is missing, builders SHOULD surface a validation warning or approval mapping instead of guessing from prose.").optional(), "description": z.string().describe("Human-readable description of what the slot expects from the buyer.").optional(), "consumed_for_production": z.boolean().describe("Dispatch hint for `build_creative` and v1↔v2 wire translators: when `true`, the slot's value is consumed as INPUT to a production step (host-read script, brief copy fed to generative synthesis, catalog feed driving per-SKU rendering) and is not rendered verbatim. When `false` (default), the slot's value is rendered verbatim on the placement (image bytes, video file, display tag).\n\nMotivates the v1↔v2 dispatch table: pre-v2 buyers shipped production-consumed inputs separately in a `inputs` map on the build_creative request; v2 collapses inputs and rendered assets into a single `assets` map keyed by `asset_group_id`. SDK translators between v1 and v2 use this flag per canonical to know which assets in the v2 manifest map back to v1 `inputs` vs v1 `assets`. Without the per-slot flag the dispatch table lives in adopter code and every SDK gets it slightly different.\n\nProducers SHOULD set this explicitly on slots whose consumption pattern isn't obvious (host-read scripts on `audio_hosted`, briefs on generative `video_hosted`, catalog feeds on `sponsored_placement`). For canonicals where every slot is render-verbatim (`image`, `display_tag`, `video_vast`), the default `false` is sufficient and the flag MAY be omitted.").default(false) }).catchall(z.any()).and(z.intersection(z.intersection(z.any(), z.intersection(z.any(), z.any())), z.intersection(z.any(), z.intersection(z.any(), z.any()))))).describe("Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.").optional(), "required_connections": z.array(z.object({ "provider": z.string().describe("Stable provider or platform namespace, preferably lowercase. Examples: `social.example`, `shortvideo.example`, or a seller-defined namespace. Omit only when the requirement is provider-agnostic, or when an `authorization_url` fully routes the human to the correct provider-specific connection flow.").optional(), "connection_type": z.enum(["advertiser_account","publisher_identity","post_authorization"]).describe("Kind of downstream connection required. `advertiser_account` is the platform account used to buy/manage ads. `publisher_identity` is the creator, page, channel, organization, or profile that owns source posts. `post_authorization` is a post-scoped grant when the platform authorizes individual posts instead of, or in addition to, the owning identity."), "required_for": z.array(z.string().min(1)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Concrete AdCP protocol operation names that require this downstream connection. Sellers SHOULD include this in product declarations when the requirement is known ahead of time, and in AUTHORIZATION_REQUIRED details when it explains the failed operation. Prefer specific operation names such as `list_creatives`, `sync_creatives`, `create_media_buy`, `get_media_buy_delivery`, or `get_creative_delivery` over broad category labels such as `reporting`.").optional(), "scope": z.enum(["account","identity","post","unknown"]).describe("Granularity of the downstream grant.").optional(), "status": z.enum(["connected","missing","pending","expired","revoked","not_required","unknown"]).describe("Current seller-observed state for this downstream connection when known. Product declarations MAY omit status or use `unknown`; AUTHORIZATION_REQUIRED details SHOULD use `missing`, `expired`, or `revoked` for the connection that blocked the call.").optional(), "connection_id": z.string().describe("Seller-defined identifier for an already-created downstream connection. Omit when no connection exists yet or when exposing it would leak platform/account state.").optional(), "resource_ref": z.object({ "platform_account_id": z.string().describe("Provider-native advertiser or business account id, when safe to disclose.").optional(), "identity_id": z.string().describe("Provider-native creator, page, channel, organization, or profile id, when safe to disclose.").optional(), "handle": z.string().describe("Provider-native public handle for the owning identity, when available.").optional(), "profile_url": z.string().url().describe("Public URL for the owning identity, when available.").optional(), "post_id": z.string().describe("Provider-native post id, when the grant is post-scoped or the failed request referenced a specific post.").optional(), "post_url": z.string().url().describe("Public URL for the referenced post, when available.").optional() }).catchall(z.any()).describe("Optional opaque provider-native resource hint, such as a platform account id, profile URL, handle, channel id, post id, or post URL. This is a hint for routing authorization, not proof that authorization exists.").optional(), "authorization_url": z.string().url().describe("Seller-hosted or provider-hosted URL where a human can complete or restore this downstream connection.").optional(), "authorization_instructions": z.string().describe("Human-readable instructions for completing or restoring this downstream connection.").optional(), "expires_at": z.string().datetime().describe("Expiration time for the downstream grant, when known.").optional() }).catchall(z.any()).and(z.any()).describe("A seller/platform-side connection or grant required by a product, format, or request. This is not the AdCP caller credential: the AdCP request is still authenticated once, and the seller uses these stored downstream connections to call a platform or service on the buyer's behalf. Use this shape for platforms that require more than one downstream grant, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.")).describe("Downstream platform connections or grants required to use this format declaration. These are in addition to the single AdCP caller credential. Use this when a platform product requires multiple downstream grants, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.").optional(), "reference_mutability": z.enum(["immutable_snapshot","mutable_requires_reapproval","mutable_auto_recheck"]).describe("Policy for formats whose `slots` accept a `published_post` reference. `immutable_snapshot`: seller snapshots the referenced post at approval and later source changes do not change the served creative. `mutable_requires_reapproval`: the source post may change and material changes require review before continued serving. `mutable_auto_recheck`: the source post may change and the seller continuously or periodically rechecks authorization/policy without requiring buyer resubmission. Omit when the format has no `published_post` slot.").optional(), "production_window_business_days": z.number().int().gte(0).describe("Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative).").optional() }).catchall(z.any()).describe("Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints)."), z.intersection(z.any(), z.any())).describe("VAST-tag-delivered video creative. Slot: `vast_tag` (vast asset, URL or inline XML, VAST 2.x-4.x). Tracking model: VAST events inherent to the spec — `impression`, `firstQuartile`, `midpoint`, `thirdQuartile`, `complete`, `start`, `pause`, `resume`, `mute`, `unmute`, `expand`, `collapse`, `fullscreen`, `creativeView`, `clickTracking`, `error`. VPAID interactivity via `vpaid_enabled: true` flag. SIMID is carried by the first-class VAST 4.1+ `` element on Linear creatives. Orientation is a parameter (vertical / horizontal / square). Distinct from `video_hosted` (direct file with external tracking).") }), z.object({ "format_kind": z.literal("audio_hosted"), "params": z.object({ "experimental": z.boolean().describe("When true, this canonical or seller narrowing may not work as declared. Adopters SHOULD preflight it with validate_input or in a sandbox and SHOULD NOT route production budget without testing; experimental status never makes the deprecated v1 path preferable. Drivers include unsettled spec shape, an adopter runtime gap, and custom shapes awaiting promotion. This replaces the earlier status plus runtime_status axes. Sellers SHOULD set experimental whenever a canonical or declaration is not production-ready.").default(false), "deprecated": z.boolean().describe("When true, this canonical (or a seller's specific narrowing of it) is going away. Existing adopters are supported through the deprecation cycle; new adoption is discouraged. Pair with `migration_target_version` to indicate when the canonical is expected to be removed. Distinct from `experimental`: an experimental canonical may stabilize and stop being experimental; a deprecated canonical is on a sunset path.").default(false), "v1_translatable": z.boolean().describe("Whether this canonical has any v1 named-format equivalent. `true` (default) — the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1→v2 projection via `v1-canonical-mapping.json` is meaningful. `false` — the canonical is inherently new in v2 and has no v1 form; v1's `list_creative_formats` couldn't express it because the underlying concept (algorithmic surface composition, AI-surface mentions, retail-media catalog placements, multi-card carousels) didn't exist as a v1 named-format archetype.\n\nLets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The six inherently-v2 canonicals in 3.2 are `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`, `seller_rendered_stateful_display`, and `coordinated_placements`.").default(true), "since_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version that introduced this canonical (e.g., '3.1', '3.2'). Lets adopters reason about minimum protocol version requirements when consuming a format declaration. Patch precision is intentionally rejected — canonicals are introduced at minor-version boundaries.").optional(), "migration_target_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version by which the working group expects this canonical to stabilize, surface a breaking revision, or (when `deprecated: true`) be removed. Patch precision is intentionally rejected — canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown').").optional(), "composition_model": z.enum(["deterministic","algorithmic"]).describe("Whether the surface composes deterministically (buyer can predict per-slot rendering — sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing — responsive_creative, agent_placement).").optional(), "provenance_required": z.boolean().describe("When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent.").optional(), "platform_extensions": z.array(z.object({ "uri": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL identifying the extension. `https://` is mandatory — `http://`, `file://`, `data:`, and other schemes are rejected at the schema layer (defense-in-depth on top of the fetch-contract normative rules). The URI base is the owning agent's URL; the path identifies the extension within that agent. Example: 'https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel'. The full fetch contract — SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds — is documented on `product-format-declaration.json#format_schema` and applies to ALL fetches of this reference shape regardless of whether the field is named `format_schema` (load-bearing for validation) or `platform_extensions` (informational); the *transport* rules are identical, only the *consumption* semantics differ."), "digest": z.string().regex(new RegExp("^sha256:[a-f0-9]{64}$")).describe("SHA-256 content digest of the extension definition (sha256:). Used to detect drift — if the agent revises the extension, the digest changes and cached definitions become invalid.") }).catchall(z.any()).describe("Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` — divergent digests in one response indicate producer-side error (e.g., concurrent extension revision mid-render). Buyers encountering divergent digests for the same URI MUST fail closed: treat all references to that URI as unresolved and surface a validation error rather than picking one branch silently. **Across responses**, digest divergence is normal — extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.")).describe("Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\n\n**Collision precedence (normative).** When two or more `platform_extensions[]` entries on the same declaration extend the same target (e.g., both extend `tracking`) with overlapping field names, **array order is authoritative — later entries override earlier ones on a per-field basis** (last-in-array-wins). SDKs MUST surface the overlap via the `errors[]` array on the `get_products` response with a structured code (`FORMAT_DECLARATION_DIVERGENT` is appropriate when the overlap appears across dual-emitted shapes; a producer-self-emitted overlap on a single declaration SHOULD use the same code with `error.details: { collision_kind: \"platform_extension_field\", target, overlapping_fields, winning_extension_uri }`). Producers SHOULD avoid the collision by emitting one extension per target or by partitioning fields across extensions; the deterministic precedence is for last-resort consistency across SDK implementations, not a sanctioned merging strategy.").optional(), "synthesis_nondeterministic": z.boolean().describe("When true, the format's production pipeline is genuinely nondeterministic — the platform cannot guarantee that synthesis from a given input set produces in-spec output. Veo / Sora / Runway-class generative video, and other AI-synthesis flows where output dimensions, duration, or quality vary per run. Implies a different validation contract: predictive `validate_input` is impossible; the platform's own post-synthesis QA loop applies; if the QA loop exhausts without producing a valid artifact, `build_creative` returns task_failed with a synthesis_failed reason. Distinct from `composition_model` (which describes how the surface composes per-slot rendering, not whether synthesis is deterministic). When false or absent, the format's production is predictable enough that `validate_input` can predict output properties from input properties.\n\n**Compatibility with `asset_source` / `item_production_model`**: `synthesis_nondeterministic: true` MAY pair with any of `seller_pre_rendered_from_brief`, `seller_human_designed`, or `agent_synthesized` (the QA loop is concept-level, not source-specific — 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.").default(false), "slots": z.array(z.object({ "asset_group_id": z.string().describe("Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings."), "asset_type": z.enum(["image","video","audio","text","markdown","url","html","css","javascript","vast","daast","webhook","brief","catalog","published_post","zip","card","object","pixel_tracker","vast_tracker","daast_tracker"]).describe("Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `published_post` is an existing-post reference asset, not uploaded media bytes and not a catalog row. `card` is the multi-card carousel element type (see card-asset.json). `pixel_tracker` / `vast_tracker` / `daast_tracker` are the renderer-fired measurement-tracker primitives — see `/schemas/core/assets/pixel-tracker-asset.json` and the VAST / DAAST tracker schemas. `object` is a last-resort fallback for structured non-asset inputs that don't fit any primitive asset_type — prefer specific types whenever possible."), "required": z.boolean().describe("Whether this slot is required for a valid manifest.").default(false), "min": z.number().int().gte(0).describe("Minimum count for repeatable / pool slots.").optional(), "max": z.number().int().gte(1).describe("Maximum count for repeatable / pool slots.").optional(), "max_chars": z.number().int().gte(1).describe("Per-slot character limit. Valid only when `asset_type` is `text`, `markdown`, or `brief`. Mutually exclusive with `max_size_kb` (which applies to binary asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "max_size_kb": z.number().int().gte(1).describe("Per-slot file size limit in kilobytes. Valid only when `asset_type` is `image`, `video`, `audio`, or `zip`. Mutually exclusive with `max_chars` (which applies to text asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Accepted intrinsic-pixel densities for this image-bearing slot. Valid when `asset_type` is `image`, and on a `card` slot where it constrains each card's image media (video media is unaffected). This makes density available to every canonical carrying image assets (native, carousel, responsive, companion images, and image itself), not only `format_kind: image`. When the image canonical also declares top-level `params.pixel_ratios`, the effective set is the intersection; an empty intersection is invalid. One matching asset satisfies the slot unless `required_pixel_ratios` requires rendition coverage.").optional(), "required_pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Required density coverage for an image rendition set. Valid only when `asset_type` is `image` and `pixel_ratios` is also declared. Every value MUST appear in the effective accepted set after intersecting any top-level image `params.pixel_ratios`, and the manifest slot value MUST be an array containing exactly one matching image rendition for each required ratio. Other accepted ratios remain optional. For example, `pixel_ratios: [1, 1.5, 2]` with `required_pixel_ratios: [1, 2]` requires the 1x and 2x renditions while making 1.5x optional. SDKs enforce intersection, subset, coverage, and duplicate-ratio rules because JSON Schema draft-07 cannot express them generically.").optional(), "logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("When `asset_group_id` is `logo`, renderer-facing brand.json logo slots acceptable for this format slot. Producers selecting from brand.json SHOULD prefer `logos[]` entries whose `slots[]` intersects this list, then apply `visual_guidelines.logo_usage_rules[]`.").optional(), "required_logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Subset of `logo_slots` for which this format expects explicit logo coverage. A manifest or brand-derived logo pool SHOULD include at least one usable logo for each required slot; if coverage is missing, builders SHOULD surface a validation warning or approval mapping instead of guessing from prose.").optional(), "description": z.string().describe("Human-readable description of what the slot expects from the buyer.").optional(), "consumed_for_production": z.boolean().describe("Dispatch hint for `build_creative` and v1↔v2 wire translators: when `true`, the slot's value is consumed as INPUT to a production step (host-read script, brief copy fed to generative synthesis, catalog feed driving per-SKU rendering) and is not rendered verbatim. When `false` (default), the slot's value is rendered verbatim on the placement (image bytes, video file, display tag).\n\nMotivates the v1↔v2 dispatch table: pre-v2 buyers shipped production-consumed inputs separately in a `inputs` map on the build_creative request; v2 collapses inputs and rendered assets into a single `assets` map keyed by `asset_group_id`. SDK translators between v1 and v2 use this flag per canonical to know which assets in the v2 manifest map back to v1 `inputs` vs v1 `assets`. Without the per-slot flag the dispatch table lives in adopter code and every SDK gets it slightly different.\n\nProducers SHOULD set this explicitly on slots whose consumption pattern isn't obvious (host-read scripts on `audio_hosted`, briefs on generative `video_hosted`, catalog feeds on `sponsored_placement`). For canonicals where every slot is render-verbatim (`image`, `display_tag`, `video_vast`), the default `false` is sufficient and the flag MAY be omitted.").default(false) }).catchall(z.any()).and(z.intersection(z.intersection(z.any(), z.intersection(z.any(), z.any())), z.intersection(z.any(), z.intersection(z.any(), z.any()))))).describe("Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.").optional(), "required_connections": z.array(z.object({ "provider": z.string().describe("Stable provider or platform namespace, preferably lowercase. Examples: `social.example`, `shortvideo.example`, or a seller-defined namespace. Omit only when the requirement is provider-agnostic, or when an `authorization_url` fully routes the human to the correct provider-specific connection flow.").optional(), "connection_type": z.enum(["advertiser_account","publisher_identity","post_authorization"]).describe("Kind of downstream connection required. `advertiser_account` is the platform account used to buy/manage ads. `publisher_identity` is the creator, page, channel, organization, or profile that owns source posts. `post_authorization` is a post-scoped grant when the platform authorizes individual posts instead of, or in addition to, the owning identity."), "required_for": z.array(z.string().min(1)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Concrete AdCP protocol operation names that require this downstream connection. Sellers SHOULD include this in product declarations when the requirement is known ahead of time, and in AUTHORIZATION_REQUIRED details when it explains the failed operation. Prefer specific operation names such as `list_creatives`, `sync_creatives`, `create_media_buy`, `get_media_buy_delivery`, or `get_creative_delivery` over broad category labels such as `reporting`.").optional(), "scope": z.enum(["account","identity","post","unknown"]).describe("Granularity of the downstream grant.").optional(), "status": z.enum(["connected","missing","pending","expired","revoked","not_required","unknown"]).describe("Current seller-observed state for this downstream connection when known. Product declarations MAY omit status or use `unknown`; AUTHORIZATION_REQUIRED details SHOULD use `missing`, `expired`, or `revoked` for the connection that blocked the call.").optional(), "connection_id": z.string().describe("Seller-defined identifier for an already-created downstream connection. Omit when no connection exists yet or when exposing it would leak platform/account state.").optional(), "resource_ref": z.object({ "platform_account_id": z.string().describe("Provider-native advertiser or business account id, when safe to disclose.").optional(), "identity_id": z.string().describe("Provider-native creator, page, channel, organization, or profile id, when safe to disclose.").optional(), "handle": z.string().describe("Provider-native public handle for the owning identity, when available.").optional(), "profile_url": z.string().url().describe("Public URL for the owning identity, when available.").optional(), "post_id": z.string().describe("Provider-native post id, when the grant is post-scoped or the failed request referenced a specific post.").optional(), "post_url": z.string().url().describe("Public URL for the referenced post, when available.").optional() }).catchall(z.any()).describe("Optional opaque provider-native resource hint, such as a platform account id, profile URL, handle, channel id, post id, or post URL. This is a hint for routing authorization, not proof that authorization exists.").optional(), "authorization_url": z.string().url().describe("Seller-hosted or provider-hosted URL where a human can complete or restore this downstream connection.").optional(), "authorization_instructions": z.string().describe("Human-readable instructions for completing or restoring this downstream connection.").optional(), "expires_at": z.string().datetime().describe("Expiration time for the downstream grant, when known.").optional() }).catchall(z.any()).and(z.any()).describe("A seller/platform-side connection or grant required by a product, format, or request. This is not the AdCP caller credential: the AdCP request is still authenticated once, and the seller uses these stored downstream connections to call a platform or service on the buyer's behalf. Use this shape for platforms that require more than one downstream grant, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.")).describe("Downstream platform connections or grants required to use this format declaration. These are in addition to the single AdCP caller credential. Use this when a platform product requires multiple downstream grants, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.").optional(), "reference_mutability": z.enum(["immutable_snapshot","mutable_requires_reapproval","mutable_auto_recheck"]).describe("Policy for formats whose `slots` accept a `published_post` reference. `immutable_snapshot`: seller snapshots the referenced post at approval and later source changes do not change the served creative. `mutable_requires_reapproval`: the source post may change and material changes require review before continued serving. `mutable_auto_recheck`: the source post may change and the seller continuously or periodically rechecks authorization/policy without requiring buyer resubmission. Omit when the format has no `published_post` slot.").optional(), "production_window_business_days": z.number().int().gte(0).describe("Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative).").optional() }).catchall(z.any()).describe("Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).").describe("Direct audio creative — buyer ships an `audio` asset (mp3/aac/wav) for asset-driven products, or ships a `script` / `creative_brief` text asset for products where the seller produces audio internally (podcast host-reads, TTS synthesis). Optional companion slots: `companion_image`, `brand_name`, `landing_page_url`. Tracking model: standard impression + completion + companion-image-click pixels via universal_macros. Distinct from `audio_daast` (DAAST tag, inherent DAAST event tracking). For host-reads and synthesized audio, the format declares `asset_source: 'publisher_host_recorded'` or `'agent_synthesized'` plus `buyer_asset_acceptance: 'rejected'`; the format's `slots` declaration enumerates which assets the buyer ships (e.g., `script` text asset for host-reads). The seller decides how to consume each asset (render verbatim vs produce audio from text) — there is no separate manifest 'inputs' map; everything the buyer ships goes in `assets`.") }), z.object({ "format_kind": z.literal("audio_daast"), "params": z.object({ "experimental": z.boolean().describe("When true, this canonical or seller narrowing may not work as declared. Adopters SHOULD preflight it with validate_input or in a sandbox and SHOULD NOT route production budget without testing; experimental status never makes the deprecated v1 path preferable. Drivers include unsettled spec shape, an adopter runtime gap, and custom shapes awaiting promotion. This replaces the earlier status plus runtime_status axes. Sellers SHOULD set experimental whenever a canonical or declaration is not production-ready.").default(false), "deprecated": z.boolean().describe("When true, this canonical (or a seller's specific narrowing of it) is going away. Existing adopters are supported through the deprecation cycle; new adoption is discouraged. Pair with `migration_target_version` to indicate when the canonical is expected to be removed. Distinct from `experimental`: an experimental canonical may stabilize and stop being experimental; a deprecated canonical is on a sunset path.").default(false), "v1_translatable": z.boolean().describe("Whether this canonical has any v1 named-format equivalent. `true` (default) — the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1→v2 projection via `v1-canonical-mapping.json` is meaningful. `false` — the canonical is inherently new in v2 and has no v1 form; v1's `list_creative_formats` couldn't express it because the underlying concept (algorithmic surface composition, AI-surface mentions, retail-media catalog placements, multi-card carousels) didn't exist as a v1 named-format archetype.\n\nLets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The six inherently-v2 canonicals in 3.2 are `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`, `seller_rendered_stateful_display`, and `coordinated_placements`.").default(true), "since_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version that introduced this canonical (e.g., '3.1', '3.2'). Lets adopters reason about minimum protocol version requirements when consuming a format declaration. Patch precision is intentionally rejected — canonicals are introduced at minor-version boundaries.").optional(), "migration_target_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version by which the working group expects this canonical to stabilize, surface a breaking revision, or (when `deprecated: true`) be removed. Patch precision is intentionally rejected — canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown').").optional(), "composition_model": z.enum(["deterministic","algorithmic"]).describe("Whether the surface composes deterministically (buyer can predict per-slot rendering — sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing — responsive_creative, agent_placement).").optional(), "provenance_required": z.boolean().describe("When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent.").optional(), "platform_extensions": z.array(z.object({ "uri": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL identifying the extension. `https://` is mandatory — `http://`, `file://`, `data:`, and other schemes are rejected at the schema layer (defense-in-depth on top of the fetch-contract normative rules). The URI base is the owning agent's URL; the path identifies the extension within that agent. Example: 'https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel'. The full fetch contract — SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds — is documented on `product-format-declaration.json#format_schema` and applies to ALL fetches of this reference shape regardless of whether the field is named `format_schema` (load-bearing for validation) or `platform_extensions` (informational); the *transport* rules are identical, only the *consumption* semantics differ."), "digest": z.string().regex(new RegExp("^sha256:[a-f0-9]{64}$")).describe("SHA-256 content digest of the extension definition (sha256:). Used to detect drift — if the agent revises the extension, the digest changes and cached definitions become invalid.") }).catchall(z.any()).describe("Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` — divergent digests in one response indicate producer-side error (e.g., concurrent extension revision mid-render). Buyers encountering divergent digests for the same URI MUST fail closed: treat all references to that URI as unresolved and surface a validation error rather than picking one branch silently. **Across responses**, digest divergence is normal — extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.")).describe("Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\n\n**Collision precedence (normative).** When two or more `platform_extensions[]` entries on the same declaration extend the same target (e.g., both extend `tracking`) with overlapping field names, **array order is authoritative — later entries override earlier ones on a per-field basis** (last-in-array-wins). SDKs MUST surface the overlap via the `errors[]` array on the `get_products` response with a structured code (`FORMAT_DECLARATION_DIVERGENT` is appropriate when the overlap appears across dual-emitted shapes; a producer-self-emitted overlap on a single declaration SHOULD use the same code with `error.details: { collision_kind: \"platform_extension_field\", target, overlapping_fields, winning_extension_uri }`). Producers SHOULD avoid the collision by emitting one extension per target or by partitioning fields across extensions; the deterministic precedence is for last-resort consistency across SDK implementations, not a sanctioned merging strategy.").optional(), "synthesis_nondeterministic": z.boolean().describe("When true, the format's production pipeline is genuinely nondeterministic — the platform cannot guarantee that synthesis from a given input set produces in-spec output. Veo / Sora / Runway-class generative video, and other AI-synthesis flows where output dimensions, duration, or quality vary per run. Implies a different validation contract: predictive `validate_input` is impossible; the platform's own post-synthesis QA loop applies; if the QA loop exhausts without producing a valid artifact, `build_creative` returns task_failed with a synthesis_failed reason. Distinct from `composition_model` (which describes how the surface composes per-slot rendering, not whether synthesis is deterministic). When false or absent, the format's production is predictable enough that `validate_input` can predict output properties from input properties.\n\n**Compatibility with `asset_source` / `item_production_model`**: `synthesis_nondeterministic: true` MAY pair with any of `seller_pre_rendered_from_brief`, `seller_human_designed`, or `agent_synthesized` (the QA loop is concept-level, not source-specific — 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.").default(false), "slots": z.array(z.object({ "asset_group_id": z.string().describe("Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings."), "asset_type": z.enum(["image","video","audio","text","markdown","url","html","css","javascript","vast","daast","webhook","brief","catalog","published_post","zip","card","object","pixel_tracker","vast_tracker","daast_tracker"]).describe("Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `published_post` is an existing-post reference asset, not uploaded media bytes and not a catalog row. `card` is the multi-card carousel element type (see card-asset.json). `pixel_tracker` / `vast_tracker` / `daast_tracker` are the renderer-fired measurement-tracker primitives — see `/schemas/core/assets/pixel-tracker-asset.json` and the VAST / DAAST tracker schemas. `object` is a last-resort fallback for structured non-asset inputs that don't fit any primitive asset_type — prefer specific types whenever possible."), "required": z.boolean().describe("Whether this slot is required for a valid manifest.").default(false), "min": z.number().int().gte(0).describe("Minimum count for repeatable / pool slots.").optional(), "max": z.number().int().gte(1).describe("Maximum count for repeatable / pool slots.").optional(), "max_chars": z.number().int().gte(1).describe("Per-slot character limit. Valid only when `asset_type` is `text`, `markdown`, or `brief`. Mutually exclusive with `max_size_kb` (which applies to binary asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "max_size_kb": z.number().int().gte(1).describe("Per-slot file size limit in kilobytes. Valid only when `asset_type` is `image`, `video`, `audio`, or `zip`. Mutually exclusive with `max_chars` (which applies to text asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Accepted intrinsic-pixel densities for this image-bearing slot. Valid when `asset_type` is `image`, and on a `card` slot where it constrains each card's image media (video media is unaffected). This makes density available to every canonical carrying image assets (native, carousel, responsive, companion images, and image itself), not only `format_kind: image`. When the image canonical also declares top-level `params.pixel_ratios`, the effective set is the intersection; an empty intersection is invalid. One matching asset satisfies the slot unless `required_pixel_ratios` requires rendition coverage.").optional(), "required_pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Required density coverage for an image rendition set. Valid only when `asset_type` is `image` and `pixel_ratios` is also declared. Every value MUST appear in the effective accepted set after intersecting any top-level image `params.pixel_ratios`, and the manifest slot value MUST be an array containing exactly one matching image rendition for each required ratio. Other accepted ratios remain optional. For example, `pixel_ratios: [1, 1.5, 2]` with `required_pixel_ratios: [1, 2]` requires the 1x and 2x renditions while making 1.5x optional. SDKs enforce intersection, subset, coverage, and duplicate-ratio rules because JSON Schema draft-07 cannot express them generically.").optional(), "logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("When `asset_group_id` is `logo`, renderer-facing brand.json logo slots acceptable for this format slot. Producers selecting from brand.json SHOULD prefer `logos[]` entries whose `slots[]` intersects this list, then apply `visual_guidelines.logo_usage_rules[]`.").optional(), "required_logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Subset of `logo_slots` for which this format expects explicit logo coverage. A manifest or brand-derived logo pool SHOULD include at least one usable logo for each required slot; if coverage is missing, builders SHOULD surface a validation warning or approval mapping instead of guessing from prose.").optional(), "description": z.string().describe("Human-readable description of what the slot expects from the buyer.").optional(), "consumed_for_production": z.boolean().describe("Dispatch hint for `build_creative` and v1↔v2 wire translators: when `true`, the slot's value is consumed as INPUT to a production step (host-read script, brief copy fed to generative synthesis, catalog feed driving per-SKU rendering) and is not rendered verbatim. When `false` (default), the slot's value is rendered verbatim on the placement (image bytes, video file, display tag).\n\nMotivates the v1↔v2 dispatch table: pre-v2 buyers shipped production-consumed inputs separately in a `inputs` map on the build_creative request; v2 collapses inputs and rendered assets into a single `assets` map keyed by `asset_group_id`. SDK translators between v1 and v2 use this flag per canonical to know which assets in the v2 manifest map back to v1 `inputs` vs v1 `assets`. Without the per-slot flag the dispatch table lives in adopter code and every SDK gets it slightly different.\n\nProducers SHOULD set this explicitly on slots whose consumption pattern isn't obvious (host-read scripts on `audio_hosted`, briefs on generative `video_hosted`, catalog feeds on `sponsored_placement`). For canonicals where every slot is render-verbatim (`image`, `display_tag`, `video_vast`), the default `false` is sufficient and the flag MAY be omitted.").default(false) }).catchall(z.any()).and(z.intersection(z.intersection(z.any(), z.intersection(z.any(), z.any())), z.intersection(z.any(), z.intersection(z.any(), z.any()))))).describe("Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.").optional(), "required_connections": z.array(z.object({ "provider": z.string().describe("Stable provider or platform namespace, preferably lowercase. Examples: `social.example`, `shortvideo.example`, or a seller-defined namespace. Omit only when the requirement is provider-agnostic, or when an `authorization_url` fully routes the human to the correct provider-specific connection flow.").optional(), "connection_type": z.enum(["advertiser_account","publisher_identity","post_authorization"]).describe("Kind of downstream connection required. `advertiser_account` is the platform account used to buy/manage ads. `publisher_identity` is the creator, page, channel, organization, or profile that owns source posts. `post_authorization` is a post-scoped grant when the platform authorizes individual posts instead of, or in addition to, the owning identity."), "required_for": z.array(z.string().min(1)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Concrete AdCP protocol operation names that require this downstream connection. Sellers SHOULD include this in product declarations when the requirement is known ahead of time, and in AUTHORIZATION_REQUIRED details when it explains the failed operation. Prefer specific operation names such as `list_creatives`, `sync_creatives`, `create_media_buy`, `get_media_buy_delivery`, or `get_creative_delivery` over broad category labels such as `reporting`.").optional(), "scope": z.enum(["account","identity","post","unknown"]).describe("Granularity of the downstream grant.").optional(), "status": z.enum(["connected","missing","pending","expired","revoked","not_required","unknown"]).describe("Current seller-observed state for this downstream connection when known. Product declarations MAY omit status or use `unknown`; AUTHORIZATION_REQUIRED details SHOULD use `missing`, `expired`, or `revoked` for the connection that blocked the call.").optional(), "connection_id": z.string().describe("Seller-defined identifier for an already-created downstream connection. Omit when no connection exists yet or when exposing it would leak platform/account state.").optional(), "resource_ref": z.object({ "platform_account_id": z.string().describe("Provider-native advertiser or business account id, when safe to disclose.").optional(), "identity_id": z.string().describe("Provider-native creator, page, channel, organization, or profile id, when safe to disclose.").optional(), "handle": z.string().describe("Provider-native public handle for the owning identity, when available.").optional(), "profile_url": z.string().url().describe("Public URL for the owning identity, when available.").optional(), "post_id": z.string().describe("Provider-native post id, when the grant is post-scoped or the failed request referenced a specific post.").optional(), "post_url": z.string().url().describe("Public URL for the referenced post, when available.").optional() }).catchall(z.any()).describe("Optional opaque provider-native resource hint, such as a platform account id, profile URL, handle, channel id, post id, or post URL. This is a hint for routing authorization, not proof that authorization exists.").optional(), "authorization_url": z.string().url().describe("Seller-hosted or provider-hosted URL where a human can complete or restore this downstream connection.").optional(), "authorization_instructions": z.string().describe("Human-readable instructions for completing or restoring this downstream connection.").optional(), "expires_at": z.string().datetime().describe("Expiration time for the downstream grant, when known.").optional() }).catchall(z.any()).and(z.any()).describe("A seller/platform-side connection or grant required by a product, format, or request. This is not the AdCP caller credential: the AdCP request is still authenticated once, and the seller uses these stored downstream connections to call a platform or service on the buyer's behalf. Use this shape for platforms that require more than one downstream grant, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.")).describe("Downstream platform connections or grants required to use this format declaration. These are in addition to the single AdCP caller credential. Use this when a platform product requires multiple downstream grants, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.").optional(), "reference_mutability": z.enum(["immutable_snapshot","mutable_requires_reapproval","mutable_auto_recheck"]).describe("Policy for formats whose `slots` accept a `published_post` reference. `immutable_snapshot`: seller snapshots the referenced post at approval and later source changes do not change the served creative. `mutable_requires_reapproval`: the source post may change and material changes require review before continued serving. `mutable_auto_recheck`: the source post may change and the seller continuously or periodically rechecks authorization/policy without requiring buyer resubmission. Omit when the format has no `published_post` slot.").optional(), "production_window_business_days": z.number().int().gte(0).describe("Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative).").optional() }).catchall(z.any()).describe("Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).").describe("DAAST-tag-delivered audio creative (audio analog of VAST). Slot: `daast_tag` (daast asset, URL or inline XML). Tracking model: DAAST events inherent to the spec — `impression`, `firstQuartile`, `midpoint`, `thirdQuartile`, `complete`, `start`, `pause`, `resume`, `mute`, `unmute`, `clickTracking`, `error`. Distinct from `audio_hosted` (direct file with external tracking).") }), z.object({ "format_kind": z.literal("sponsored_placement"), "params": z.intersection(z.object({ "experimental": z.boolean().describe("When true, this canonical or seller narrowing may not work as declared. Adopters SHOULD preflight it with validate_input or in a sandbox and SHOULD NOT route production budget without testing; experimental status never makes the deprecated v1 path preferable. Drivers include unsettled spec shape, an adopter runtime gap, and custom shapes awaiting promotion. This replaces the earlier status plus runtime_status axes. Sellers SHOULD set experimental whenever a canonical or declaration is not production-ready.").default(false), "deprecated": z.boolean().describe("When true, this canonical (or a seller's specific narrowing of it) is going away. Existing adopters are supported through the deprecation cycle; new adoption is discouraged. Pair with `migration_target_version` to indicate when the canonical is expected to be removed. Distinct from `experimental`: an experimental canonical may stabilize and stop being experimental; a deprecated canonical is on a sunset path.").default(false), "v1_translatable": z.boolean().describe("Whether this canonical has any v1 named-format equivalent. `true` (default) — the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1→v2 projection via `v1-canonical-mapping.json` is meaningful. `false` — the canonical is inherently new in v2 and has no v1 form; v1's `list_creative_formats` couldn't express it because the underlying concept (algorithmic surface composition, AI-surface mentions, retail-media catalog placements, multi-card carousels) didn't exist as a v1 named-format archetype.\n\nLets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The six inherently-v2 canonicals in 3.2 are `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`, `seller_rendered_stateful_display`, and `coordinated_placements`.").default(true), "since_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version that introduced this canonical (e.g., '3.1', '3.2'). Lets adopters reason about minimum protocol version requirements when consuming a format declaration. Patch precision is intentionally rejected — canonicals are introduced at minor-version boundaries.").optional(), "migration_target_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version by which the working group expects this canonical to stabilize, surface a breaking revision, or (when `deprecated: true`) be removed. Patch precision is intentionally rejected — canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown').").optional(), "composition_model": z.enum(["deterministic","algorithmic"]).describe("Whether the surface composes deterministically (buyer can predict per-slot rendering — sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing — responsive_creative, agent_placement).").optional(), "provenance_required": z.boolean().describe("When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent.").optional(), "platform_extensions": z.array(z.object({ "uri": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL identifying the extension. `https://` is mandatory — `http://`, `file://`, `data:`, and other schemes are rejected at the schema layer (defense-in-depth on top of the fetch-contract normative rules). The URI base is the owning agent's URL; the path identifies the extension within that agent. Example: 'https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel'. The full fetch contract — SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds — is documented on `product-format-declaration.json#format_schema` and applies to ALL fetches of this reference shape regardless of whether the field is named `format_schema` (load-bearing for validation) or `platform_extensions` (informational); the *transport* rules are identical, only the *consumption* semantics differ."), "digest": z.string().regex(new RegExp("^sha256:[a-f0-9]{64}$")).describe("SHA-256 content digest of the extension definition (sha256:). Used to detect drift — if the agent revises the extension, the digest changes and cached definitions become invalid.") }).catchall(z.any()).describe("Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` — divergent digests in one response indicate producer-side error (e.g., concurrent extension revision mid-render). Buyers encountering divergent digests for the same URI MUST fail closed: treat all references to that URI as unresolved and surface a validation error rather than picking one branch silently. **Across responses**, digest divergence is normal — extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.")).describe("Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\n\n**Collision precedence (normative).** When two or more `platform_extensions[]` entries on the same declaration extend the same target (e.g., both extend `tracking`) with overlapping field names, **array order is authoritative — later entries override earlier ones on a per-field basis** (last-in-array-wins). SDKs MUST surface the overlap via the `errors[]` array on the `get_products` response with a structured code (`FORMAT_DECLARATION_DIVERGENT` is appropriate when the overlap appears across dual-emitted shapes; a producer-self-emitted overlap on a single declaration SHOULD use the same code with `error.details: { collision_kind: \"platform_extension_field\", target, overlapping_fields, winning_extension_uri }`). Producers SHOULD avoid the collision by emitting one extension per target or by partitioning fields across extensions; the deterministic precedence is for last-resort consistency across SDK implementations, not a sanctioned merging strategy.").optional(), "synthesis_nondeterministic": z.boolean().describe("When true, the format's production pipeline is genuinely nondeterministic — the platform cannot guarantee that synthesis from a given input set produces in-spec output. Veo / Sora / Runway-class generative video, and other AI-synthesis flows where output dimensions, duration, or quality vary per run. Implies a different validation contract: predictive `validate_input` is impossible; the platform's own post-synthesis QA loop applies; if the QA loop exhausts without producing a valid artifact, `build_creative` returns task_failed with a synthesis_failed reason. Distinct from `composition_model` (which describes how the surface composes per-slot rendering, not whether synthesis is deterministic). When false or absent, the format's production is predictable enough that `validate_input` can predict output properties from input properties.\n\n**Compatibility with `asset_source` / `item_production_model`**: `synthesis_nondeterministic: true` MAY pair with any of `seller_pre_rendered_from_brief`, `seller_human_designed`, or `agent_synthesized` (the QA loop is concept-level, not source-specific — 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.").default(false), "slots": z.array(z.object({ "asset_group_id": z.string().describe("Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings."), "asset_type": z.enum(["image","video","audio","text","markdown","url","html","css","javascript","vast","daast","webhook","brief","catalog","published_post","zip","card","object","pixel_tracker","vast_tracker","daast_tracker"]).describe("Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `published_post` is an existing-post reference asset, not uploaded media bytes and not a catalog row. `card` is the multi-card carousel element type (see card-asset.json). `pixel_tracker` / `vast_tracker` / `daast_tracker` are the renderer-fired measurement-tracker primitives — see `/schemas/core/assets/pixel-tracker-asset.json` and the VAST / DAAST tracker schemas. `object` is a last-resort fallback for structured non-asset inputs that don't fit any primitive asset_type — prefer specific types whenever possible."), "required": z.boolean().describe("Whether this slot is required for a valid manifest.").default(false), "min": z.number().int().gte(0).describe("Minimum count for repeatable / pool slots.").optional(), "max": z.number().int().gte(1).describe("Maximum count for repeatable / pool slots.").optional(), "max_chars": z.number().int().gte(1).describe("Per-slot character limit. Valid only when `asset_type` is `text`, `markdown`, or `brief`. Mutually exclusive with `max_size_kb` (which applies to binary asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "max_size_kb": z.number().int().gte(1).describe("Per-slot file size limit in kilobytes. Valid only when `asset_type` is `image`, `video`, `audio`, or `zip`. Mutually exclusive with `max_chars` (which applies to text asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Accepted intrinsic-pixel densities for this image-bearing slot. Valid when `asset_type` is `image`, and on a `card` slot where it constrains each card's image media (video media is unaffected). This makes density available to every canonical carrying image assets (native, carousel, responsive, companion images, and image itself), not only `format_kind: image`. When the image canonical also declares top-level `params.pixel_ratios`, the effective set is the intersection; an empty intersection is invalid. One matching asset satisfies the slot unless `required_pixel_ratios` requires rendition coverage.").optional(), "required_pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Required density coverage for an image rendition set. Valid only when `asset_type` is `image` and `pixel_ratios` is also declared. Every value MUST appear in the effective accepted set after intersecting any top-level image `params.pixel_ratios`, and the manifest slot value MUST be an array containing exactly one matching image rendition for each required ratio. Other accepted ratios remain optional. For example, `pixel_ratios: [1, 1.5, 2]` with `required_pixel_ratios: [1, 2]` requires the 1x and 2x renditions while making 1.5x optional. SDKs enforce intersection, subset, coverage, and duplicate-ratio rules because JSON Schema draft-07 cannot express them generically.").optional(), "logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("When `asset_group_id` is `logo`, renderer-facing brand.json logo slots acceptable for this format slot. Producers selecting from brand.json SHOULD prefer `logos[]` entries whose `slots[]` intersects this list, then apply `visual_guidelines.logo_usage_rules[]`.").optional(), "required_logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Subset of `logo_slots` for which this format expects explicit logo coverage. A manifest or brand-derived logo pool SHOULD include at least one usable logo for each required slot; if coverage is missing, builders SHOULD surface a validation warning or approval mapping instead of guessing from prose.").optional(), "description": z.string().describe("Human-readable description of what the slot expects from the buyer.").optional(), "consumed_for_production": z.boolean().describe("Dispatch hint for `build_creative` and v1↔v2 wire translators: when `true`, the slot's value is consumed as INPUT to a production step (host-read script, brief copy fed to generative synthesis, catalog feed driving per-SKU rendering) and is not rendered verbatim. When `false` (default), the slot's value is rendered verbatim on the placement (image bytes, video file, display tag).\n\nMotivates the v1↔v2 dispatch table: pre-v2 buyers shipped production-consumed inputs separately in a `inputs` map on the build_creative request; v2 collapses inputs and rendered assets into a single `assets` map keyed by `asset_group_id`. SDK translators between v1 and v2 use this flag per canonical to know which assets in the v2 manifest map back to v1 `inputs` vs v1 `assets`. Without the per-slot flag the dispatch table lives in adopter code and every SDK gets it slightly different.\n\nProducers SHOULD set this explicitly on slots whose consumption pattern isn't obvious (host-read scripts on `audio_hosted`, briefs on generative `video_hosted`, catalog feeds on `sponsored_placement`). For canonicals where every slot is render-verbatim (`image`, `display_tag`, `video_vast`), the default `false` is sufficient and the flag MAY be omitted.").default(false) }).catchall(z.any()).and(z.intersection(z.intersection(z.any(), z.intersection(z.any(), z.any())), z.intersection(z.any(), z.intersection(z.any(), z.any()))))).describe("Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.").optional(), "required_connections": z.array(z.object({ "provider": z.string().describe("Stable provider or platform namespace, preferably lowercase. Examples: `social.example`, `shortvideo.example`, or a seller-defined namespace. Omit only when the requirement is provider-agnostic, or when an `authorization_url` fully routes the human to the correct provider-specific connection flow.").optional(), "connection_type": z.enum(["advertiser_account","publisher_identity","post_authorization"]).describe("Kind of downstream connection required. `advertiser_account` is the platform account used to buy/manage ads. `publisher_identity` is the creator, page, channel, organization, or profile that owns source posts. `post_authorization` is a post-scoped grant when the platform authorizes individual posts instead of, or in addition to, the owning identity."), "required_for": z.array(z.string().min(1)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Concrete AdCP protocol operation names that require this downstream connection. Sellers SHOULD include this in product declarations when the requirement is known ahead of time, and in AUTHORIZATION_REQUIRED details when it explains the failed operation. Prefer specific operation names such as `list_creatives`, `sync_creatives`, `create_media_buy`, `get_media_buy_delivery`, or `get_creative_delivery` over broad category labels such as `reporting`.").optional(), "scope": z.enum(["account","identity","post","unknown"]).describe("Granularity of the downstream grant.").optional(), "status": z.enum(["connected","missing","pending","expired","revoked","not_required","unknown"]).describe("Current seller-observed state for this downstream connection when known. Product declarations MAY omit status or use `unknown`; AUTHORIZATION_REQUIRED details SHOULD use `missing`, `expired`, or `revoked` for the connection that blocked the call.").optional(), "connection_id": z.string().describe("Seller-defined identifier for an already-created downstream connection. Omit when no connection exists yet or when exposing it would leak platform/account state.").optional(), "resource_ref": z.object({ "platform_account_id": z.string().describe("Provider-native advertiser or business account id, when safe to disclose.").optional(), "identity_id": z.string().describe("Provider-native creator, page, channel, organization, or profile id, when safe to disclose.").optional(), "handle": z.string().describe("Provider-native public handle for the owning identity, when available.").optional(), "profile_url": z.string().url().describe("Public URL for the owning identity, when available.").optional(), "post_id": z.string().describe("Provider-native post id, when the grant is post-scoped or the failed request referenced a specific post.").optional(), "post_url": z.string().url().describe("Public URL for the referenced post, when available.").optional() }).catchall(z.any()).describe("Optional opaque provider-native resource hint, such as a platform account id, profile URL, handle, channel id, post id, or post URL. This is a hint for routing authorization, not proof that authorization exists.").optional(), "authorization_url": z.string().url().describe("Seller-hosted or provider-hosted URL where a human can complete or restore this downstream connection.").optional(), "authorization_instructions": z.string().describe("Human-readable instructions for completing or restoring this downstream connection.").optional(), "expires_at": z.string().datetime().describe("Expiration time for the downstream grant, when known.").optional() }).catchall(z.any()).and(z.any()).describe("A seller/platform-side connection or grant required by a product, format, or request. This is not the AdCP caller credential: the AdCP request is still authenticated once, and the seller uses these stored downstream connections to call a platform or service on the buyer's behalf. Use this shape for platforms that require more than one downstream grant, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.")).describe("Downstream platform connections or grants required to use this format declaration. These are in addition to the single AdCP caller credential. Use this when a platform product requires multiple downstream grants, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.").optional(), "reference_mutability": z.enum(["immutable_snapshot","mutable_requires_reapproval","mutable_auto_recheck"]).describe("Policy for formats whose `slots` accept a `published_post` reference. `immutable_snapshot`: seller snapshots the referenced post at approval and later source changes do not change the served creative. `mutable_requires_reapproval`: the source post may change and material changes require review before continued serving. `mutable_auto_recheck`: the source post may change and the seller continuously or periodically rechecks authorization/policy without requiring buyer resubmission. Omit when the format has no `published_post` slot.").optional(), "production_window_business_days": z.number().int().gte(0).describe("Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative).").optional() }).catchall(z.any()).describe("Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints)."), z.any()).describe("Catalog-driven retail-media format. Slot: `source_catalog` (catalog asset — product/SKU/ASIN/GTIN catalog reference, REQUIRED), optional `hero_asset`, optional `landing_page_url`. Buyer supplies the catalog reference; surface composes per-item or multi-item rendering using its native placement template. **Composition is deterministic** — buyer can predict per-slot rendering from the catalog item structure. Tracking model: per-item impression + click + conversion (catalog-keyed via offering_id/sku/gtin macros). Covers Amazon Sponsored Products, Criteo Sponsored Products, CitrusAd Sponsored Products, Walmart Connect Sponsored Products, Pinterest Collection (catalog-driven mode).\n\n**Scope (normative — buyer-agent routing).** This canonical is the home for catalog-driven retail-media placements ONLY. The defining feature is the `source_catalog` slot — products under this canonical compose their creative *per catalog item* using the buyer-supplied catalog feed. Without a catalog feed there is nothing to render against. Buyer agents reading `format_kind: sponsored_placement` MUST attach a catalog reference; sellers MUST require `source_catalog` in the manifest.\n\n**Not this canonical (route elsewhere):**\n- IAB in-feed native ads, content-recommendation widgets (Taboola, Outbrain, Yahoo Native, AdMob Native, in-feed sponsored cards) — use `native_in_feed` (asset-bundle composition; no catalog).\n- Algorithmic surface that picks from a buyer-supplied asset pool (Google PMax, Meta Advantage+) — use `responsive_creative`.\n- Single-image or single-video creative — use `image` or `video_hosted`.\n\nThe earlier broader framing ('any sponsored placement') was too loose for buyer-agent routing — a buyer reading `sponsored_placement` couldn't disambiguate a catalog-driven Amazon SP from an in-feed Taboola widget. As of 3.1, the canonical is narrowed to catalog-keyed retail-media; native moves to `native_in_feed`. Distinct from `responsive_creative` (algorithmic combinator from buyer pool) and `agent_placement` (text/audio AI-surface composition).") }), z.object({ "format_kind": z.literal("native_in_feed"), "params": z.intersection(z.object({ "experimental": z.boolean().describe("When true, this canonical or seller narrowing may not work as declared. Adopters SHOULD preflight it with validate_input or in a sandbox and SHOULD NOT route production budget without testing; experimental status never makes the deprecated v1 path preferable. Drivers include unsettled spec shape, an adopter runtime gap, and custom shapes awaiting promotion. This replaces the earlier status plus runtime_status axes. Sellers SHOULD set experimental whenever a canonical or declaration is not production-ready.").default(false), "deprecated": z.boolean().describe("When true, this canonical (or a seller's specific narrowing of it) is going away. Existing adopters are supported through the deprecation cycle; new adoption is discouraged. Pair with `migration_target_version` to indicate when the canonical is expected to be removed. Distinct from `experimental`: an experimental canonical may stabilize and stop being experimental; a deprecated canonical is on a sunset path.").default(false), "v1_translatable": z.boolean().describe("Whether this canonical has any v1 named-format equivalent. `true` (default) — the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1→v2 projection via `v1-canonical-mapping.json` is meaningful. `false` — the canonical is inherently new in v2 and has no v1 form; v1's `list_creative_formats` couldn't express it because the underlying concept (algorithmic surface composition, AI-surface mentions, retail-media catalog placements, multi-card carousels) didn't exist as a v1 named-format archetype.\n\nLets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The six inherently-v2 canonicals in 3.2 are `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`, `seller_rendered_stateful_display`, and `coordinated_placements`.").default(true), "since_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version that introduced this canonical (e.g., '3.1', '3.2'). Lets adopters reason about minimum protocol version requirements when consuming a format declaration. Patch precision is intentionally rejected — canonicals are introduced at minor-version boundaries.").optional(), "migration_target_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version by which the working group expects this canonical to stabilize, surface a breaking revision, or (when `deprecated: true`) be removed. Patch precision is intentionally rejected — canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown').").optional(), "composition_model": z.enum(["deterministic","algorithmic"]).describe("Whether the surface composes deterministically (buyer can predict per-slot rendering — sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing — responsive_creative, agent_placement).").optional(), "provenance_required": z.boolean().describe("When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent.").optional(), "platform_extensions": z.array(z.object({ "uri": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL identifying the extension. `https://` is mandatory — `http://`, `file://`, `data:`, and other schemes are rejected at the schema layer (defense-in-depth on top of the fetch-contract normative rules). The URI base is the owning agent's URL; the path identifies the extension within that agent. Example: 'https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel'. The full fetch contract — SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds — is documented on `product-format-declaration.json#format_schema` and applies to ALL fetches of this reference shape regardless of whether the field is named `format_schema` (load-bearing for validation) or `platform_extensions` (informational); the *transport* rules are identical, only the *consumption* semantics differ."), "digest": z.string().regex(new RegExp("^sha256:[a-f0-9]{64}$")).describe("SHA-256 content digest of the extension definition (sha256:). Used to detect drift — if the agent revises the extension, the digest changes and cached definitions become invalid.") }).catchall(z.any()).describe("Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` — divergent digests in one response indicate producer-side error (e.g., concurrent extension revision mid-render). Buyers encountering divergent digests for the same URI MUST fail closed: treat all references to that URI as unresolved and surface a validation error rather than picking one branch silently. **Across responses**, digest divergence is normal — extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.")).describe("Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\n\n**Collision precedence (normative).** When two or more `platform_extensions[]` entries on the same declaration extend the same target (e.g., both extend `tracking`) with overlapping field names, **array order is authoritative — later entries override earlier ones on a per-field basis** (last-in-array-wins). SDKs MUST surface the overlap via the `errors[]` array on the `get_products` response with a structured code (`FORMAT_DECLARATION_DIVERGENT` is appropriate when the overlap appears across dual-emitted shapes; a producer-self-emitted overlap on a single declaration SHOULD use the same code with `error.details: { collision_kind: \"platform_extension_field\", target, overlapping_fields, winning_extension_uri }`). Producers SHOULD avoid the collision by emitting one extension per target or by partitioning fields across extensions; the deterministic precedence is for last-resort consistency across SDK implementations, not a sanctioned merging strategy.").optional(), "synthesis_nondeterministic": z.boolean().describe("When true, the format's production pipeline is genuinely nondeterministic — the platform cannot guarantee that synthesis from a given input set produces in-spec output. Veo / Sora / Runway-class generative video, and other AI-synthesis flows where output dimensions, duration, or quality vary per run. Implies a different validation contract: predictive `validate_input` is impossible; the platform's own post-synthesis QA loop applies; if the QA loop exhausts without producing a valid artifact, `build_creative` returns task_failed with a synthesis_failed reason. Distinct from `composition_model` (which describes how the surface composes per-slot rendering, not whether synthesis is deterministic). When false or absent, the format's production is predictable enough that `validate_input` can predict output properties from input properties.\n\n**Compatibility with `asset_source` / `item_production_model`**: `synthesis_nondeterministic: true` MAY pair with any of `seller_pre_rendered_from_brief`, `seller_human_designed`, or `agent_synthesized` (the QA loop is concept-level, not source-specific — 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.").default(false), "slots": z.array(z.object({ "asset_group_id": z.string().describe("Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings."), "asset_type": z.enum(["image","video","audio","text","markdown","url","html","css","javascript","vast","daast","webhook","brief","catalog","published_post","zip","card","object","pixel_tracker","vast_tracker","daast_tracker"]).describe("Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `published_post` is an existing-post reference asset, not uploaded media bytes and not a catalog row. `card` is the multi-card carousel element type (see card-asset.json). `pixel_tracker` / `vast_tracker` / `daast_tracker` are the renderer-fired measurement-tracker primitives — see `/schemas/core/assets/pixel-tracker-asset.json` and the VAST / DAAST tracker schemas. `object` is a last-resort fallback for structured non-asset inputs that don't fit any primitive asset_type — prefer specific types whenever possible."), "required": z.boolean().describe("Whether this slot is required for a valid manifest.").default(false), "min": z.number().int().gte(0).describe("Minimum count for repeatable / pool slots.").optional(), "max": z.number().int().gte(1).describe("Maximum count for repeatable / pool slots.").optional(), "max_chars": z.number().int().gte(1).describe("Per-slot character limit. Valid only when `asset_type` is `text`, `markdown`, or `brief`. Mutually exclusive with `max_size_kb` (which applies to binary asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "max_size_kb": z.number().int().gte(1).describe("Per-slot file size limit in kilobytes. Valid only when `asset_type` is `image`, `video`, `audio`, or `zip`. Mutually exclusive with `max_chars` (which applies to text asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Accepted intrinsic-pixel densities for this image-bearing slot. Valid when `asset_type` is `image`, and on a `card` slot where it constrains each card's image media (video media is unaffected). This makes density available to every canonical carrying image assets (native, carousel, responsive, companion images, and image itself), not only `format_kind: image`. When the image canonical also declares top-level `params.pixel_ratios`, the effective set is the intersection; an empty intersection is invalid. One matching asset satisfies the slot unless `required_pixel_ratios` requires rendition coverage.").optional(), "required_pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Required density coverage for an image rendition set. Valid only when `asset_type` is `image` and `pixel_ratios` is also declared. Every value MUST appear in the effective accepted set after intersecting any top-level image `params.pixel_ratios`, and the manifest slot value MUST be an array containing exactly one matching image rendition for each required ratio. Other accepted ratios remain optional. For example, `pixel_ratios: [1, 1.5, 2]` with `required_pixel_ratios: [1, 2]` requires the 1x and 2x renditions while making 1.5x optional. SDKs enforce intersection, subset, coverage, and duplicate-ratio rules because JSON Schema draft-07 cannot express them generically.").optional(), "logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("When `asset_group_id` is `logo`, renderer-facing brand.json logo slots acceptable for this format slot. Producers selecting from brand.json SHOULD prefer `logos[]` entries whose `slots[]` intersects this list, then apply `visual_guidelines.logo_usage_rules[]`.").optional(), "required_logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Subset of `logo_slots` for which this format expects explicit logo coverage. A manifest or brand-derived logo pool SHOULD include at least one usable logo for each required slot; if coverage is missing, builders SHOULD surface a validation warning or approval mapping instead of guessing from prose.").optional(), "description": z.string().describe("Human-readable description of what the slot expects from the buyer.").optional(), "consumed_for_production": z.boolean().describe("Dispatch hint for `build_creative` and v1↔v2 wire translators: when `true`, the slot's value is consumed as INPUT to a production step (host-read script, brief copy fed to generative synthesis, catalog feed driving per-SKU rendering) and is not rendered verbatim. When `false` (default), the slot's value is rendered verbatim on the placement (image bytes, video file, display tag).\n\nMotivates the v1↔v2 dispatch table: pre-v2 buyers shipped production-consumed inputs separately in a `inputs` map on the build_creative request; v2 collapses inputs and rendered assets into a single `assets` map keyed by `asset_group_id`. SDK translators between v1 and v2 use this flag per canonical to know which assets in the v2 manifest map back to v1 `inputs` vs v1 `assets`. Without the per-slot flag the dispatch table lives in adopter code and every SDK gets it slightly different.\n\nProducers SHOULD set this explicitly on slots whose consumption pattern isn't obvious (host-read scripts on `audio_hosted`, briefs on generative `video_hosted`, catalog feeds on `sponsored_placement`). For canonicals where every slot is render-verbatim (`image`, `display_tag`, `video_vast`), the default `false` is sufficient and the flag MAY be omitted.").default(false) }).catchall(z.any()).and(z.intersection(z.intersection(z.any(), z.intersection(z.any(), z.any())), z.intersection(z.any(), z.intersection(z.any(), z.any()))))).describe("Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.").optional(), "required_connections": z.array(z.object({ "provider": z.string().describe("Stable provider or platform namespace, preferably lowercase. Examples: `social.example`, `shortvideo.example`, or a seller-defined namespace. Omit only when the requirement is provider-agnostic, or when an `authorization_url` fully routes the human to the correct provider-specific connection flow.").optional(), "connection_type": z.enum(["advertiser_account","publisher_identity","post_authorization"]).describe("Kind of downstream connection required. `advertiser_account` is the platform account used to buy/manage ads. `publisher_identity` is the creator, page, channel, organization, or profile that owns source posts. `post_authorization` is a post-scoped grant when the platform authorizes individual posts instead of, or in addition to, the owning identity."), "required_for": z.array(z.string().min(1)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Concrete AdCP protocol operation names that require this downstream connection. Sellers SHOULD include this in product declarations when the requirement is known ahead of time, and in AUTHORIZATION_REQUIRED details when it explains the failed operation. Prefer specific operation names such as `list_creatives`, `sync_creatives`, `create_media_buy`, `get_media_buy_delivery`, or `get_creative_delivery` over broad category labels such as `reporting`.").optional(), "scope": z.enum(["account","identity","post","unknown"]).describe("Granularity of the downstream grant.").optional(), "status": z.enum(["connected","missing","pending","expired","revoked","not_required","unknown"]).describe("Current seller-observed state for this downstream connection when known. Product declarations MAY omit status or use `unknown`; AUTHORIZATION_REQUIRED details SHOULD use `missing`, `expired`, or `revoked` for the connection that blocked the call.").optional(), "connection_id": z.string().describe("Seller-defined identifier for an already-created downstream connection. Omit when no connection exists yet or when exposing it would leak platform/account state.").optional(), "resource_ref": z.object({ "platform_account_id": z.string().describe("Provider-native advertiser or business account id, when safe to disclose.").optional(), "identity_id": z.string().describe("Provider-native creator, page, channel, organization, or profile id, when safe to disclose.").optional(), "handle": z.string().describe("Provider-native public handle for the owning identity, when available.").optional(), "profile_url": z.string().url().describe("Public URL for the owning identity, when available.").optional(), "post_id": z.string().describe("Provider-native post id, when the grant is post-scoped or the failed request referenced a specific post.").optional(), "post_url": z.string().url().describe("Public URL for the referenced post, when available.").optional() }).catchall(z.any()).describe("Optional opaque provider-native resource hint, such as a platform account id, profile URL, handle, channel id, post id, or post URL. This is a hint for routing authorization, not proof that authorization exists.").optional(), "authorization_url": z.string().url().describe("Seller-hosted or provider-hosted URL where a human can complete or restore this downstream connection.").optional(), "authorization_instructions": z.string().describe("Human-readable instructions for completing or restoring this downstream connection.").optional(), "expires_at": z.string().datetime().describe("Expiration time for the downstream grant, when known.").optional() }).catchall(z.any()).and(z.any()).describe("A seller/platform-side connection or grant required by a product, format, or request. This is not the AdCP caller credential: the AdCP request is still authenticated once, and the seller uses these stored downstream connections to call a platform or service on the buyer's behalf. Use this shape for platforms that require more than one downstream grant, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.")).describe("Downstream platform connections or grants required to use this format declaration. These are in addition to the single AdCP caller credential. Use this when a platform product requires multiple downstream grants, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.").optional(), "reference_mutability": z.enum(["immutable_snapshot","mutable_requires_reapproval","mutable_auto_recheck"]).describe("Policy for formats whose `slots` accept a `published_post` reference. `immutable_snapshot`: seller snapshots the referenced post at approval and later source changes do not change the served creative. `mutable_requires_reapproval`: the source post may change and material changes require review before continued serving. `mutable_auto_recheck`: the source post may change and the seller continuously or periodically rechecks authorization/policy without requiring buyer resubmission. Omit when the format has no `published_post` slot.").optional(), "production_window_business_days": z.number().int().gte(0).describe("Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative).").optional() }).catchall(z.any()).describe("Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints)."), z.intersection(z.any(), z.any())).describe("IAB-shaped native creative for in-feed and content-recommendation surfaces. Default slots cover the primary IAB OpenRTB Native 1.2 asset types — `title` (Title Asset), `body_text` (Data Asset type 2), `main_image` (Image Asset main), `icon` (Image Asset icon), `cta` (Data Asset type 12), `advertiser_name` (Data Asset type 1), `sponsored_label` (Title-adjacent), `landing_page_url` (Link Asset), `display_url` (Data Asset type 11 — visible URL/domain, distinct from clickthrough), `rating` (Data Asset type 3 — app/product rating), `price` (Data Asset type 6 — product price), plus renderer-fired `impression_tracker` / `viewability_tracker` / `click_tracker` (`pixel_tracker`). Products MAY use `slots_override` to add other IAB Native data asset types (likes — type 4, downloads — type 5, saleprice — type 7, phone_number — type 8, address — type 9, desc2 — type 10, etc.) or to remove slots the surface doesn't render. The publisher's renderer assembles these into its own look-and-feel — feed card, content-recommendation slot, in-stream native unit. Buyer ships a single asset bundle; the surface chooses presentation.\n\n**Scope (normative — buyer-agent routing).** This canonical is the home for:\n- IAB OpenRTB Native 1.2 in-feed native ads (publisher feeds, app feeds)\n- Content-recommendation widgets (Taboola, Outbrain, Yahoo Recommendations)\n- AdMob Native / Yahoo Native publisher slots\n- In-feed sponsored placements without catalog dependency\n\n**Not this canonical:**\n- Catalog-driven retail-media (Amazon SP, Criteo SP, CitrusAd SP) — use `sponsored_placement` (requires `source_catalog`).\n- Algorithmic surface that picks from a buyer-supplied asset pool (Google PMax, Meta Advantage+) — use `responsive_creative`.\n- Multi-card carousel — use `image_carousel`.\n- Video-first native units where the asset is a hosted video file — use `video_hosted` with `applies_to_channels: [\"native\"]`. (Distinct from the CTV menu profile: a menu hero remains this canonical because the platform assembles the full asset bundle and the video rides the Native 1.2 `vasttag` video asset, playing on focus rather than being the unit itself.)\n\nDistinct from `sponsored_placement` along the catalog axis: native_in_feed is asset-bundle composition; sponsored_placement is catalog-row composition. A buyer agent reading `format_kind: native_in_feed` knows to assemble title + image + body + CTA; reading `format_kind: sponsored_placement` knows to attach a catalog feed.") }), z.object({ "format_kind": z.literal("responsive_creative"), "params": z.object({ "experimental": z.boolean().describe("When true, this canonical or seller narrowing may not work as declared. Adopters SHOULD preflight it with validate_input or in a sandbox and SHOULD NOT route production budget without testing; experimental status never makes the deprecated v1 path preferable. Drivers include unsettled spec shape, an adopter runtime gap, and custom shapes awaiting promotion. This replaces the earlier status plus runtime_status axes. Sellers SHOULD set experimental whenever a canonical or declaration is not production-ready.").default(false), "deprecated": z.boolean().describe("When true, this canonical (or a seller's specific narrowing of it) is going away. Existing adopters are supported through the deprecation cycle; new adoption is discouraged. Pair with `migration_target_version` to indicate when the canonical is expected to be removed. Distinct from `experimental`: an experimental canonical may stabilize and stop being experimental; a deprecated canonical is on a sunset path.").default(false), "v1_translatable": z.boolean().describe("Whether this canonical has any v1 named-format equivalent. `true` (default) — the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1→v2 projection via `v1-canonical-mapping.json` is meaningful. `false` — the canonical is inherently new in v2 and has no v1 form; v1's `list_creative_formats` couldn't express it because the underlying concept (algorithmic surface composition, AI-surface mentions, retail-media catalog placements, multi-card carousels) didn't exist as a v1 named-format archetype.\n\nLets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The six inherently-v2 canonicals in 3.2 are `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`, `seller_rendered_stateful_display`, and `coordinated_placements`.").default(true), "since_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version that introduced this canonical (e.g., '3.1', '3.2'). Lets adopters reason about minimum protocol version requirements when consuming a format declaration. Patch precision is intentionally rejected — canonicals are introduced at minor-version boundaries.").optional(), "migration_target_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version by which the working group expects this canonical to stabilize, surface a breaking revision, or (when `deprecated: true`) be removed. Patch precision is intentionally rejected — canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown').").optional(), "composition_model": z.enum(["deterministic","algorithmic"]).describe("Whether the surface composes deterministically (buyer can predict per-slot rendering — sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing — responsive_creative, agent_placement).").optional(), "provenance_required": z.boolean().describe("When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent.").optional(), "platform_extensions": z.array(z.object({ "uri": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL identifying the extension. `https://` is mandatory — `http://`, `file://`, `data:`, and other schemes are rejected at the schema layer (defense-in-depth on top of the fetch-contract normative rules). The URI base is the owning agent's URL; the path identifies the extension within that agent. Example: 'https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel'. The full fetch contract — SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds — is documented on `product-format-declaration.json#format_schema` and applies to ALL fetches of this reference shape regardless of whether the field is named `format_schema` (load-bearing for validation) or `platform_extensions` (informational); the *transport* rules are identical, only the *consumption* semantics differ."), "digest": z.string().regex(new RegExp("^sha256:[a-f0-9]{64}$")).describe("SHA-256 content digest of the extension definition (sha256:). Used to detect drift — if the agent revises the extension, the digest changes and cached definitions become invalid.") }).catchall(z.any()).describe("Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` — divergent digests in one response indicate producer-side error (e.g., concurrent extension revision mid-render). Buyers encountering divergent digests for the same URI MUST fail closed: treat all references to that URI as unresolved and surface a validation error rather than picking one branch silently. **Across responses**, digest divergence is normal — extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.")).describe("Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\n\n**Collision precedence (normative).** When two or more `platform_extensions[]` entries on the same declaration extend the same target (e.g., both extend `tracking`) with overlapping field names, **array order is authoritative — later entries override earlier ones on a per-field basis** (last-in-array-wins). SDKs MUST surface the overlap via the `errors[]` array on the `get_products` response with a structured code (`FORMAT_DECLARATION_DIVERGENT` is appropriate when the overlap appears across dual-emitted shapes; a producer-self-emitted overlap on a single declaration SHOULD use the same code with `error.details: { collision_kind: \"platform_extension_field\", target, overlapping_fields, winning_extension_uri }`). Producers SHOULD avoid the collision by emitting one extension per target or by partitioning fields across extensions; the deterministic precedence is for last-resort consistency across SDK implementations, not a sanctioned merging strategy.").optional(), "synthesis_nondeterministic": z.boolean().describe("When true, the format's production pipeline is genuinely nondeterministic — the platform cannot guarantee that synthesis from a given input set produces in-spec output. Veo / Sora / Runway-class generative video, and other AI-synthesis flows where output dimensions, duration, or quality vary per run. Implies a different validation contract: predictive `validate_input` is impossible; the platform's own post-synthesis QA loop applies; if the QA loop exhausts without producing a valid artifact, `build_creative` returns task_failed with a synthesis_failed reason. Distinct from `composition_model` (which describes how the surface composes per-slot rendering, not whether synthesis is deterministic). When false or absent, the format's production is predictable enough that `validate_input` can predict output properties from input properties.\n\n**Compatibility with `asset_source` / `item_production_model`**: `synthesis_nondeterministic: true` MAY pair with any of `seller_pre_rendered_from_brief`, `seller_human_designed`, or `agent_synthesized` (the QA loop is concept-level, not source-specific — 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.").default(false), "slots": z.array(z.object({ "asset_group_id": z.string().describe("Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings."), "asset_type": z.enum(["image","video","audio","text","markdown","url","html","css","javascript","vast","daast","webhook","brief","catalog","published_post","zip","card","object","pixel_tracker","vast_tracker","daast_tracker"]).describe("Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `published_post` is an existing-post reference asset, not uploaded media bytes and not a catalog row. `card` is the multi-card carousel element type (see card-asset.json). `pixel_tracker` / `vast_tracker` / `daast_tracker` are the renderer-fired measurement-tracker primitives — see `/schemas/core/assets/pixel-tracker-asset.json` and the VAST / DAAST tracker schemas. `object` is a last-resort fallback for structured non-asset inputs that don't fit any primitive asset_type — prefer specific types whenever possible."), "required": z.boolean().describe("Whether this slot is required for a valid manifest.").default(false), "min": z.number().int().gte(0).describe("Minimum count for repeatable / pool slots.").optional(), "max": z.number().int().gte(1).describe("Maximum count for repeatable / pool slots.").optional(), "max_chars": z.number().int().gte(1).describe("Per-slot character limit. Valid only when `asset_type` is `text`, `markdown`, or `brief`. Mutually exclusive with `max_size_kb` (which applies to binary asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "max_size_kb": z.number().int().gte(1).describe("Per-slot file size limit in kilobytes. Valid only when `asset_type` is `image`, `video`, `audio`, or `zip`. Mutually exclusive with `max_chars` (which applies to text asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Accepted intrinsic-pixel densities for this image-bearing slot. Valid when `asset_type` is `image`, and on a `card` slot where it constrains each card's image media (video media is unaffected). This makes density available to every canonical carrying image assets (native, carousel, responsive, companion images, and image itself), not only `format_kind: image`. When the image canonical also declares top-level `params.pixel_ratios`, the effective set is the intersection; an empty intersection is invalid. One matching asset satisfies the slot unless `required_pixel_ratios` requires rendition coverage.").optional(), "required_pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Required density coverage for an image rendition set. Valid only when `asset_type` is `image` and `pixel_ratios` is also declared. Every value MUST appear in the effective accepted set after intersecting any top-level image `params.pixel_ratios`, and the manifest slot value MUST be an array containing exactly one matching image rendition for each required ratio. Other accepted ratios remain optional. For example, `pixel_ratios: [1, 1.5, 2]` with `required_pixel_ratios: [1, 2]` requires the 1x and 2x renditions while making 1.5x optional. SDKs enforce intersection, subset, coverage, and duplicate-ratio rules because JSON Schema draft-07 cannot express them generically.").optional(), "logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("When `asset_group_id` is `logo`, renderer-facing brand.json logo slots acceptable for this format slot. Producers selecting from brand.json SHOULD prefer `logos[]` entries whose `slots[]` intersects this list, then apply `visual_guidelines.logo_usage_rules[]`.").optional(), "required_logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Subset of `logo_slots` for which this format expects explicit logo coverage. A manifest or brand-derived logo pool SHOULD include at least one usable logo for each required slot; if coverage is missing, builders SHOULD surface a validation warning or approval mapping instead of guessing from prose.").optional(), "description": z.string().describe("Human-readable description of what the slot expects from the buyer.").optional(), "consumed_for_production": z.boolean().describe("Dispatch hint for `build_creative` and v1↔v2 wire translators: when `true`, the slot's value is consumed as INPUT to a production step (host-read script, brief copy fed to generative synthesis, catalog feed driving per-SKU rendering) and is not rendered verbatim. When `false` (default), the slot's value is rendered verbatim on the placement (image bytes, video file, display tag).\n\nMotivates the v1↔v2 dispatch table: pre-v2 buyers shipped production-consumed inputs separately in a `inputs` map on the build_creative request; v2 collapses inputs and rendered assets into a single `assets` map keyed by `asset_group_id`. SDK translators between v1 and v2 use this flag per canonical to know which assets in the v2 manifest map back to v1 `inputs` vs v1 `assets`. Without the per-slot flag the dispatch table lives in adopter code and every SDK gets it slightly different.\n\nProducers SHOULD set this explicitly on slots whose consumption pattern isn't obvious (host-read scripts on `audio_hosted`, briefs on generative `video_hosted`, catalog feeds on `sponsored_placement`). For canonicals where every slot is render-verbatim (`image`, `display_tag`, `video_vast`), the default `false` is sufficient and the flag MAY be omitted.").default(false) }).catchall(z.any()).and(z.intersection(z.intersection(z.any(), z.intersection(z.any(), z.any())), z.intersection(z.any(), z.intersection(z.any(), z.any()))))).describe("Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.").optional(), "required_connections": z.array(z.object({ "provider": z.string().describe("Stable provider or platform namespace, preferably lowercase. Examples: `social.example`, `shortvideo.example`, or a seller-defined namespace. Omit only when the requirement is provider-agnostic, or when an `authorization_url` fully routes the human to the correct provider-specific connection flow.").optional(), "connection_type": z.enum(["advertiser_account","publisher_identity","post_authorization"]).describe("Kind of downstream connection required. `advertiser_account` is the platform account used to buy/manage ads. `publisher_identity` is the creator, page, channel, organization, or profile that owns source posts. `post_authorization` is a post-scoped grant when the platform authorizes individual posts instead of, or in addition to, the owning identity."), "required_for": z.array(z.string().min(1)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Concrete AdCP protocol operation names that require this downstream connection. Sellers SHOULD include this in product declarations when the requirement is known ahead of time, and in AUTHORIZATION_REQUIRED details when it explains the failed operation. Prefer specific operation names such as `list_creatives`, `sync_creatives`, `create_media_buy`, `get_media_buy_delivery`, or `get_creative_delivery` over broad category labels such as `reporting`.").optional(), "scope": z.enum(["account","identity","post","unknown"]).describe("Granularity of the downstream grant.").optional(), "status": z.enum(["connected","missing","pending","expired","revoked","not_required","unknown"]).describe("Current seller-observed state for this downstream connection when known. Product declarations MAY omit status or use `unknown`; AUTHORIZATION_REQUIRED details SHOULD use `missing`, `expired`, or `revoked` for the connection that blocked the call.").optional(), "connection_id": z.string().describe("Seller-defined identifier for an already-created downstream connection. Omit when no connection exists yet or when exposing it would leak platform/account state.").optional(), "resource_ref": z.object({ "platform_account_id": z.string().describe("Provider-native advertiser or business account id, when safe to disclose.").optional(), "identity_id": z.string().describe("Provider-native creator, page, channel, organization, or profile id, when safe to disclose.").optional(), "handle": z.string().describe("Provider-native public handle for the owning identity, when available.").optional(), "profile_url": z.string().url().describe("Public URL for the owning identity, when available.").optional(), "post_id": z.string().describe("Provider-native post id, when the grant is post-scoped or the failed request referenced a specific post.").optional(), "post_url": z.string().url().describe("Public URL for the referenced post, when available.").optional() }).catchall(z.any()).describe("Optional opaque provider-native resource hint, such as a platform account id, profile URL, handle, channel id, post id, or post URL. This is a hint for routing authorization, not proof that authorization exists.").optional(), "authorization_url": z.string().url().describe("Seller-hosted or provider-hosted URL where a human can complete or restore this downstream connection.").optional(), "authorization_instructions": z.string().describe("Human-readable instructions for completing or restoring this downstream connection.").optional(), "expires_at": z.string().datetime().describe("Expiration time for the downstream grant, when known.").optional() }).catchall(z.any()).and(z.any()).describe("A seller/platform-side connection or grant required by a product, format, or request. This is not the AdCP caller credential: the AdCP request is still authenticated once, and the seller uses these stored downstream connections to call a platform or service on the buyer's behalf. Use this shape for platforms that require more than one downstream grant, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.")).describe("Downstream platform connections or grants required to use this format declaration. These are in addition to the single AdCP caller credential. Use this when a platform product requires multiple downstream grants, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.").optional(), "reference_mutability": z.enum(["immutable_snapshot","mutable_requires_reapproval","mutable_auto_recheck"]).describe("Policy for formats whose `slots` accept a `published_post` reference. `immutable_snapshot`: seller snapshots the referenced post at approval and later source changes do not change the served creative. `mutable_requires_reapproval`: the source post may change and material changes require review before continued serving. `mutable_auto_recheck`: the source post may change and the seller continuously or periodically rechecks authorization/policy without requiring buyer resubmission. Omit when the format has no `published_post` slot.").optional(), "production_window_business_days": z.number().int().gte(0).describe("Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative).").optional() }).catchall(z.any()).describe("Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).").describe("Buyer supplies a pool of typed assets (multiple headlines, descriptions, images, videos, logos); the surface algorithmically composes combinations per placement. **Composition is algorithmic** — surface picks combinations and reports per-asset performance breakdowns. Covers Google Responsive Display Ads (RDA), Responsive Search Ads (RSA), Performance Max (PMax), Demand Gen, and Meta Advantage+ creative. Industry term: \"Responsive\" (Google) / \"Advantage+ creative\" (Meta) / \"Dynamic Creative\" (older Meta term). Distinct from `sponsored_placement` (catalog-driven, deterministic) and `agent_placement` (AI-surface composition). The structured `slots` field below enumerates expected canonical asset_group_id slots; per-slot count/length narrowing lives in flat parameters (`headlines_min`, `headline_max_chars`, etc.).") }), z.object({ "format_kind": z.literal("agent_placement"), "params": z.object({ "experimental": z.boolean().describe("When true, this canonical or seller narrowing may not work as declared. Adopters SHOULD preflight it with validate_input or in a sandbox and SHOULD NOT route production budget without testing; experimental status never makes the deprecated v1 path preferable. Drivers include unsettled spec shape, an adopter runtime gap, and custom shapes awaiting promotion. This replaces the earlier status plus runtime_status axes. Sellers SHOULD set experimental whenever a canonical or declaration is not production-ready.").default(false), "deprecated": z.boolean().describe("When true, this canonical (or a seller's specific narrowing of it) is going away. Existing adopters are supported through the deprecation cycle; new adoption is discouraged. Pair with `migration_target_version` to indicate when the canonical is expected to be removed. Distinct from `experimental`: an experimental canonical may stabilize and stop being experimental; a deprecated canonical is on a sunset path.").default(false), "v1_translatable": z.boolean().describe("Whether this canonical has any v1 named-format equivalent. `true` (default) — the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1→v2 projection via `v1-canonical-mapping.json` is meaningful. `false` — the canonical is inherently new in v2 and has no v1 form; v1's `list_creative_formats` couldn't express it because the underlying concept (algorithmic surface composition, AI-surface mentions, retail-media catalog placements, multi-card carousels) didn't exist as a v1 named-format archetype.\n\nLets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The six inherently-v2 canonicals in 3.2 are `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`, `seller_rendered_stateful_display`, and `coordinated_placements`.").default(true), "since_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version that introduced this canonical (e.g., '3.1', '3.2'). Lets adopters reason about minimum protocol version requirements when consuming a format declaration. Patch precision is intentionally rejected — canonicals are introduced at minor-version boundaries.").optional(), "migration_target_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version by which the working group expects this canonical to stabilize, surface a breaking revision, or (when `deprecated: true`) be removed. Patch precision is intentionally rejected — canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown').").optional(), "composition_model": z.enum(["deterministic","algorithmic"]).describe("Whether the surface composes deterministically (buyer can predict per-slot rendering — sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing — responsive_creative, agent_placement).").optional(), "provenance_required": z.boolean().describe("When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent.").optional(), "platform_extensions": z.array(z.object({ "uri": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL identifying the extension. `https://` is mandatory — `http://`, `file://`, `data:`, and other schemes are rejected at the schema layer (defense-in-depth on top of the fetch-contract normative rules). The URI base is the owning agent's URL; the path identifies the extension within that agent. Example: 'https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel'. The full fetch contract — SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds — is documented on `product-format-declaration.json#format_schema` and applies to ALL fetches of this reference shape regardless of whether the field is named `format_schema` (load-bearing for validation) or `platform_extensions` (informational); the *transport* rules are identical, only the *consumption* semantics differ."), "digest": z.string().regex(new RegExp("^sha256:[a-f0-9]{64}$")).describe("SHA-256 content digest of the extension definition (sha256:). Used to detect drift — if the agent revises the extension, the digest changes and cached definitions become invalid.") }).catchall(z.any()).describe("Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` — divergent digests in one response indicate producer-side error (e.g., concurrent extension revision mid-render). Buyers encountering divergent digests for the same URI MUST fail closed: treat all references to that URI as unresolved and surface a validation error rather than picking one branch silently. **Across responses**, digest divergence is normal — extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.")).describe("Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\n\n**Collision precedence (normative).** When two or more `platform_extensions[]` entries on the same declaration extend the same target (e.g., both extend `tracking`) with overlapping field names, **array order is authoritative — later entries override earlier ones on a per-field basis** (last-in-array-wins). SDKs MUST surface the overlap via the `errors[]` array on the `get_products` response with a structured code (`FORMAT_DECLARATION_DIVERGENT` is appropriate when the overlap appears across dual-emitted shapes; a producer-self-emitted overlap on a single declaration SHOULD use the same code with `error.details: { collision_kind: \"platform_extension_field\", target, overlapping_fields, winning_extension_uri }`). Producers SHOULD avoid the collision by emitting one extension per target or by partitioning fields across extensions; the deterministic precedence is for last-resort consistency across SDK implementations, not a sanctioned merging strategy.").optional(), "synthesis_nondeterministic": z.boolean().describe("When true, the format's production pipeline is genuinely nondeterministic — the platform cannot guarantee that synthesis from a given input set produces in-spec output. Veo / Sora / Runway-class generative video, and other AI-synthesis flows where output dimensions, duration, or quality vary per run. Implies a different validation contract: predictive `validate_input` is impossible; the platform's own post-synthesis QA loop applies; if the QA loop exhausts without producing a valid artifact, `build_creative` returns task_failed with a synthesis_failed reason. Distinct from `composition_model` (which describes how the surface composes per-slot rendering, not whether synthesis is deterministic). When false or absent, the format's production is predictable enough that `validate_input` can predict output properties from input properties.\n\n**Compatibility with `asset_source` / `item_production_model`**: `synthesis_nondeterministic: true` MAY pair with any of `seller_pre_rendered_from_brief`, `seller_human_designed`, or `agent_synthesized` (the QA loop is concept-level, not source-specific — 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.").default(false), "slots": z.array(z.object({ "asset_group_id": z.string().describe("Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings."), "asset_type": z.enum(["image","video","audio","text","markdown","url","html","css","javascript","vast","daast","webhook","brief","catalog","published_post","zip","card","object","pixel_tracker","vast_tracker","daast_tracker"]).describe("Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `published_post` is an existing-post reference asset, not uploaded media bytes and not a catalog row. `card` is the multi-card carousel element type (see card-asset.json). `pixel_tracker` / `vast_tracker` / `daast_tracker` are the renderer-fired measurement-tracker primitives — see `/schemas/core/assets/pixel-tracker-asset.json` and the VAST / DAAST tracker schemas. `object` is a last-resort fallback for structured non-asset inputs that don't fit any primitive asset_type — prefer specific types whenever possible."), "required": z.boolean().describe("Whether this slot is required for a valid manifest.").default(false), "min": z.number().int().gte(0).describe("Minimum count for repeatable / pool slots.").optional(), "max": z.number().int().gte(1).describe("Maximum count for repeatable / pool slots.").optional(), "max_chars": z.number().int().gte(1).describe("Per-slot character limit. Valid only when `asset_type` is `text`, `markdown`, or `brief`. Mutually exclusive with `max_size_kb` (which applies to binary asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "max_size_kb": z.number().int().gte(1).describe("Per-slot file size limit in kilobytes. Valid only when `asset_type` is `image`, `video`, `audio`, or `zip`. Mutually exclusive with `max_chars` (which applies to text asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Accepted intrinsic-pixel densities for this image-bearing slot. Valid when `asset_type` is `image`, and on a `card` slot where it constrains each card's image media (video media is unaffected). This makes density available to every canonical carrying image assets (native, carousel, responsive, companion images, and image itself), not only `format_kind: image`. When the image canonical also declares top-level `params.pixel_ratios`, the effective set is the intersection; an empty intersection is invalid. One matching asset satisfies the slot unless `required_pixel_ratios` requires rendition coverage.").optional(), "required_pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Required density coverage for an image rendition set. Valid only when `asset_type` is `image` and `pixel_ratios` is also declared. Every value MUST appear in the effective accepted set after intersecting any top-level image `params.pixel_ratios`, and the manifest slot value MUST be an array containing exactly one matching image rendition for each required ratio. Other accepted ratios remain optional. For example, `pixel_ratios: [1, 1.5, 2]` with `required_pixel_ratios: [1, 2]` requires the 1x and 2x renditions while making 1.5x optional. SDKs enforce intersection, subset, coverage, and duplicate-ratio rules because JSON Schema draft-07 cannot express them generically.").optional(), "logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("When `asset_group_id` is `logo`, renderer-facing brand.json logo slots acceptable for this format slot. Producers selecting from brand.json SHOULD prefer `logos[]` entries whose `slots[]` intersects this list, then apply `visual_guidelines.logo_usage_rules[]`.").optional(), "required_logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Subset of `logo_slots` for which this format expects explicit logo coverage. A manifest or brand-derived logo pool SHOULD include at least one usable logo for each required slot; if coverage is missing, builders SHOULD surface a validation warning or approval mapping instead of guessing from prose.").optional(), "description": z.string().describe("Human-readable description of what the slot expects from the buyer.").optional(), "consumed_for_production": z.boolean().describe("Dispatch hint for `build_creative` and v1↔v2 wire translators: when `true`, the slot's value is consumed as INPUT to a production step (host-read script, brief copy fed to generative synthesis, catalog feed driving per-SKU rendering) and is not rendered verbatim. When `false` (default), the slot's value is rendered verbatim on the placement (image bytes, video file, display tag).\n\nMotivates the v1↔v2 dispatch table: pre-v2 buyers shipped production-consumed inputs separately in a `inputs` map on the build_creative request; v2 collapses inputs and rendered assets into a single `assets` map keyed by `asset_group_id`. SDK translators between v1 and v2 use this flag per canonical to know which assets in the v2 manifest map back to v1 `inputs` vs v1 `assets`. Without the per-slot flag the dispatch table lives in adopter code and every SDK gets it slightly different.\n\nProducers SHOULD set this explicitly on slots whose consumption pattern isn't obvious (host-read scripts on `audio_hosted`, briefs on generative `video_hosted`, catalog feeds on `sponsored_placement`). For canonicals where every slot is render-verbatim (`image`, `display_tag`, `video_vast`), the default `false` is sufficient and the flag MAY be omitted.").default(false) }).catchall(z.any()).and(z.intersection(z.intersection(z.any(), z.intersection(z.any(), z.any())), z.intersection(z.any(), z.intersection(z.any(), z.any()))))).describe("Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.").optional(), "required_connections": z.array(z.object({ "provider": z.string().describe("Stable provider or platform namespace, preferably lowercase. Examples: `social.example`, `shortvideo.example`, or a seller-defined namespace. Omit only when the requirement is provider-agnostic, or when an `authorization_url` fully routes the human to the correct provider-specific connection flow.").optional(), "connection_type": z.enum(["advertiser_account","publisher_identity","post_authorization"]).describe("Kind of downstream connection required. `advertiser_account` is the platform account used to buy/manage ads. `publisher_identity` is the creator, page, channel, organization, or profile that owns source posts. `post_authorization` is a post-scoped grant when the platform authorizes individual posts instead of, or in addition to, the owning identity."), "required_for": z.array(z.string().min(1)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Concrete AdCP protocol operation names that require this downstream connection. Sellers SHOULD include this in product declarations when the requirement is known ahead of time, and in AUTHORIZATION_REQUIRED details when it explains the failed operation. Prefer specific operation names such as `list_creatives`, `sync_creatives`, `create_media_buy`, `get_media_buy_delivery`, or `get_creative_delivery` over broad category labels such as `reporting`.").optional(), "scope": z.enum(["account","identity","post","unknown"]).describe("Granularity of the downstream grant.").optional(), "status": z.enum(["connected","missing","pending","expired","revoked","not_required","unknown"]).describe("Current seller-observed state for this downstream connection when known. Product declarations MAY omit status or use `unknown`; AUTHORIZATION_REQUIRED details SHOULD use `missing`, `expired`, or `revoked` for the connection that blocked the call.").optional(), "connection_id": z.string().describe("Seller-defined identifier for an already-created downstream connection. Omit when no connection exists yet or when exposing it would leak platform/account state.").optional(), "resource_ref": z.object({ "platform_account_id": z.string().describe("Provider-native advertiser or business account id, when safe to disclose.").optional(), "identity_id": z.string().describe("Provider-native creator, page, channel, organization, or profile id, when safe to disclose.").optional(), "handle": z.string().describe("Provider-native public handle for the owning identity, when available.").optional(), "profile_url": z.string().url().describe("Public URL for the owning identity, when available.").optional(), "post_id": z.string().describe("Provider-native post id, when the grant is post-scoped or the failed request referenced a specific post.").optional(), "post_url": z.string().url().describe("Public URL for the referenced post, when available.").optional() }).catchall(z.any()).describe("Optional opaque provider-native resource hint, such as a platform account id, profile URL, handle, channel id, post id, or post URL. This is a hint for routing authorization, not proof that authorization exists.").optional(), "authorization_url": z.string().url().describe("Seller-hosted or provider-hosted URL where a human can complete or restore this downstream connection.").optional(), "authorization_instructions": z.string().describe("Human-readable instructions for completing or restoring this downstream connection.").optional(), "expires_at": z.string().datetime().describe("Expiration time for the downstream grant, when known.").optional() }).catchall(z.any()).and(z.any()).describe("A seller/platform-side connection or grant required by a product, format, or request. This is not the AdCP caller credential: the AdCP request is still authenticated once, and the seller uses these stored downstream connections to call a platform or service on the buyer's behalf. Use this shape for platforms that require more than one downstream grant, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.")).describe("Downstream platform connections or grants required to use this format declaration. These are in addition to the single AdCP caller credential. Use this when a platform product requires multiple downstream grants, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.").optional(), "reference_mutability": z.enum(["immutable_snapshot","mutable_requires_reapproval","mutable_auto_recheck"]).describe("Policy for formats whose `slots` accept a `published_post` reference. `immutable_snapshot`: seller snapshots the referenced post at approval and later source changes do not change the served creative. `mutable_requires_reapproval`: the source post may change and material changes require review before continued serving. `mutable_auto_recheck`: the source post may change and the seller continuously or periodically rechecks authorization/policy without requiring buyer resubmission. Omit when the format has no `published_post` slot.").optional(), "production_window_business_days": z.number().int().gte(0).describe("Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative).").optional() }).catchall(z.any()).describe("Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).").describe("**3.2-track canonical.** The structural shape (algorithmic composition + brand-context input + optional offering/landing_page) is captured here so adopters can declare against it in 3.1 catalogs, but the **mention-level tracking contract is intentionally underspecified for 3.1**: no normative macro vocabulary, no postback shape, no cross-surface dedup model. Adopters claiming `agent_placement` in 3.1 ship private tracking integrations and SHOULD leave `experimental: true` on the product declaration that references this canonical; buyer agents MUST treat agent_placement attribution as adapter-defined until the 3.2 tracking-macro spec lands. The canonical promotes to a normatively-buyer-callable surface in 3.2 (or later) once the tracking contract is specified.\n\nSponsored placement integrated into an AI-surface's response to a user. Buyer supplies a `BrandRef` (resolving brand.json for context), an optional `offering_ref` to focus the mention on a specific offering, and an optional `landing_page_url` the surface MAY attach as a citation. The surface (LLM, voice assistant, sponsored-search ranker) composes a natural-language mention, sponsored card, or audio snippet within its response to a user query. **Composition is algorithmic** — the agent chooses phrasing and presentation. Output asset_type varies by surface: `text` for chat UIs and sponsored search snippets; `audio` (synthesized) for voice assistants; `card` for structured AI-surface result cards. Tracking model: mention-level impression + attribution events; per-mention id keys back to brand and offering — but see the 3.2-track note above; the wire shape of these events is not yet specified. Distinct from `si_chat` (which is the user-converses-with-brand's-agent pattern — brand owns the conversational surface) and from `sponsored_placement` (retail-media catalog-driven). Parallels `sponsored_placement` structurally: both are surface-composed placements; agent_placement is for AI/agentic surfaces, sponsored_placement is for retail media.") }), z.object({ "format_kind": z.literal("seller_rendered_stateful_display"), "params": z.object({ "experimental": z.boolean().describe("When true, this canonical or seller narrowing may not work as declared. Adopters SHOULD preflight it with validate_input or in a sandbox and SHOULD NOT route production budget without testing; experimental status never makes the deprecated v1 path preferable. Drivers include unsettled spec shape, an adopter runtime gap, and custom shapes awaiting promotion. This replaces the earlier status plus runtime_status axes. Sellers SHOULD set experimental whenever a canonical or declaration is not production-ready.").default(false), "deprecated": z.boolean().describe("When true, this canonical (or a seller's specific narrowing of it) is going away. Existing adopters are supported through the deprecation cycle; new adoption is discouraged. Pair with `migration_target_version` to indicate when the canonical is expected to be removed. Distinct from `experimental`: an experimental canonical may stabilize and stop being experimental; a deprecated canonical is on a sunset path.").default(false), "v1_translatable": z.boolean().describe("Whether this canonical has any v1 named-format equivalent. `true` (default) — the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1→v2 projection via `v1-canonical-mapping.json` is meaningful. `false` — the canonical is inherently new in v2 and has no v1 form; v1's `list_creative_formats` couldn't express it because the underlying concept (algorithmic surface composition, AI-surface mentions, retail-media catalog placements, multi-card carousels) didn't exist as a v1 named-format archetype.\n\nLets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The six inherently-v2 canonicals in 3.2 are `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`, `seller_rendered_stateful_display`, and `coordinated_placements`.").default(true), "since_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version that introduced this canonical (e.g., '3.1', '3.2'). Lets adopters reason about minimum protocol version requirements when consuming a format declaration. Patch precision is intentionally rejected — canonicals are introduced at minor-version boundaries.").optional(), "migration_target_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version by which the working group expects this canonical to stabilize, surface a breaking revision, or (when `deprecated: true`) be removed. Patch precision is intentionally rejected — canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown').").optional(), "composition_model": z.enum(["deterministic","algorithmic"]).describe("Whether the surface composes deterministically (buyer can predict per-slot rendering — sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing — responsive_creative, agent_placement).").optional(), "provenance_required": z.boolean().describe("When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent.").optional(), "platform_extensions": z.array(z.object({ "uri": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL identifying the extension. `https://` is mandatory — `http://`, `file://`, `data:`, and other schemes are rejected at the schema layer (defense-in-depth on top of the fetch-contract normative rules). The URI base is the owning agent's URL; the path identifies the extension within that agent. Example: 'https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel'. The full fetch contract — SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds — is documented on `product-format-declaration.json#format_schema` and applies to ALL fetches of this reference shape regardless of whether the field is named `format_schema` (load-bearing for validation) or `platform_extensions` (informational); the *transport* rules are identical, only the *consumption* semantics differ."), "digest": z.string().regex(new RegExp("^sha256:[a-f0-9]{64}$")).describe("SHA-256 content digest of the extension definition (sha256:). Used to detect drift — if the agent revises the extension, the digest changes and cached definitions become invalid.") }).catchall(z.any()).describe("Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` — divergent digests in one response indicate producer-side error (e.g., concurrent extension revision mid-render). Buyers encountering divergent digests for the same URI MUST fail closed: treat all references to that URI as unresolved and surface a validation error rather than picking one branch silently. **Across responses**, digest divergence is normal — extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.")).describe("Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\n\n**Collision precedence (normative).** When two or more `platform_extensions[]` entries on the same declaration extend the same target (e.g., both extend `tracking`) with overlapping field names, **array order is authoritative — later entries override earlier ones on a per-field basis** (last-in-array-wins). SDKs MUST surface the overlap via the `errors[]` array on the `get_products` response with a structured code (`FORMAT_DECLARATION_DIVERGENT` is appropriate when the overlap appears across dual-emitted shapes; a producer-self-emitted overlap on a single declaration SHOULD use the same code with `error.details: { collision_kind: \"platform_extension_field\", target, overlapping_fields, winning_extension_uri }`). Producers SHOULD avoid the collision by emitting one extension per target or by partitioning fields across extensions; the deterministic precedence is for last-resort consistency across SDK implementations, not a sanctioned merging strategy.").optional(), "synthesis_nondeterministic": z.boolean().describe("When true, the format's production pipeline is genuinely nondeterministic — the platform cannot guarantee that synthesis from a given input set produces in-spec output. Veo / Sora / Runway-class generative video, and other AI-synthesis flows where output dimensions, duration, or quality vary per run. Implies a different validation contract: predictive `validate_input` is impossible; the platform's own post-synthesis QA loop applies; if the QA loop exhausts without producing a valid artifact, `build_creative` returns task_failed with a synthesis_failed reason. Distinct from `composition_model` (which describes how the surface composes per-slot rendering, not whether synthesis is deterministic). When false or absent, the format's production is predictable enough that `validate_input` can predict output properties from input properties.\n\n**Compatibility with `asset_source` / `item_production_model`**: `synthesis_nondeterministic: true` MAY pair with any of `seller_pre_rendered_from_brief`, `seller_human_designed`, or `agent_synthesized` (the QA loop is concept-level, not source-specific — 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.").default(false), "slots": z.array(z.object({ "asset_group_id": z.string().describe("Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings."), "asset_type": z.enum(["image","video","audio","text","markdown","url","html","css","javascript","vast","daast","webhook","brief","catalog","published_post","zip","card","object","pixel_tracker","vast_tracker","daast_tracker"]).describe("Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `published_post` is an existing-post reference asset, not uploaded media bytes and not a catalog row. `card` is the multi-card carousel element type (see card-asset.json). `pixel_tracker` / `vast_tracker` / `daast_tracker` are the renderer-fired measurement-tracker primitives — see `/schemas/core/assets/pixel-tracker-asset.json` and the VAST / DAAST tracker schemas. `object` is a last-resort fallback for structured non-asset inputs that don't fit any primitive asset_type — prefer specific types whenever possible."), "required": z.boolean().describe("Whether this slot is required for a valid manifest.").default(false), "min": z.number().int().gte(0).describe("Minimum count for repeatable / pool slots.").optional(), "max": z.number().int().gte(1).describe("Maximum count for repeatable / pool slots.").optional(), "max_chars": z.number().int().gte(1).describe("Per-slot character limit. Valid only when `asset_type` is `text`, `markdown`, or `brief`. Mutually exclusive with `max_size_kb` (which applies to binary asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "max_size_kb": z.number().int().gte(1).describe("Per-slot file size limit in kilobytes. Valid only when `asset_type` is `image`, `video`, `audio`, or `zip`. Mutually exclusive with `max_chars` (which applies to text asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Accepted intrinsic-pixel densities for this image-bearing slot. Valid when `asset_type` is `image`, and on a `card` slot where it constrains each card's image media (video media is unaffected). This makes density available to every canonical carrying image assets (native, carousel, responsive, companion images, and image itself), not only `format_kind: image`. When the image canonical also declares top-level `params.pixel_ratios`, the effective set is the intersection; an empty intersection is invalid. One matching asset satisfies the slot unless `required_pixel_ratios` requires rendition coverage.").optional(), "required_pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Required density coverage for an image rendition set. Valid only when `asset_type` is `image` and `pixel_ratios` is also declared. Every value MUST appear in the effective accepted set after intersecting any top-level image `params.pixel_ratios`, and the manifest slot value MUST be an array containing exactly one matching image rendition for each required ratio. Other accepted ratios remain optional. For example, `pixel_ratios: [1, 1.5, 2]` with `required_pixel_ratios: [1, 2]` requires the 1x and 2x renditions while making 1.5x optional. SDKs enforce intersection, subset, coverage, and duplicate-ratio rules because JSON Schema draft-07 cannot express them generically.").optional(), "logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("When `asset_group_id` is `logo`, renderer-facing brand.json logo slots acceptable for this format slot. Producers selecting from brand.json SHOULD prefer `logos[]` entries whose `slots[]` intersects this list, then apply `visual_guidelines.logo_usage_rules[]`.").optional(), "required_logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Subset of `logo_slots` for which this format expects explicit logo coverage. A manifest or brand-derived logo pool SHOULD include at least one usable logo for each required slot; if coverage is missing, builders SHOULD surface a validation warning or approval mapping instead of guessing from prose.").optional(), "description": z.string().describe("Human-readable description of what the slot expects from the buyer.").optional(), "consumed_for_production": z.boolean().describe("Dispatch hint for `build_creative` and v1↔v2 wire translators: when `true`, the slot's value is consumed as INPUT to a production step (host-read script, brief copy fed to generative synthesis, catalog feed driving per-SKU rendering) and is not rendered verbatim. When `false` (default), the slot's value is rendered verbatim on the placement (image bytes, video file, display tag).\n\nMotivates the v1↔v2 dispatch table: pre-v2 buyers shipped production-consumed inputs separately in a `inputs` map on the build_creative request; v2 collapses inputs and rendered assets into a single `assets` map keyed by `asset_group_id`. SDK translators between v1 and v2 use this flag per canonical to know which assets in the v2 manifest map back to v1 `inputs` vs v1 `assets`. Without the per-slot flag the dispatch table lives in adopter code and every SDK gets it slightly different.\n\nProducers SHOULD set this explicitly on slots whose consumption pattern isn't obvious (host-read scripts on `audio_hosted`, briefs on generative `video_hosted`, catalog feeds on `sponsored_placement`). For canonicals where every slot is render-verbatim (`image`, `display_tag`, `video_vast`), the default `false` is sufficient and the flag MAY be omitted.").default(false) }).catchall(z.any()).and(z.intersection(z.intersection(z.any(), z.intersection(z.any(), z.any())), z.intersection(z.any(), z.intersection(z.any(), z.any()))))).describe("Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.").optional(), "required_connections": z.array(z.object({ "provider": z.string().describe("Stable provider or platform namespace, preferably lowercase. Examples: `social.example`, `shortvideo.example`, or a seller-defined namespace. Omit only when the requirement is provider-agnostic, or when an `authorization_url` fully routes the human to the correct provider-specific connection flow.").optional(), "connection_type": z.enum(["advertiser_account","publisher_identity","post_authorization"]).describe("Kind of downstream connection required. `advertiser_account` is the platform account used to buy/manage ads. `publisher_identity` is the creator, page, channel, organization, or profile that owns source posts. `post_authorization` is a post-scoped grant when the platform authorizes individual posts instead of, or in addition to, the owning identity."), "required_for": z.array(z.string().min(1)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Concrete AdCP protocol operation names that require this downstream connection. Sellers SHOULD include this in product declarations when the requirement is known ahead of time, and in AUTHORIZATION_REQUIRED details when it explains the failed operation. Prefer specific operation names such as `list_creatives`, `sync_creatives`, `create_media_buy`, `get_media_buy_delivery`, or `get_creative_delivery` over broad category labels such as `reporting`.").optional(), "scope": z.enum(["account","identity","post","unknown"]).describe("Granularity of the downstream grant.").optional(), "status": z.enum(["connected","missing","pending","expired","revoked","not_required","unknown"]).describe("Current seller-observed state for this downstream connection when known. Product declarations MAY omit status or use `unknown`; AUTHORIZATION_REQUIRED details SHOULD use `missing`, `expired`, or `revoked` for the connection that blocked the call.").optional(), "connection_id": z.string().describe("Seller-defined identifier for an already-created downstream connection. Omit when no connection exists yet or when exposing it would leak platform/account state.").optional(), "resource_ref": z.object({ "platform_account_id": z.string().describe("Provider-native advertiser or business account id, when safe to disclose.").optional(), "identity_id": z.string().describe("Provider-native creator, page, channel, organization, or profile id, when safe to disclose.").optional(), "handle": z.string().describe("Provider-native public handle for the owning identity, when available.").optional(), "profile_url": z.string().url().describe("Public URL for the owning identity, when available.").optional(), "post_id": z.string().describe("Provider-native post id, when the grant is post-scoped or the failed request referenced a specific post.").optional(), "post_url": z.string().url().describe("Public URL for the referenced post, when available.").optional() }).catchall(z.any()).describe("Optional opaque provider-native resource hint, such as a platform account id, profile URL, handle, channel id, post id, or post URL. This is a hint for routing authorization, not proof that authorization exists.").optional(), "authorization_url": z.string().url().describe("Seller-hosted or provider-hosted URL where a human can complete or restore this downstream connection.").optional(), "authorization_instructions": z.string().describe("Human-readable instructions for completing or restoring this downstream connection.").optional(), "expires_at": z.string().datetime().describe("Expiration time for the downstream grant, when known.").optional() }).catchall(z.any()).and(z.any()).describe("A seller/platform-side connection or grant required by a product, format, or request. This is not the AdCP caller credential: the AdCP request is still authenticated once, and the seller uses these stored downstream connections to call a platform or service on the buyer's behalf. Use this shape for platforms that require more than one downstream grant, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.")).describe("Downstream platform connections or grants required to use this format declaration. These are in addition to the single AdCP caller credential. Use this when a platform product requires multiple downstream grants, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.").optional(), "reference_mutability": z.enum(["immutable_snapshot","mutable_requires_reapproval","mutable_auto_recheck"]).describe("Policy for formats whose `slots` accept a `published_post` reference. `immutable_snapshot`: seller snapshots the referenced post at approval and later source changes do not change the served creative. `mutable_requires_reapproval`: the source post may change and material changes require review before continued serving. `mutable_auto_recheck`: the source post may change and the seller continuously or periodically rechecks authorization/policy without requiring buyer resubmission. Omit when the format has no `published_post` slot.").optional(), "production_window_business_days": z.number().int().gte(0).describe("Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative).").optional() }).catchall(z.any()).describe("Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).").describe("Seller-rendered display unit whose declaration is an executable template contract: buyer-known visual states, explicit transitions, breakpoint canvases, and per-state slot bindings. The seller owns the runtime; `supply_mode` declares which end the buyer feeds. For machine-rendered `components` and `rendered_canvases` supply, sellers MUST support `preview_creative` returning every state × breakpoint from a candidate manifest. `layered_source` instead follows the asynchronous seller-production preview path after the declared production window. `composition_model: deterministic` describes serving the finished states, not instant derivation from layered source. Buyer-executable HTML/MRAID is `html5`, a buyer-delivered tag is `display_tag`, arbitrary games/hotspots/scripts remain `custom`, and per-impression algorithmic assembly is `responsive_creative`.") }), z.object({ "format_kind": z.literal("coordinated_placements"), "params": z.object({ "experimental": z.boolean().describe("When true, this canonical or seller narrowing may not work as declared. Adopters SHOULD preflight it with validate_input or in a sandbox and SHOULD NOT route production budget without testing; experimental status never makes the deprecated v1 path preferable. Drivers include unsettled spec shape, an adopter runtime gap, and custom shapes awaiting promotion. This replaces the earlier status plus runtime_status axes. Sellers SHOULD set experimental whenever a canonical or declaration is not production-ready.").default(false), "deprecated": z.boolean().describe("When true, this canonical (or a seller's specific narrowing of it) is going away. Existing adopters are supported through the deprecation cycle; new adoption is discouraged. Pair with `migration_target_version` to indicate when the canonical is expected to be removed. Distinct from `experimental`: an experimental canonical may stabilize and stop being experimental; a deprecated canonical is on a sunset path.").default(false), "v1_translatable": z.boolean().describe("Whether this canonical has any v1 named-format equivalent. `true` (default) — the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1→v2 projection via `v1-canonical-mapping.json` is meaningful. `false` — the canonical is inherently new in v2 and has no v1 form; v1's `list_creative_formats` couldn't express it because the underlying concept (algorithmic surface composition, AI-surface mentions, retail-media catalog placements, multi-card carousels) didn't exist as a v1 named-format archetype.\n\nLets SDKs distinguish two failure modes that today look identical: (a) the registry hasn't covered this canonical yet (correctable — seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational — buyer needs v2-aware consumption, or seller declares `canonical_formats_only: true` on the product declaration). SDKs encountering `v1_translatable: false` on a canonical SHOULD NOT emit `FORMAT_PROJECTION_FAILED` (which signals registry-coverage gap) — instead surface the inherent v1-unreachability as a different diagnostic or skip silently. The six inherently-v2 canonicals in 3.2 are `image_carousel`, `sponsored_placement`, `responsive_creative`, `agent_placement`, `seller_rendered_stateful_display`, and `coordinated_placements`.").default(true), "since_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version that introduced this canonical (e.g., '3.1', '3.2'). Lets adopters reason about minimum protocol version requirements when consuming a format declaration. Patch precision is intentionally rejected — canonicals are introduced at minor-version boundaries.").optional(), "migration_target_version": z.string().regex(new RegExp("^[1-9]\\d*\\.(0|[1-9]\\d*)$")).describe("AdCP MAJOR.MINOR version by which the working group expects this canonical to stabilize, surface a breaking revision, or (when `deprecated: true`) be removed. Patch precision is intentionally rejected — canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown').").optional(), "composition_model": z.enum(["deterministic","algorithmic"]).describe("Whether the surface composes deterministically (buyer can predict per-slot rendering — sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing — responsive_creative, agent_placement).").optional(), "provenance_required": z.boolean().describe("When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent.").optional(), "platform_extensions": z.array(z.object({ "uri": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL identifying the extension. `https://` is mandatory — `http://`, `file://`, `data:`, and other schemes are rejected at the schema layer (defense-in-depth on top of the fetch-contract normative rules). The URI base is the owning agent's URL; the path identifies the extension within that agent. Example: 'https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel'. The full fetch contract — SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds — is documented on `product-format-declaration.json#format_schema` and applies to ALL fetches of this reference shape regardless of whether the field is named `format_schema` (load-bearing for validation) or `platform_extensions` (informational); the *transport* rules are identical, only the *consumption* semantics differ."), "digest": z.string().regex(new RegExp("^sha256:[a-f0-9]{64}$")).describe("SHA-256 content digest of the extension definition (sha256:). Used to detect drift — if the agent revises the extension, the digest changes and cached definitions become invalid.") }).catchall(z.any()).describe("Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` — divergent digests in one response indicate producer-side error (e.g., concurrent extension revision mid-render). Buyers encountering divergent digests for the same URI MUST fail closed: treat all references to that URI as unresolved and surface a validation error rather than picking one branch silently. **Across responses**, digest divergence is normal — extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.")).describe("Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\n\n**Collision precedence (normative).** When two or more `platform_extensions[]` entries on the same declaration extend the same target (e.g., both extend `tracking`) with overlapping field names, **array order is authoritative — later entries override earlier ones on a per-field basis** (last-in-array-wins). SDKs MUST surface the overlap via the `errors[]` array on the `get_products` response with a structured code (`FORMAT_DECLARATION_DIVERGENT` is appropriate when the overlap appears across dual-emitted shapes; a producer-self-emitted overlap on a single declaration SHOULD use the same code with `error.details: { collision_kind: \"platform_extension_field\", target, overlapping_fields, winning_extension_uri }`). Producers SHOULD avoid the collision by emitting one extension per target or by partitioning fields across extensions; the deterministic precedence is for last-resort consistency across SDK implementations, not a sanctioned merging strategy.").optional(), "synthesis_nondeterministic": z.boolean().describe("When true, the format's production pipeline is genuinely nondeterministic — the platform cannot guarantee that synthesis from a given input set produces in-spec output. Veo / Sora / Runway-class generative video, and other AI-synthesis flows where output dimensions, duration, or quality vary per run. Implies a different validation contract: predictive `validate_input` is impossible; the platform's own post-synthesis QA loop applies; if the QA loop exhausts without producing a valid artifact, `build_creative` returns task_failed with a synthesis_failed reason. Distinct from `composition_model` (which describes how the surface composes per-slot rendering, not whether synthesis is deterministic). When false or absent, the format's production is predictable enough that `validate_input` can predict output properties from input properties.\n\n**Compatibility with `asset_source` / `item_production_model`**: `synthesis_nondeterministic: true` MAY pair with any of `seller_pre_rendered_from_brief`, `seller_human_designed`, or `agent_synthesized` (the QA loop is concept-level, not source-specific — 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.").default(false), "slots": z.array(z.object({ "asset_group_id": z.string().describe("Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings."), "asset_type": z.enum(["image","video","audio","text","markdown","url","html","css","javascript","vast","daast","webhook","brief","catalog","published_post","zip","card","object","pixel_tracker","vast_tracker","daast_tracker"]).describe("Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `published_post` is an existing-post reference asset, not uploaded media bytes and not a catalog row. `card` is the multi-card carousel element type (see card-asset.json). `pixel_tracker` / `vast_tracker` / `daast_tracker` are the renderer-fired measurement-tracker primitives — see `/schemas/core/assets/pixel-tracker-asset.json` and the VAST / DAAST tracker schemas. `object` is a last-resort fallback for structured non-asset inputs that don't fit any primitive asset_type — prefer specific types whenever possible."), "required": z.boolean().describe("Whether this slot is required for a valid manifest.").default(false), "min": z.number().int().gte(0).describe("Minimum count for repeatable / pool slots.").optional(), "max": z.number().int().gte(1).describe("Maximum count for repeatable / pool slots.").optional(), "max_chars": z.number().int().gte(1).describe("Per-slot character limit. Valid only when `asset_type` is `text`, `markdown`, or `brief`. Mutually exclusive with `max_size_kb` (which applies to binary asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "max_size_kb": z.number().int().gte(1).describe("Per-slot file size limit in kilobytes. Valid only when `asset_type` is `image`, `video`, `audio`, or `zip`. Mutually exclusive with `max_chars` (which applies to text asset types). Schema enforces via if/then so a producer can't set both on the same slot.").optional(), "pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Accepted intrinsic-pixel densities for this image-bearing slot. Valid when `asset_type` is `image`, and on a `card` slot where it constrains each card's image media (video media is unaffected). This makes density available to every canonical carrying image assets (native, carousel, responsive, companion images, and image itself), not only `format_kind: image`. When the image canonical also declares top-level `params.pixel_ratios`, the effective set is the intersection; an empty intersection is invalid. One matching asset satisfies the slot unless `required_pixel_ratios` requires rendition coverage.").optional(), "required_pixel_ratios": z.array(z.number().gt(0)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Required density coverage for an image rendition set. Valid only when `asset_type` is `image` and `pixel_ratios` is also declared. Every value MUST appear in the effective accepted set after intersecting any top-level image `params.pixel_ratios`, and the manifest slot value MUST be an array containing exactly one matching image rendition for each required ratio. Other accepted ratios remain optional. For example, `pixel_ratios: [1, 1.5, 2]` with `required_pixel_ratios: [1, 2]` requires the 1x and 2x renditions while making 1.5x optional. SDKs enforce intersection, subset, coverage, and duplicate-ratio rules because JSON Schema draft-07 cannot express them generically.").optional(), "logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("When `asset_group_id` is `logo`, renderer-facing brand.json logo slots acceptable for this format slot. Producers selecting from brand.json SHOULD prefer `logos[]` entries whose `slots[]` intersects this list, then apply `visual_guidelines.logo_usage_rules[]`.").optional(), "required_logo_slots": z.array(z.enum(["logo_card_light","logo_card_dark","profile_mark","favicon","app_icon","social_profile_mark","nav_header","footer","email_header","watermark","ad_end_card","co_brand_lockup","marketplace_listing"]).describe("Canonical renderer-facing logo slot. Use when selecting a logo variant from brand.json for a specific UI or creative placement.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Subset of `logo_slots` for which this format expects explicit logo coverage. A manifest or brand-derived logo pool SHOULD include at least one usable logo for each required slot; if coverage is missing, builders SHOULD surface a validation warning or approval mapping instead of guessing from prose.").optional(), "description": z.string().describe("Human-readable description of what the slot expects from the buyer.").optional(), "consumed_for_production": z.boolean().describe("Dispatch hint for `build_creative` and v1↔v2 wire translators: when `true`, the slot's value is consumed as INPUT to a production step (host-read script, brief copy fed to generative synthesis, catalog feed driving per-SKU rendering) and is not rendered verbatim. When `false` (default), the slot's value is rendered verbatim on the placement (image bytes, video file, display tag).\n\nMotivates the v1↔v2 dispatch table: pre-v2 buyers shipped production-consumed inputs separately in a `inputs` map on the build_creative request; v2 collapses inputs and rendered assets into a single `assets` map keyed by `asset_group_id`. SDK translators between v1 and v2 use this flag per canonical to know which assets in the v2 manifest map back to v1 `inputs` vs v1 `assets`. Without the per-slot flag the dispatch table lives in adopter code and every SDK gets it slightly different.\n\nProducers SHOULD set this explicitly on slots whose consumption pattern isn't obvious (host-read scripts on `audio_hosted`, briefs on generative `video_hosted`, catalog feeds on `sponsored_placement`). For canonicals where every slot is render-verbatim (`image`, `display_tag`, `video_vast`), the default `false` is sufficient and the flag MAY be omitted.").default(false) }).catchall(z.any()).and(z.intersection(z.intersection(z.any(), z.intersection(z.any(), z.any())), z.intersection(z.any(), z.intersection(z.any(), z.any()))))).describe("Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.").optional(), "required_connections": z.array(z.object({ "provider": z.string().describe("Stable provider or platform namespace, preferably lowercase. Examples: `social.example`, `shortvideo.example`, or a seller-defined namespace. Omit only when the requirement is provider-agnostic, or when an `authorization_url` fully routes the human to the correct provider-specific connection flow.").optional(), "connection_type": z.enum(["advertiser_account","publisher_identity","post_authorization"]).describe("Kind of downstream connection required. `advertiser_account` is the platform account used to buy/manage ads. `publisher_identity` is the creator, page, channel, organization, or profile that owns source posts. `post_authorization` is a post-scoped grant when the platform authorizes individual posts instead of, or in addition to, the owning identity."), "required_for": z.array(z.string().min(1)).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Concrete AdCP protocol operation names that require this downstream connection. Sellers SHOULD include this in product declarations when the requirement is known ahead of time, and in AUTHORIZATION_REQUIRED details when it explains the failed operation. Prefer specific operation names such as `list_creatives`, `sync_creatives`, `create_media_buy`, `get_media_buy_delivery`, or `get_creative_delivery` over broad category labels such as `reporting`.").optional(), "scope": z.enum(["account","identity","post","unknown"]).describe("Granularity of the downstream grant.").optional(), "status": z.enum(["connected","missing","pending","expired","revoked","not_required","unknown"]).describe("Current seller-observed state for this downstream connection when known. Product declarations MAY omit status or use `unknown`; AUTHORIZATION_REQUIRED details SHOULD use `missing`, `expired`, or `revoked` for the connection that blocked the call.").optional(), "connection_id": z.string().describe("Seller-defined identifier for an already-created downstream connection. Omit when no connection exists yet or when exposing it would leak platform/account state.").optional(), "resource_ref": z.object({ "platform_account_id": z.string().describe("Provider-native advertiser or business account id, when safe to disclose.").optional(), "identity_id": z.string().describe("Provider-native creator, page, channel, organization, or profile id, when safe to disclose.").optional(), "handle": z.string().describe("Provider-native public handle for the owning identity, when available.").optional(), "profile_url": z.string().url().describe("Public URL for the owning identity, when available.").optional(), "post_id": z.string().describe("Provider-native post id, when the grant is post-scoped or the failed request referenced a specific post.").optional(), "post_url": z.string().url().describe("Public URL for the referenced post, when available.").optional() }).catchall(z.any()).describe("Optional opaque provider-native resource hint, such as a platform account id, profile URL, handle, channel id, post id, or post URL. This is a hint for routing authorization, not proof that authorization exists.").optional(), "authorization_url": z.string().url().describe("Seller-hosted or provider-hosted URL where a human can complete or restore this downstream connection.").optional(), "authorization_instructions": z.string().describe("Human-readable instructions for completing or restoring this downstream connection.").optional(), "expires_at": z.string().datetime().describe("Expiration time for the downstream grant, when known.").optional() }).catchall(z.any()).and(z.any()).describe("A seller/platform-side connection or grant required by a product, format, or request. This is not the AdCP caller credential: the AdCP request is still authenticated once, and the seller uses these stored downstream connections to call a platform or service on the buyer's behalf. Use this shape for platforms that require more than one downstream grant, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.")).describe("Downstream platform connections or grants required to use this format declaration. These are in addition to the single AdCP caller credential. Use this when a platform product requires multiple downstream grants, such as an advertiser account connection plus a publisher identity or post authorization for published-post references.").optional(), "reference_mutability": z.enum(["immutable_snapshot","mutable_requires_reapproval","mutable_auto_recheck"]).describe("Policy for formats whose `slots` accept a `published_post` reference. `immutable_snapshot`: seller snapshots the referenced post at approval and later source changes do not change the served creative. `mutable_requires_reapproval`: the source post may change and material changes require review before continued serving. `mutable_auto_recheck`: the source post may change and the seller continuously or periodically rechecks authorization/policy without requiring buyer resubmission. Omit when the format has no `published_post` slot.").optional(), "production_window_business_days": z.number().int().gte(0).describe("Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative).").optional() }).catchall(z.any()).describe("Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).").describe("One creative manifest atomically supplies assets for multiple declared product placements. Each component binds to a public `Product.placements[]` entry and either declares an inline non-custom canonical format or references a sibling format option on the same product. Components cannot nest coordinated placements. The manifest supplies component slots under `component_assets.`; `shared_slots` assets are supplied once at top level. Inventory exclusivity remains `Product.exclusivity`, not a creative-format parameter. Ordinary products whose placements accept independently assigned creatives do not need this canonical.") }), z.object({ "format_kind": z.literal("custom"), "params": z.record(z.string(), z.any()).describe("Custom shape's params. Validated against the schema fetched from `format_schema.uri` at the cached `format_schema.digest`.") }).describe("Adopter-defined shape that doesn't fit the 14 canonicals. Requires `format_shape` (vocabulary-registered global pattern) and `format_schema` (URI+digest reference to a fetchable schema describing the actual params/slots). `params` shape is governed by the fetched schema rather than baked into AdCP — kept as `type: object` here with `additionalProperties: true` because the canonical schema validates dynamically post-fetch.")])).and(z.intersection(z.intersection(z.any(), z.union([z.union([z.any(), z.any()]), z.any().refine((value) => !z.union([z.any(), z.any()]).safeParse(value).success, "Invalid input: Should NOT be valid against schema")]).superRefine((value,ctx) => { const result = z.any().safeParse(value).success ? z.union([z.any(), z.any()]).safeParse(value) : z.any().refine((value) => !z.union([z.any(), z.any()]).safeParse(value).success, "Invalid input: Should NOT be valid against schema").safeParse(value); if (!result.success) { result.error.issues.forEach((error: any) => ctx.addIssue(error)) } -})), z.intersection(z.any().refine((value) => !z.intersection(z.any(), z.any()).safeParse(value).success, "Invalid input: Should NOT be valid against schema"), z.intersection(z.any().refine((value) => !z.any().safeParse(value).success, "Invalid input: Should NOT be valid against schema"), z.any())))).describe("Inline format declaration on a product. The `format_kind` discriminator names which canonical format the product narrows; `params` carries the canonical's parameter schema (slots, dimensions, durations, codecs, character limits, platform_extensions, etc.). Optional `format_option_id` (stable identifier for routing when a product's `format_options` contains multiple declarations sharing the same `format_kind`), optional `publisher_domain` (namespace for the format option when it comes from a publisher adagents.json catalog), `display_name` (seller-controlled human-readable label for dashboard and catalog UIs), and `applies_to_channels` (subset of the product's declared channels this declaration applies to — lets a multi-channel product carry distinct format_options per channel). Discriminated-union shape generates clean tagged unions in TypeScript and Pydantic codegen. Replaces v1's named-format pattern (where products referenced a separately-defined format file via compound `format_id`). Canonical product-bound declarations are the AdCP 3.2 authoring path; v1 named formats remain only as deprecated 3.x compatibility projections.\n\n**Closed-set semantics (normative).** `format_options[]` is the closed set of accepted formats for this product. Sellers MUST reject `create_media_buy` requests targeting any `format_kind` (or format option reference) not present in this list — typically with `UNSUPPORTED_FEATURE` or a seller-specific code; the rejection is structural, not negotiable. `seller_preference` modulates *within* the accepted set (a soft ranking hint between equally-acceptable options), it is NOT an enforcement axis. A product wanting to say 'this format is the only one that works' lists exactly that one entry in `format_options[]`; everything else falls outside the set and is rejected by the closed-set rule.\n\n**Format matching vs satisfaction (normative).** Legacy named formats MUST be normalized to canonical declarations before comparison; do not exact-match raw `(agent_url, id)` pairs once a `format_id` has been projected through `canonical`, `v1_format_ref`, or the canonical mapping registry. Equivalence matching can treat a legacy fixed-size display ID and `format_kind: \"image\"` with matching `width`/`height` as the same underlying shape. Product satisfaction is stricter and directional: when this declaration specifies fixed constraints such as `width`, `height`, `duration_ms_exact`, or `duration_ms_range`, a buyer request or creative manifest MUST declare and satisfy those constraints. A broad request with no dimensions or duration does not satisfy a fixed-size or fixed-duration product; a broad product MAY accept a more specific creative unless another product constraint excludes it. Duration precedence is `duration_ms_exact` > `duration_ms_range`. Range constraints use containment: a range-based request satisfies this declaration only when every value it permits falls within this declaration's accepted range; overlap alone is insufficient. An exact value satisfies a range when the exact value falls inside the accepted interval. For hosted audio/video, a null range endpoint is unbounded: [null, 60000] means up to 60s, and [15000, null] means at least 15s; [null, null] is invalid because at least one endpoint must be bounded.\n\n**Custom format_kind** (`format_kind: \"custom\"`): for adopter-defined shapes that don't fit the 12 canonicals (multi-placement takeover, roadblock, branded content, cross-screen sponsorship, sponsorship lockup, newsletter sponsorship, AR lens, playable, live event sponsorship). When `format_kind` is `custom`, the declaration MUST carry `format_shape` (recognized global pattern from the [format-shape vocabulary registry](/schemas/core/format-shape-vocabulary.json)) AND `format_schema` (URI+digest reference to a fetchable schema describing the actual `params` and `slots`). Buyer agents fetch the schema, validate manifests structurally, and reason about manifests without per-seller integration code. See [adcp#3666](https://github.com/adcontextprotocol/adcp/issues/3666) for the canonical promotion queue."), z.object({ "applies_to_property_ids": z.array(z.string().regex(new RegExp("^[a-z0-9_]+$")).describe("Identifier for a publisher property. Must be lowercase alphanumeric with underscores only.")).describe("Optional property IDs from this file's `properties[]` that this format declaration applies to. When omitted, the declaration applies to all properties in the file. Mutually compatible with `applies_to_property_tags` (union is the effective scope). Example: Meta declares Reels with `applies_to_property_ids: [\"instagram\", \"facebook\"]` because WhatsApp doesn't carry Reels inventory.").optional(), "applies_to_property_tags": z.array(z.string().regex(new RegExp("^[a-z0-9_]+$")).describe("Tag for categorizing publisher properties. Must be lowercase alphanumeric with underscores only.")).describe("Optional property tags from this file's `tags` map that this format declaration applies to. When omitted, the declaration applies to all properties in the file. Useful for network-wide formats (e.g., a managed network declaring `applies_to_property_tags: [\"premium_video\"]`). Compatible with `applies_to_property_ids` — a property is in scope if it matches either ID list or tag list.").optional(), "reference_renderer": z.object({ "runtime": z.literal("browser-esm").describe("Execution contract for the referenced package. browser-esm means a browser-safe ECMAScript module that accepts canonical manifest data and returns an inert presentation without Node.js APIs, ambient credentials, delivery tracking, or undeclared network access. Non-JavaScript clients use a hosted preview_creative provider or display the manifest."), "package": z.string().regex(new RegExp("^(?:@[a-z0-9][a-z0-9._~-]*/)?[a-z0-9][a-z0-9._~-]*$")).describe("npm package name, scoped or unscoped. The package is resolved from the npm registry; the AdCP registry does not proxy its executable contents."), "version": z.string().regex(new RegExp("^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$")).describe("Exact semantic version. Ranges and tags such as latest are forbidden so the registry entry is reproducible. Package semantic versioning identifies the pinned distribution artifact; it is independent of any one format revision because one package may expose renderers for multiple formats."), "export": z.string().min(1).describe("Named package export that implements the renderer contract for this enclosing format entry. Compatibility is bound at the export-to-entry edge, not to the package major: the export's documented input/output contract MUST implement the enclosing format entry's revision. When that format entry moves to a new major revision, the registry MUST point it to a compatible export, rotating package version and integrity only when the selected artifact changes. One package version MAY expose different named exports for different formats or format revisions."), "format_revision": z.string().regex(new RegExp("^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)$")).describe("Exact canonical format-entry revision implemented by this named export. It MUST equal the enclosing community format entry's format_revision; compatibility binds to this export edge, not the package major."), "integrity": z.string().regex(new RegExp("^(?:sha256-[A-Za-z0-9+/]{43}=|sha384-[A-Za-z0-9+/]{64}|sha512-[A-Za-z0-9+/]{86}==)$")).describe("Subresource Integrity value for the exact npm package tarball. Consumers MUST compare this value before loading code, require the provenance subject digest to match the same tarball, and fail closed on mismatch."), "provenance": z.object({ "source_repository": z.string().url().regex(new RegExp("^https://github\\.com/")).describe("Allowlisted source repository that the npm provenance attestation MUST identify."), "workflow_path": z.string().regex(new RegExp("^\\.github/workflows/[A-Za-z0-9._/-]+\\.ya?ml$")).describe("Repository-relative GitHub Actions workflow path that npm provenance buildDefinition.externalParameters.workflow.path MUST identify.") }).strict() }).strict().describe("Optional only for an AgenticAdvertising.org community-registry format entry fetched from the configured registry origin. Points to a pinned browser-ESM OSS renderer package export that provides a non-authoritative reference presentation. The named export's contract, not the package major, MUST implement this enclosing format entry's revision. Non-JavaScript clients use hosted preview_creative or display the manifest. Publisher-origin catalogs and seller product declarations do not use this field: publishers describe placement-specific chrome with placements[].presentation_ref and grant route-specific preview authority with placements[].preview_provider.").optional(), "format_revision": z.string().regex(new RegExp("^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)$")).describe("Registry-managed revision of this community format entry. Required when reference_renderer is present and MUST equal reference_renderer.format_revision.").optional() }).and(z.any()))).describe("Publisher-authoritative format catalog. Declares the 3.1+ canonical format-option shapes the publisher supports across its properties — the single place a publisher (or its community-registry stand-in) asserts \"these are the formats my inventory accepts.\" Products selling this publisher's inventory SHOULD reference these declarations by `format_option_id` on the placement or via inline `format_options` whose `format_option_id` matches an entry here, eliminating the N-copies-of-Meta-Reels-on-N-products drift surface.\n\nEach item is a `ProductFormatDeclaration` (same 3.1+ canonical format-option shape used on Products) plus optional `applies_to_property_ids` / `applies_to_property_tags` for property scoping within the file. A `formats[]` entry without scope applies to all properties in the file; with scope, only to the named subset (e.g., Reels applies to Instagram + Facebook but not WhatsApp).\n\n**Community registry pattern (normative for unadopted platforms).** When a platform hasn't adopted AdCP (Meta, TikTok, Snap, Pinterest, etc.), AAO publishes a community-maintained adagents.json at `https://creative.adcontextprotocol.org/translated//adagents.json` carrying that platform's `formats[]`. Buyer SDKs fetch the platform's own `/.well-known/adagents.json` first; on 404 or absence-of-formats[], they fall back to the AAO mirror. When the platform adopts AdCP and publishes their own adagents.json with `formats[]`, the platform-hosted file takes precedence and the mirror entry becomes redundant (AAO maintainers deprecate it).\n\nThe `v1_format_ref.agent_url` on each declaration SHOULD match the agent_url of the file's hosting location — platform-hosted formats point at the platform's agent, community-mirror formats point at `https://creative.adcontextprotocol.org/translated/`. This keeps the legacy named-format namespace converged regardless of which side hosts the catalog. See `docs/creative/canonical-formats.mdx` Meta Reels worked example.").optional(), "superseded_by": z.string().url().regex(new RegExp("^https://")).describe("Optional pointer indicating this adagents.json file has been superseded by another adagents.json at a different URL. Used by the AAO community-mirror lifecycle: when a platform (e.g., Meta) adopts AdCP and publishes its own adagents.json at `/.well-known/adagents.json`, the AAO mirror file at `creative.adcontextprotocol.org/translated//adagents.json` sets `superseded_by` to the platform-hosted URL. Buyer SDKs encountering a file with `superseded_by` SHOULD short-circuit and re-fetch from the named URL rather than serving stale content from the mirror. The mirror SHOULD continue serving with `superseded_by` set for ≥1 minor release after platform adoption so buyer caches keyed on the mirror URL get an explicit migration signal rather than a silent break.").optional(), "tags": z.record(z.string(), z.object({ "name": z.string().describe("Human-readable name for this tag"), "description": z.string().describe("Description of what this tag represents") }).catchall(z.any())).describe("Metadata for each tag referenced by properties. Provides human-readable context for property tag values.").optional(), "placement_tags": z.record(z.string(), z.object({ "name": z.string().describe("Human-readable name for this placement tag"), "description": z.string().describe("Description of what this placement tag represents") }).catchall(z.any())).describe("Metadata for each tag referenced by placements. Provides human-readable context for publisher-defined placement tag values used in grouping and authorization.").optional(), "authorized_agents": z.array(z.union([z.object({ "authorization_type": z.literal("property_ids").describe("Discriminator indicating authorization by specific property IDs"), "property_ids": z.array(z.string().regex(new RegExp("^[a-z0-9_]+$")).describe("Identifier for a publisher property. Must be lowercase alphanumeric with underscores only.")).describe("Property IDs this agent is authorized for. Resolved against the top-level properties array in this file"), "collections": z.array(z.object({ "publisher_domain": z.string().regex(new RegExp("^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$")).describe("Domain where the adagents.json declaring these collections is hosted (e.g., 'mrbeast.com'). The collections array in that file contains the authoritative collection definitions."), "collection_ids": z.array(z.string()).describe("Collection IDs from the adagents.json collections array. Each ID must match a collection_id declared in that file.") }).catchall(z.any()).describe("References collections declared in an adagents.json. Buyers resolve full collection objects by fetching the adagents.json at the given domain and matching collection_ids against its collections array.")).describe("Optional collection constraints. When present, authorization only applies to inventory associated with these collections.").optional(), "placement_ids": z.array(z.string()).describe("Optional placement constraints. When present, authorization only applies to these placement IDs from the top-level placements array in this file.").optional(), "placement_tags": z.array(z.string()).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Optional placement tag constraints. When present, authorization only applies to placements whose tags include any of these publisher-defined values.").optional(), "delegation_type": z.enum(["direct","delegated","ad_network"]).describe("Commercial relationship for this inventory path. 'direct' means the publisher treats this as a direct way to buy from them, even if a third party operates the software. 'delegated' means the agent is authorized to sell on the publisher's behalf. 'ad_network' means the inventory is sold as part of a network/package context rather than as the publisher's direct endpoint.").optional(), "exclusive": z.boolean().describe("Whether this agent is the publisher's sole authorized path for the scoped inventory slice. When false or absent, other authorized agents may also sell the same inventory.").optional(), "countries": z.array(z.string().regex(new RegExp("^[A-Z]{2}$"))).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Optional ISO 3166-1 alpha-2 country codes limiting where this authorization applies. Omit for worldwide authorization.").optional(), "effective_from": z.string().datetime().describe("Optional start time for this authorization window.").optional(), "effective_until": z.string().datetime().describe("Optional end time for this authorization window.").optional() }).catchall(z.any()).and(z.object({ "url": z.string().url().describe("The authorized agent's API endpoint URL. Callers comparing this URL against a registry (sales-agent list, signal-provider registry, TMP provider lookup, etc.) MUST canonicalize both sides per the AdCP URL canonicalization rules, not byte-equality — two URLs that differ only in case, default port, or percent-encoding of unreserved characters are the same agent. See docs/reference/url-canonicalization."), "authorized_for": z.string().min(1).max(500).describe("Human-readable description of what this agent is authorized to do — what it sells (for sales/property agents) or what data it provides (for signal agents). The variant the entry uses (`property_ids`, `signal_tags`, etc.) tells consumers which inflection applies; this field carries the operator-supplied label."), "signing_keys": z.array(z.object({ "kid": z.string().describe("Key identifier for selecting the correct signing key."), "kty": z.string().describe("JWK key type, such as 'OKP', 'EC', or 'RSA'."), "alg": z.string().describe("Expected signing algorithm for this key, such as 'EdDSA' or 'RS256'.").optional(), "use": z.string().describe("Optional JWK use value. Typically 'sig' for signing keys.").optional(), "crv": z.string().describe("Curve name for OKP or EC keys, such as 'Ed25519' or 'P-256'.").optional(), "x": z.string().describe("Base64url-encoded public key x coordinate or public key value for OKP keys.").optional(), "y": z.string().describe("Base64url-encoded public key y coordinate for EC keys.").optional(), "n": z.string().describe("Base64url-encoded RSA modulus.").optional(), "e": z.string().describe("Base64url-encoded RSA public exponent.").optional(), "revoked_at": z.string().datetime().describe("Optional revocation timestamp. When present, verifiers MUST reject any signature produced with this key whose signing epoch (or equivalent time reference) is at or after this timestamp. The key may continue to appear in the trust anchor during a grace period so caches that have not yet refreshed still find the key and can evaluate the revocation marker. Keys past their revocation can be removed once the cache TTL (recommended: 5 minutes) has elapsed across all verifiers.").optional() }).catchall(z.any()).describe("Publisher-attested public key material for an authorized agent. Buyers use these keys to verify signed agent responses against the trust anchor published in adagents.json rather than trusting key discovery from the agent domain alone.")).describe("Optional publisher-attested public signing keys for this agent. Use these as the trust anchor for verifying signed agent responses instead of relying on key discovery from the agent domain alone.").optional(), "encryption_keys": z.array(z.object({ "kid": z.string().max(8).describe("Key identifier. Opaque — MUST NOT encode geographic or deployment information."), "kty": z.literal("OKP").describe("JWK key type. Must be OKP for X25519."), "crv": z.literal("X25519").describe("Curve name. Must be X25519 for TMPX encryption."), "use": z.literal("enc").describe("JWK use value. Must be enc for encryption keys."), "x": z.string().describe("Base64url-encoded X25519 public key (32 bytes).") }).strict().describe("X25519 public key for HPKE encryption. Used for TMPX exposure token encryption with HPKE mode_base.")).describe("X25519 public keys for TMPX exposure token encryption. Each key identifies a cluster master that can decrypt TMPX tokens. Used with HPKE mode_base — read replicas encrypt with this public key, only the master can decrypt.").optional(), "last_updated": z.string().datetime().describe("Optional ISO 8601 timestamp indicating when this `authorized_agents[]` entry last changed. Independent of the file-level `last_updated`. Lets validators perform a partial walk by skipping entries whose `last_updated` is older than their indexed value. Advisory — consumers MAY ignore and re-index the full file.").optional() }).describe("Fields shared by every variant of `authorized_agents[*]` in adagents.json, regardless of authorization_type. Variants `allOf` this base and add their discriminator-specific fields. Centralized to prevent drift across all authorization-type variants.")), z.object({ "authorization_type": z.literal("property_tags").describe("Discriminator indicating authorization by property tags"), "property_tags": z.array(z.string().regex(new RegExp("^[a-z0-9_]+$")).describe("Tag for categorizing publisher properties. Must be lowercase alphanumeric with underscores only.")).describe("Tags identifying which properties this agent is authorized for. Resolved against the top-level properties array in this file using tag matching"), "collections": z.array(z.object({ "publisher_domain": z.string().regex(new RegExp("^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$")).describe("Domain where the adagents.json declaring these collections is hosted (e.g., 'mrbeast.com'). The collections array in that file contains the authoritative collection definitions."), "collection_ids": z.array(z.string()).describe("Collection IDs from the adagents.json collections array. Each ID must match a collection_id declared in that file.") }).catchall(z.any()).describe("References collections declared in an adagents.json. Buyers resolve full collection objects by fetching the adagents.json at the given domain and matching collection_ids against its collections array.")).describe("Optional collection constraints. When present, authorization only applies to inventory associated with these collections.").optional(), "placement_ids": z.array(z.string()).describe("Optional placement constraints. When present, authorization only applies to these placement IDs from the top-level placements array in this file.").optional(), "placement_tags": z.array(z.string()).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Optional placement tag constraints. When present, authorization only applies to placements whose tags include any of these publisher-defined values.").optional(), "delegation_type": z.enum(["direct","delegated","ad_network"]).describe("Commercial relationship for this inventory path. 'direct' means the publisher treats this as a direct way to buy from them, even if a third party operates the software. 'delegated' means the agent is authorized to sell on the publisher's behalf. 'ad_network' means the inventory is sold as part of a network/package context rather than as the publisher's direct endpoint.").optional(), "exclusive": z.boolean().describe("Whether this agent is the publisher's sole authorized path for the scoped inventory slice. When false or absent, other authorized agents may also sell the same inventory.").optional(), "countries": z.array(z.string().regex(new RegExp("^[A-Z]{2}$"))).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Optional ISO 3166-1 alpha-2 country codes limiting where this authorization applies. Omit for worldwide authorization.").optional(), "effective_from": z.string().datetime().describe("Optional start time for this authorization window.").optional(), "effective_until": z.string().datetime().describe("Optional end time for this authorization window.").optional() }).catchall(z.any()).and(z.object({ "url": z.string().url().describe("The authorized agent's API endpoint URL. Callers comparing this URL against a registry (sales-agent list, signal-provider registry, TMP provider lookup, etc.) MUST canonicalize both sides per the AdCP URL canonicalization rules, not byte-equality — two URLs that differ only in case, default port, or percent-encoding of unreserved characters are the same agent. See docs/reference/url-canonicalization."), "authorized_for": z.string().min(1).max(500).describe("Human-readable description of what this agent is authorized to do — what it sells (for sales/property agents) or what data it provides (for signal agents). The variant the entry uses (`property_ids`, `signal_tags`, etc.) tells consumers which inflection applies; this field carries the operator-supplied label."), "signing_keys": z.array(z.object({ "kid": z.string().describe("Key identifier for selecting the correct signing key."), "kty": z.string().describe("JWK key type, such as 'OKP', 'EC', or 'RSA'."), "alg": z.string().describe("Expected signing algorithm for this key, such as 'EdDSA' or 'RS256'.").optional(), "use": z.string().describe("Optional JWK use value. Typically 'sig' for signing keys.").optional(), "crv": z.string().describe("Curve name for OKP or EC keys, such as 'Ed25519' or 'P-256'.").optional(), "x": z.string().describe("Base64url-encoded public key x coordinate or public key value for OKP keys.").optional(), "y": z.string().describe("Base64url-encoded public key y coordinate for EC keys.").optional(), "n": z.string().describe("Base64url-encoded RSA modulus.").optional(), "e": z.string().describe("Base64url-encoded RSA public exponent.").optional(), "revoked_at": z.string().datetime().describe("Optional revocation timestamp. When present, verifiers MUST reject any signature produced with this key whose signing epoch (or equivalent time reference) is at or after this timestamp. The key may continue to appear in the trust anchor during a grace period so caches that have not yet refreshed still find the key and can evaluate the revocation marker. Keys past their revocation can be removed once the cache TTL (recommended: 5 minutes) has elapsed across all verifiers.").optional() }).catchall(z.any()).describe("Publisher-attested public key material for an authorized agent. Buyers use these keys to verify signed agent responses against the trust anchor published in adagents.json rather than trusting key discovery from the agent domain alone.")).describe("Optional publisher-attested public signing keys for this agent. Use these as the trust anchor for verifying signed agent responses instead of relying on key discovery from the agent domain alone.").optional(), "encryption_keys": z.array(z.object({ "kid": z.string().max(8).describe("Key identifier. Opaque — MUST NOT encode geographic or deployment information."), "kty": z.literal("OKP").describe("JWK key type. Must be OKP for X25519."), "crv": z.literal("X25519").describe("Curve name. Must be X25519 for TMPX encryption."), "use": z.literal("enc").describe("JWK use value. Must be enc for encryption keys."), "x": z.string().describe("Base64url-encoded X25519 public key (32 bytes).") }).strict().describe("X25519 public key for HPKE encryption. Used for TMPX exposure token encryption with HPKE mode_base.")).describe("X25519 public keys for TMPX exposure token encryption. Each key identifies a cluster master that can decrypt TMPX tokens. Used with HPKE mode_base — read replicas encrypt with this public key, only the master can decrypt.").optional(), "last_updated": z.string().datetime().describe("Optional ISO 8601 timestamp indicating when this `authorized_agents[]` entry last changed. Independent of the file-level `last_updated`. Lets validators perform a partial walk by skipping entries whose `last_updated` is older than their indexed value. Advisory — consumers MAY ignore and re-index the full file.").optional() }).describe("Fields shared by every variant of `authorized_agents[*]` in adagents.json, regardless of authorization_type. Variants `allOf` this base and add their discriminator-specific fields. Centralized to prevent drift across all authorization-type variants.")), z.object({ "authorization_type": z.literal("inline_properties").describe("Discriminator indicating authorization by inline property definitions. Companion field is `properties` (not `inline_properties`) — the only authorization_type whose companion field name does not mirror the discriminator value."), "properties": z.array(z.object({ "property_id": z.string().regex(new RegExp("^[a-z0-9_]+$")).describe("Unique identifier for this property (optional). Enables referencing properties by ID instead of repeating full objects.").optional(), "property_type": z.enum(["website","mobile_app","ctv_app","desktop_app","dooh","podcast","radio","linear_tv","streaming_audio","ai_assistant"]).describe("Type of advertising property"), "name": z.string().describe("Human-readable property name"), "identifiers": z.array(z.object({ "type": z.enum(["domain","subdomain","network_id","ios_bundle","android_package","apple_app_store_id","google_play_id","roku_store_id","fire_tv_asin","samsung_app_id","apple_tv_bundle","bundle_id","venue_id","screen_id","openooh_venue_type","rss_url","apple_podcast_id","spotify_collection_id","podcast_guid","station_id","facility_id"]).describe("Type of identifier for this property"), "value": z.string().describe("The identifier value. For domain type: 'example.com' matches base domain plus www and m subdomains; 'edition.example.com' matches that specific subdomain; '*.example.com' matches ALL subdomains but NOT base domain") }).catchall(z.any())).describe("Array of identifiers for this property"), "tags": z.array(z.string().regex(new RegExp("^[a-z0-9_]+$")).describe("Tag for categorizing publisher properties. Must be lowercase alphanumeric with underscores only.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Tags for categorization and grouping (e.g., network membership, content categories)").optional(), "supported_channels": z.array(z.enum(["display","olv","social","search","ctv","linear_tv","radio","streaming_audio","podcast","dooh","ooh","print","cinema","email","gaming","retail_media","influencer","affiliate","product_placement","sponsored_intelligence"]).describe("Standardized advertising media channels describing how buyers allocate budget. Channels are planning abstractions, not technical substrates. See the Media Channel Taxonomy specification for detailed definitions.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Advertising channels this property supports (e.g., ['display', 'olv', 'social']). Publishers declare which channels their inventory aligns with. Properties may support multiple channels. See the Media Channel Taxonomy for definitions.").optional(), "publisher_domain": z.string().describe("Domain where adagents.json should be checked for authorization validation. Optional in adagents.json (file location implies domain).").optional() }).catchall(z.any()).describe("An advertising property that can be validated via adagents.json")).describe("Specific properties this agent is authorized for, defined inline on the agent entry (alternative to property_ids/property_tags). Note: this is the companion field for `authorization_type: \"inline_properties\"` — the field is named `properties`, not `inline_properties`."), "collections": z.array(z.object({ "publisher_domain": z.string().regex(new RegExp("^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$")).describe("Domain where the adagents.json declaring these collections is hosted (e.g., 'mrbeast.com'). The collections array in that file contains the authoritative collection definitions."), "collection_ids": z.array(z.string()).describe("Collection IDs from the adagents.json collections array. Each ID must match a collection_id declared in that file.") }).catchall(z.any()).describe("References collections declared in an adagents.json. Buyers resolve full collection objects by fetching the adagents.json at the given domain and matching collection_ids against its collections array.")).describe("Optional collection constraints. When present, authorization only applies to inventory associated with these collections.").optional(), "placement_ids": z.array(z.string()).describe("Optional placement constraints. When present, authorization only applies to these placement IDs from the top-level placements array in this file.").optional(), "placement_tags": z.array(z.string()).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Optional placement tag constraints. When present, authorization only applies to placements whose tags include any of these publisher-defined values.").optional(), "delegation_type": z.enum(["direct","delegated","ad_network"]).describe("Commercial relationship for this inventory path. 'direct' means the publisher treats this as a direct way to buy from them, even if a third party operates the software. 'delegated' means the agent is authorized to sell on the publisher's behalf. 'ad_network' means the inventory is sold as part of a network/package context rather than as the publisher's direct endpoint.").optional(), "exclusive": z.boolean().describe("Whether this agent is the publisher's sole authorized path for the scoped inventory slice. When false or absent, other authorized agents may also sell the same inventory.").optional(), "countries": z.array(z.string().regex(new RegExp("^[A-Z]{2}$"))).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Optional ISO 3166-1 alpha-2 country codes limiting where this authorization applies. Omit for worldwide authorization.").optional(), "effective_from": z.string().datetime().describe("Optional start time for this authorization window.").optional(), "effective_until": z.string().datetime().describe("Optional end time for this authorization window.").optional() }).catchall(z.any()).and(z.object({ "url": z.string().url().describe("The authorized agent's API endpoint URL. Callers comparing this URL against a registry (sales-agent list, signal-provider registry, TMP provider lookup, etc.) MUST canonicalize both sides per the AdCP URL canonicalization rules, not byte-equality — two URLs that differ only in case, default port, or percent-encoding of unreserved characters are the same agent. See docs/reference/url-canonicalization."), "authorized_for": z.string().min(1).max(500).describe("Human-readable description of what this agent is authorized to do — what it sells (for sales/property agents) or what data it provides (for signal agents). The variant the entry uses (`property_ids`, `signal_tags`, etc.) tells consumers which inflection applies; this field carries the operator-supplied label."), "signing_keys": z.array(z.object({ "kid": z.string().describe("Key identifier for selecting the correct signing key."), "kty": z.string().describe("JWK key type, such as 'OKP', 'EC', or 'RSA'."), "alg": z.string().describe("Expected signing algorithm for this key, such as 'EdDSA' or 'RS256'.").optional(), "use": z.string().describe("Optional JWK use value. Typically 'sig' for signing keys.").optional(), "crv": z.string().describe("Curve name for OKP or EC keys, such as 'Ed25519' or 'P-256'.").optional(), "x": z.string().describe("Base64url-encoded public key x coordinate or public key value for OKP keys.").optional(), "y": z.string().describe("Base64url-encoded public key y coordinate for EC keys.").optional(), "n": z.string().describe("Base64url-encoded RSA modulus.").optional(), "e": z.string().describe("Base64url-encoded RSA public exponent.").optional(), "revoked_at": z.string().datetime().describe("Optional revocation timestamp. When present, verifiers MUST reject any signature produced with this key whose signing epoch (or equivalent time reference) is at or after this timestamp. The key may continue to appear in the trust anchor during a grace period so caches that have not yet refreshed still find the key and can evaluate the revocation marker. Keys past their revocation can be removed once the cache TTL (recommended: 5 minutes) has elapsed across all verifiers.").optional() }).catchall(z.any()).describe("Publisher-attested public key material for an authorized agent. Buyers use these keys to verify signed agent responses against the trust anchor published in adagents.json rather than trusting key discovery from the agent domain alone.")).describe("Optional publisher-attested public signing keys for this agent. Use these as the trust anchor for verifying signed agent responses instead of relying on key discovery from the agent domain alone.").optional(), "encryption_keys": z.array(z.object({ "kid": z.string().max(8).describe("Key identifier. Opaque — MUST NOT encode geographic or deployment information."), "kty": z.literal("OKP").describe("JWK key type. Must be OKP for X25519."), "crv": z.literal("X25519").describe("Curve name. Must be X25519 for TMPX encryption."), "use": z.literal("enc").describe("JWK use value. Must be enc for encryption keys."), "x": z.string().describe("Base64url-encoded X25519 public key (32 bytes).") }).strict().describe("X25519 public key for HPKE encryption. Used for TMPX exposure token encryption with HPKE mode_base.")).describe("X25519 public keys for TMPX exposure token encryption. Each key identifies a cluster master that can decrypt TMPX tokens. Used with HPKE mode_base — read replicas encrypt with this public key, only the master can decrypt.").optional(), "last_updated": z.string().datetime().describe("Optional ISO 8601 timestamp indicating when this `authorized_agents[]` entry last changed. Independent of the file-level `last_updated`. Lets validators perform a partial walk by skipping entries whose `last_updated` is older than their indexed value. Advisory — consumers MAY ignore and re-index the full file.").optional() }).describe("Fields shared by every variant of `authorized_agents[*]` in adagents.json, regardless of authorization_type. Variants `allOf` this base and add their discriminator-specific fields. Centralized to prevent drift across all authorization-type variants.")), z.object({ "authorization_type": z.literal("publisher_properties").describe("Discriminator indicating authorization for properties from other publisher domains"), "publisher_properties": z.array(z.union([z.object({ "publisher_domain": z.string().regex(new RegExp("^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$")).describe("Domain where publisher's adagents.json is hosted (e.g., 'cnn.com'). XOR with `publisher_domains` — exactly one MUST be present on each `publisher_properties[]` entry; both-present and neither-present both fail validation.").optional(), "publisher_domains": z.array(z.string().regex(new RegExp("^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$"))).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Compact form for fanning the same selector across many publishers (e.g., a managed network listing every publisher it represents). Each entry is the domain where that publisher's adagents.json is hosted. Each listed domain MUST be canonicalized to lowercase (the `pattern` already rejects uppercase). Mutually exclusive with `publisher_domain`. Each listed domain counts as explicitly scoped for the `managerdomain` fallback safety rule.").optional(), "selection_type": z.literal("all").describe("Discriminator indicating all properties from each addressed publisher are included") }).catchall(z.any()).and(z.intersection(z.any().refine((value) => !z.any().safeParse(value).success, "Invalid input: Should NOT be valid against schema"), z.union([z.any(), z.any()]))).describe("Select all properties from one publisher domain, or from each publisher domain when `publisher_domains` is used. Consumers MAY satisfy the selector from the parent file's top-level `properties[]` when those properties carry a `publisher_domain` matching one of the listed domains (see Resolution paths in the spec)."), z.object({ "publisher_domain": z.string().regex(new RegExp("^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$")).describe("Domain where publisher's adagents.json is hosted (e.g., 'cnn.com')."), "selection_type": z.literal("by_id").describe("Discriminator indicating selection by specific property IDs"), "property_ids": z.array(z.string().regex(new RegExp("^[a-z0-9_]+$")).describe("Identifier for a publisher property. Must be lowercase alphanumeric with underscores only.")).describe("Specific property IDs from the publisher's adagents.json") }).catchall(z.any()).describe("Select specific properties by ID. Single-publisher only — property IDs are publisher-scoped, so the compact `publisher_domains[]` form is intentionally NOT available for this selector. Use multiple `publisher_properties[]` entries (one per publisher) when each publisher's ID set differs."), z.object({ "publisher_domain": z.string().regex(new RegExp("^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$")).describe("Domain where publisher's adagents.json is hosted (e.g., 'cnn.com'). XOR with `publisher_domains` — exactly one MUST be present on each `publisher_properties[]` entry; both-present and neither-present both fail validation.").optional(), "publisher_domains": z.array(z.string().regex(new RegExp("^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$"))).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Compact form for fanning the same tag predicate across many publishers (canonical managed-network shape). Each entry is the domain where that publisher's adagents.json is hosted. Each listed domain MUST be canonicalized to lowercase (the `pattern` already rejects uppercase). Mutually exclusive with `publisher_domain`. Each listed domain counts as explicitly scoped for the `managerdomain` fallback safety rule.").optional(), "selection_type": z.literal("by_tag").describe("Discriminator indicating selection by property tags"), "property_tags": z.array(z.string().regex(new RegExp("^[a-z0-9_]+$")).describe("Tag for categorizing publisher properties. Must be lowercase alphanumeric with underscores only.")).describe("Property tags resolved against each addressed publisher's adagents.json, OR against the parent file's top-level `properties[]` when those properties carry a `publisher_domain` matching the selector. Selector covers all properties carrying any of these tags.") }).catchall(z.any()).and(z.intersection(z.any().refine((value) => !z.any().safeParse(value).success, "Invalid input: Should NOT be valid against schema"), z.union([z.any(), z.any()]))).describe("Select properties by tag membership. With `publisher_domains`, the same `property_tags` predicate is resolved against each listed publisher's adagents.json — the common managed-network case where every represented site tags inventory with a shared label. Consumers MAY also satisfy the predicate from the parent file's top-level `properties[]` when those properties carry a `publisher_domain` matching one of the selector's `publisher_domains[]` (see Resolution paths in the spec).")]).describe("Selects properties from a publisher's adagents.json. Used for both product definitions and agent authorization. Supports three selection patterns: all properties, specific IDs, or by tags. Each selector targets one publisher via `publisher_domain` (string) or a fan-out across many publishers that share the same selector via `publisher_domains` (array). Exactly one of `publisher_domain` or `publisher_domains` MUST be present. When `publisher_domains` is used, the selector is logically equivalent to repeating the same entry once per listed domain.")).describe("Properties from other publisher domains this agent is authorized for. Each entry specifies a publisher domain and which of their properties this agent can sell"), "collections": z.array(z.object({ "publisher_domain": z.string().regex(new RegExp("^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$")).describe("Domain where the adagents.json declaring these collections is hosted (e.g., 'mrbeast.com'). The collections array in that file contains the authoritative collection definitions."), "collection_ids": z.array(z.string()).describe("Collection IDs from the adagents.json collections array. Each ID must match a collection_id declared in that file.") }).catchall(z.any()).describe("References collections declared in an adagents.json. Buyers resolve full collection objects by fetching the adagents.json at the given domain and matching collection_ids against its collections array.")).describe("Optional collection constraints. When present, authorization only applies to inventory associated with these collections.").optional(), "placement_ids": z.array(z.string()).describe("Optional placement constraints. When present, authorization only applies to these placement IDs from the top-level placements array in this file.").optional(), "placement_tags": z.array(z.string()).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Optional placement tag constraints. When present, authorization only applies to placements whose tags include any of these publisher-defined values.").optional(), "delegation_type": z.enum(["direct","delegated","ad_network"]).describe("Commercial relationship for this inventory path. 'direct' means the publisher treats this as a direct way to buy from them, even if a third party operates the software. 'delegated' means the agent is authorized to sell on the publisher's behalf. 'ad_network' means the inventory is sold as part of a network/package context rather than as the publisher's direct endpoint.").optional(), "exclusive": z.boolean().describe("Whether this agent is the publisher's sole authorized path for the scoped inventory slice. When false or absent, other authorized agents may also sell the same inventory.").optional(), "countries": z.array(z.string().regex(new RegExp("^[A-Z]{2}$"))).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Optional ISO 3166-1 alpha-2 country codes limiting where this authorization applies. Omit for worldwide authorization.").optional(), "effective_from": z.string().datetime().describe("Optional start time for this authorization window.").optional(), "effective_until": z.string().datetime().describe("Optional end time for this authorization window.").optional() }).catchall(z.any()).and(z.object({ "url": z.string().url().describe("The authorized agent's API endpoint URL. Callers comparing this URL against a registry (sales-agent list, signal-provider registry, TMP provider lookup, etc.) MUST canonicalize both sides per the AdCP URL canonicalization rules, not byte-equality — two URLs that differ only in case, default port, or percent-encoding of unreserved characters are the same agent. See docs/reference/url-canonicalization."), "authorized_for": z.string().min(1).max(500).describe("Human-readable description of what this agent is authorized to do — what it sells (for sales/property agents) or what data it provides (for signal agents). The variant the entry uses (`property_ids`, `signal_tags`, etc.) tells consumers which inflection applies; this field carries the operator-supplied label."), "signing_keys": z.array(z.object({ "kid": z.string().describe("Key identifier for selecting the correct signing key."), "kty": z.string().describe("JWK key type, such as 'OKP', 'EC', or 'RSA'."), "alg": z.string().describe("Expected signing algorithm for this key, such as 'EdDSA' or 'RS256'.").optional(), "use": z.string().describe("Optional JWK use value. Typically 'sig' for signing keys.").optional(), "crv": z.string().describe("Curve name for OKP or EC keys, such as 'Ed25519' or 'P-256'.").optional(), "x": z.string().describe("Base64url-encoded public key x coordinate or public key value for OKP keys.").optional(), "y": z.string().describe("Base64url-encoded public key y coordinate for EC keys.").optional(), "n": z.string().describe("Base64url-encoded RSA modulus.").optional(), "e": z.string().describe("Base64url-encoded RSA public exponent.").optional(), "revoked_at": z.string().datetime().describe("Optional revocation timestamp. When present, verifiers MUST reject any signature produced with this key whose signing epoch (or equivalent time reference) is at or after this timestamp. The key may continue to appear in the trust anchor during a grace period so caches that have not yet refreshed still find the key and can evaluate the revocation marker. Keys past their revocation can be removed once the cache TTL (recommended: 5 minutes) has elapsed across all verifiers.").optional() }).catchall(z.any()).describe("Publisher-attested public key material for an authorized agent. Buyers use these keys to verify signed agent responses against the trust anchor published in adagents.json rather than trusting key discovery from the agent domain alone.")).describe("Optional publisher-attested public signing keys for this agent. Use these as the trust anchor for verifying signed agent responses instead of relying on key discovery from the agent domain alone.").optional(), "encryption_keys": z.array(z.object({ "kid": z.string().max(8).describe("Key identifier. Opaque — MUST NOT encode geographic or deployment information."), "kty": z.literal("OKP").describe("JWK key type. Must be OKP for X25519."), "crv": z.literal("X25519").describe("Curve name. Must be X25519 for TMPX encryption."), "use": z.literal("enc").describe("JWK use value. Must be enc for encryption keys."), "x": z.string().describe("Base64url-encoded X25519 public key (32 bytes).") }).strict().describe("X25519 public key for HPKE encryption. Used for TMPX exposure token encryption with HPKE mode_base.")).describe("X25519 public keys for TMPX exposure token encryption. Each key identifies a cluster master that can decrypt TMPX tokens. Used with HPKE mode_base — read replicas encrypt with this public key, only the master can decrypt.").optional(), "last_updated": z.string().datetime().describe("Optional ISO 8601 timestamp indicating when this `authorized_agents[]` entry last changed. Independent of the file-level `last_updated`. Lets validators perform a partial walk by skipping entries whose `last_updated` is older than their indexed value. Advisory — consumers MAY ignore and re-index the full file.").optional() }).describe("Fields shared by every variant of `authorized_agents[*]` in adagents.json, regardless of authorization_type. Variants `allOf` this base and add their discriminator-specific fields. Centralized to prevent drift across all authorization-type variants.")), z.object({ "authorization_type": z.literal("signal_ids").describe("Discriminator indicating authorization by specific signal IDs"), "signal_ids": z.array(z.string().regex(new RegExp("^[a-zA-Z0-9_-]+$"))).describe("Signal IDs this agent is authorized to resell. Resolved against the top-level signals array in this file") }).catchall(z.any()).and(z.object({ "url": z.string().url().describe("The authorized agent's API endpoint URL. Callers comparing this URL against a registry (sales-agent list, signal-provider registry, TMP provider lookup, etc.) MUST canonicalize both sides per the AdCP URL canonicalization rules, not byte-equality — two URLs that differ only in case, default port, or percent-encoding of unreserved characters are the same agent. See docs/reference/url-canonicalization."), "authorized_for": z.string().min(1).max(500).describe("Human-readable description of what this agent is authorized to do — what it sells (for sales/property agents) or what data it provides (for signal agents). The variant the entry uses (`property_ids`, `signal_tags`, etc.) tells consumers which inflection applies; this field carries the operator-supplied label."), "signing_keys": z.array(z.object({ "kid": z.string().describe("Key identifier for selecting the correct signing key."), "kty": z.string().describe("JWK key type, such as 'OKP', 'EC', or 'RSA'."), "alg": z.string().describe("Expected signing algorithm for this key, such as 'EdDSA' or 'RS256'.").optional(), "use": z.string().describe("Optional JWK use value. Typically 'sig' for signing keys.").optional(), "crv": z.string().describe("Curve name for OKP or EC keys, such as 'Ed25519' or 'P-256'.").optional(), "x": z.string().describe("Base64url-encoded public key x coordinate or public key value for OKP keys.").optional(), "y": z.string().describe("Base64url-encoded public key y coordinate for EC keys.").optional(), "n": z.string().describe("Base64url-encoded RSA modulus.").optional(), "e": z.string().describe("Base64url-encoded RSA public exponent.").optional(), "revoked_at": z.string().datetime().describe("Optional revocation timestamp. When present, verifiers MUST reject any signature produced with this key whose signing epoch (or equivalent time reference) is at or after this timestamp. The key may continue to appear in the trust anchor during a grace period so caches that have not yet refreshed still find the key and can evaluate the revocation marker. Keys past their revocation can be removed once the cache TTL (recommended: 5 minutes) has elapsed across all verifiers.").optional() }).catchall(z.any()).describe("Publisher-attested public key material for an authorized agent. Buyers use these keys to verify signed agent responses against the trust anchor published in adagents.json rather than trusting key discovery from the agent domain alone.")).describe("Optional publisher-attested public signing keys for this agent. Use these as the trust anchor for verifying signed agent responses instead of relying on key discovery from the agent domain alone.").optional(), "encryption_keys": z.array(z.object({ "kid": z.string().max(8).describe("Key identifier. Opaque — MUST NOT encode geographic or deployment information."), "kty": z.literal("OKP").describe("JWK key type. Must be OKP for X25519."), "crv": z.literal("X25519").describe("Curve name. Must be X25519 for TMPX encryption."), "use": z.literal("enc").describe("JWK use value. Must be enc for encryption keys."), "x": z.string().describe("Base64url-encoded X25519 public key (32 bytes).") }).strict().describe("X25519 public key for HPKE encryption. Used for TMPX exposure token encryption with HPKE mode_base.")).describe("X25519 public keys for TMPX exposure token encryption. Each key identifies a cluster master that can decrypt TMPX tokens. Used with HPKE mode_base — read replicas encrypt with this public key, only the master can decrypt.").optional(), "last_updated": z.string().datetime().describe("Optional ISO 8601 timestamp indicating when this `authorized_agents[]` entry last changed. Independent of the file-level `last_updated`. Lets validators perform a partial walk by skipping entries whose `last_updated` is older than their indexed value. Advisory — consumers MAY ignore and re-index the full file.").optional() }).describe("Fields shared by every variant of `authorized_agents[*]` in adagents.json, regardless of authorization_type. Variants `allOf` this base and add their discriminator-specific fields. Centralized to prevent drift across all authorization-type variants.")).describe("Authorization for signals by specific signal IDs"), z.object({ "authorization_type": z.literal("signal_tags").describe("Discriminator indicating authorization by signal tags"), "signal_tags": z.array(z.string().regex(new RegExp("^[a-z0-9_-]+$"))).describe("Signal tags this agent is authorized for. Agent can resell all signals with these tags") }).catchall(z.any()).and(z.object({ "url": z.string().url().describe("The authorized agent's API endpoint URL. Callers comparing this URL against a registry (sales-agent list, signal-provider registry, TMP provider lookup, etc.) MUST canonicalize both sides per the AdCP URL canonicalization rules, not byte-equality — two URLs that differ only in case, default port, or percent-encoding of unreserved characters are the same agent. See docs/reference/url-canonicalization."), "authorized_for": z.string().min(1).max(500).describe("Human-readable description of what this agent is authorized to do — what it sells (for sales/property agents) or what data it provides (for signal agents). The variant the entry uses (`property_ids`, `signal_tags`, etc.) tells consumers which inflection applies; this field carries the operator-supplied label."), "signing_keys": z.array(z.object({ "kid": z.string().describe("Key identifier for selecting the correct signing key."), "kty": z.string().describe("JWK key type, such as 'OKP', 'EC', or 'RSA'."), "alg": z.string().describe("Expected signing algorithm for this key, such as 'EdDSA' or 'RS256'.").optional(), "use": z.string().describe("Optional JWK use value. Typically 'sig' for signing keys.").optional(), "crv": z.string().describe("Curve name for OKP or EC keys, such as 'Ed25519' or 'P-256'.").optional(), "x": z.string().describe("Base64url-encoded public key x coordinate or public key value for OKP keys.").optional(), "y": z.string().describe("Base64url-encoded public key y coordinate for EC keys.").optional(), "n": z.string().describe("Base64url-encoded RSA modulus.").optional(), "e": z.string().describe("Base64url-encoded RSA public exponent.").optional(), "revoked_at": z.string().datetime().describe("Optional revocation timestamp. When present, verifiers MUST reject any signature produced with this key whose signing epoch (or equivalent time reference) is at or after this timestamp. The key may continue to appear in the trust anchor during a grace period so caches that have not yet refreshed still find the key and can evaluate the revocation marker. Keys past their revocation can be removed once the cache TTL (recommended: 5 minutes) has elapsed across all verifiers.").optional() }).catchall(z.any()).describe("Publisher-attested public key material for an authorized agent. Buyers use these keys to verify signed agent responses against the trust anchor published in adagents.json rather than trusting key discovery from the agent domain alone.")).describe("Optional publisher-attested public signing keys for this agent. Use these as the trust anchor for verifying signed agent responses instead of relying on key discovery from the agent domain alone.").optional(), "encryption_keys": z.array(z.object({ "kid": z.string().max(8).describe("Key identifier. Opaque — MUST NOT encode geographic or deployment information."), "kty": z.literal("OKP").describe("JWK key type. Must be OKP for X25519."), "crv": z.literal("X25519").describe("Curve name. Must be X25519 for TMPX encryption."), "use": z.literal("enc").describe("JWK use value. Must be enc for encryption keys."), "x": z.string().describe("Base64url-encoded X25519 public key (32 bytes).") }).strict().describe("X25519 public key for HPKE encryption. Used for TMPX exposure token encryption with HPKE mode_base.")).describe("X25519 public keys for TMPX exposure token encryption. Each key identifies a cluster master that can decrypt TMPX tokens. Used with HPKE mode_base — read replicas encrypt with this public key, only the master can decrypt.").optional(), "last_updated": z.string().datetime().describe("Optional ISO 8601 timestamp indicating when this `authorized_agents[]` entry last changed. Independent of the file-level `last_updated`. Lets validators perform a partial walk by skipping entries whose `last_updated` is older than their indexed value. Advisory — consumers MAY ignore and re-index the full file.").optional() }).describe("Fields shared by every variant of `authorized_agents[*]` in adagents.json, regardless of authorization_type. Variants `allOf` this base and add their discriminator-specific fields. Centralized to prevent drift across all authorization-type variants.")).describe("Authorization for signals by tag membership")])).describe("Array of sales agents authorized to make inventory from this file available to buyers. Authorization can be scoped to specific properties, collections, countries, and time windows, with optional delegation metadata indicating whether the path is direct, delegated, or network-mediated. MAY be empty (`[]`) for a catalog-only community mirror — a file that publishes `formats`/`properties`/`placements` (with `catalog_etag`) for a platform that has not adopted AdCP, where there is no sales agent to authorize (see `catalog_etag`, `formats`, `superseded_by`). An empty array asserts *no sales authorization*: validators MUST NOT read it as deny-all, authorize-all, or a revocation, MUST NOT treat its presence as an error, and MUST still consume the catalog arrays."), "last_updated": z.string().datetime().describe("ISO 8601 timestamp indicating when this file was last updated").optional(), "property_features": z.array(z.object({ "url": z.string().url().describe("The agent's API endpoint URL. Callers comparing this URL against a feature-provider registry MUST canonicalize both sides per the AdCP URL canonicalization rules, not byte-equality. See docs/reference/url-canonicalization."), "name": z.string().describe("Human-readable name of the vendor/agent (e.g., 'Scope3', 'TAG', 'OneTrust')"), "features": z.array(z.string()).describe("Feature IDs this agent provides (e.g., 'carbon_score', 'tag_certified_against_fraud'). Use get_adcp_capabilities on the agent for full definitions."), "publisher_id": z.string().describe("Optional publisher identifier at this agent (for lookup)").optional() }).catchall(z.any())).describe("[AdCP 3.0] Optional list of agents that provide property feature data (certifications, scores, compliance status). Used for discovery - actual data is accessed through property list filters.").optional(), "signals": z.array(z.object({ "id": z.string().regex(new RegExp("^[a-zA-Z0-9_-]+$")).describe("Signal identifier within the publishing domain's adagents.json signals[]"), "name": z.string().min(1).max(255).describe("Human-readable signal name"), "description": z.string().max(2000).describe("Detailed description of what this signal represents and how it's derived").optional(), "value_type": z.enum(["binary","categorical","numeric"]).describe("The data type of this signal's values"), "tags": z.array(z.string().regex(new RegExp("^[a-z0-9_-]+$"))).describe("Tags for grouping and filtering this domain's published signal definitions").optional(), "allowed_values": z.array(z.string()).describe("For categorical signals, the valid values users can be assigned").optional(), "restricted_attributes": z.array(z.enum(["racial_ethnic_origin","political_opinions","religious_beliefs","trade_union_membership","health_data","sex_life_sexual_orientation","genetic_data","biometric_data","age","familial_status"]).describe("Personal data categories that may be restricted from use in audience targeting. Combines GDPR Article 9 special categories with US civil-rights protected classes (FHA familial_status, ADEA age). Used in two places: (1) on campaign plans via restricted_attributes to declare which categories are prohibited, and (2) on signal-definition.json via restricted_attributes to declare which categories a signal touches. Governance agents match plan restrictions against signal declarations for structural validation.")).describe("Restricted attribute categories this signal touches. Data providers SHOULD declare these so governance agents can structurally match signals against a plan's restricted_attributes without relying on semantic inference from the signal name or description.").optional(), "demographic_predicate": z.object({ "age": z.object({ "min": z.number().int().gte(0).lte(150).describe("Inclusive minimum age in completed years. Omit for an open lower bound.").optional(), "max": z.number().int().gte(0).lte(150).describe("Inclusive maximum age in completed years. Omit for an open upper bound.").optional(), "include_unknown": z.boolean().describe("Whether delivery to people whose age is unavailable is part of this predicate. This field has no default and MUST be supplied.") }).catchall(z.any()).and(z.union([z.any(), z.any()])).describe("A canonical audience-age predicate in completed integer years. min and max are inclusive; omitting one bound means no restriction in that direction. At least one bound is required. include_unknown is always explicit because people whose age is unavailable are not members of any numeric interval. Implementations MUST reject min greater than max; JSON Schema draft-07 cannot compare sibling numeric values.") }).catchall(z.any()).describe("Authoritative machine-readable demographic meaning for this signal. Signal names and taxonomy labels alone never establish exact demographic equivalence. Signals carrying this field MUST also declare restricted_attributes including age.").optional(), "policy_categories": z.array(z.string()).describe("Policy categories this signal is sensitive for (e.g., a children's interest signal declares ['children_directed']). Governance agents match these against a plan's policy_categories to flag sensitive data usage.").optional(), "range": z.object({ "min": z.number().describe("Minimum value"), "max": z.number().describe("Maximum value"), "unit": z.string().describe("Unit of measurement (e.g., 'score', 'dollars', 'years')").optional() }).strict().describe("For numeric signals, the valid value range").optional(), "taxonomy": z.object({ "ref": z.string().url().describe("URI identifying the taxonomy or taxonomy documentation."), "version": z.string().describe("Version identifier for the taxonomy when the taxonomy has versioned definitions.").optional(), "segtax": z.number().int().gte(1).describe("OpenRTB segtax code when the taxonomy maps to an OpenRTB segment taxonomy.").optional(), "etag": z.string().describe("Optional validator for custom taxonomy definitions so consumers can detect drift between signal publication and taxonomy resolution.").optional(), "values": z.array(z.object({ "id": z.string().min(1).describe("Taxonomy node identifier."), "path": z.string().describe("Optional human-readable or taxonomy-native path for display and review.").optional(), "modifiers": z.array(z.string()).describe("Optional taxonomy-specific modifiers that qualify the node.").optional() }).strict()).describe("Taxonomy node values that describe this signal. These are meaning/discovery metadata for the signal definition, not the values a buyer submits in package targeting expressions."), "value_mappings": z.array(z.object({ "value": z.string().describe("Categorical value from allowed_values[]."), "taxonomy_value_id": z.string().describe("Taxonomy node identifier corresponding to this categorical value."), "path": z.string().describe("Optional human-readable or taxonomy-native path for display and review.").optional(), "modifiers": z.array(z.string()).describe("Optional taxonomy-specific modifiers that qualify the mapped value.").optional() }).strict()).describe("For categorical signals, maps package-targeting allowed_values[] strings to stable taxonomy node identifiers. The allowed_values[] strings remain the wire values buyers submit; this mapping explains how those strings resolve into the published taxonomy. Each value_mappings[].value SHOULD match one of the signal's allowed_values[] entries; JSON Schema draft-07 cannot enforce this cross-array constraint.").optional(), "parent_match_behavior": z.enum(["exact_only","descendants_supported","unknown"]).describe("Whether this signal definition supports treating a parent taxonomy node as matching descendant nodes for discovery/filtering or seller-side expansion. 'exact_only' means only explicitly listed node ids match. 'descendants_supported' means the seller can expand known, version-pinned parent nodes to descendants internally, typically by ORing the children in its execution system. 'unknown' means the provider has not declared parent-node behavior. This field is metadata about discovery/translation behavior, not a package targeting operator.").optional() }).strict().describe("Optional taxonomy metadata describing what this signal means in an external audience, content, retail-media, or provider-owned taxonomy. Taxonomy metadata does not create a new value_type and does not change package targeting grammar: buyers still target the named signal according to value_type. When a taxonomy value is a parent node, parent/descendant expansion is seller behavior and must be declared through parent_match_behavior rather than assumed.").optional(), "segmentation_criteria": z.string().max(500).describe("Rules governing inclusion of identifiers in the segment. Aligns with IAB Data Transparency Standard audience criteria disclosure.").optional(), "criteria_url": z.string().url().describe("Optional URL to a longer-form methodology or criteria document. This is a disclosure pointer; buyers should not branch programmatically on the linked content.").optional(), "data_sources": z.array(z.enum(["app_behavior","app_usage","web_usage","geo_location","email","tv_ott_or_stb_device","panel","online_ecommerce","credit_data","loyalty_card","transaction","online_survey","offline_survey","public_record_census","public_record_voter_file","public_record_other","offline_transaction"])).describe("Origin categories of the raw data used to compile the signal, aligned with IAB Data Transparency Standard source disclosure. Use 'panel' for respondent-panel or JIC-style audience sources. Offline and public-record sources require onboarder disclosure. Co-viewing projection and reconciliation of seller claims against a JIC or measurement vendor belong in measurement reporting/vendor metrics rather than package signal targeting.").optional(), "methodology": z.enum(["observed","declared","derived","inferred","modeled"]).describe("How the signal's audience membership or attribute was determined. 'modeled' requires the modeling block.").optional(), "audience_expansion": z.boolean().describe("Whether look-alike or similar-audience expansion was used to include additional identifiers. When true, modeling is required.").optional(), "device_expansion": z.boolean().describe("Whether the signal was expanded deterministically across devices of the same user, household, or business. Probabilistic cross-device expansion is modeling and should use methodology 'modeled' or the modeling block.").optional(), "refresh_cadence": z.enum(["intra_day","daily","weekly","monthly","bi_monthly","quarterly","bi_annually","annually"]).describe("Cadence at which the signal definition's underlying segment membership is refreshed.").optional(), "lookback_window": z.enum(["intra_day","daily","weekly","monthly","bi_monthly","quarterly","bi_annually","annually"]).describe("Time window in which a qualifying event can occur for inclusion.").optional(), "onboarder": z.object({ "match_keys": z.array(z.enum(["name","address","email","postal","lat_long","mobile_id","cookie_id","ip","customer_id","phone"])), "pre_onboarding_audience_expansion": z.boolean().optional(), "pre_onboarding_device_expansion": z.boolean().optional(), "pre_onboarding_precision_level": z.enum(["individual","household","business","geography"]).optional() }).strict().describe("Onboarder disclosure. Required when data_sources includes an offline_* or public_record_* source.").optional(), "subject_type": z.enum(["individual","household","business","contextual","none"]).describe("What kind of subject this signal characterizes.").optional(), "resolution_method": z.enum(["deterministic_id","probabilistic_device","browser","geographic","content_signal","mixed"]).describe("How the subject is resolved at decision time.").optional(), "id_types": z.array(z.enum(["cookie","mobile_id","platform_id","user_enabled_id"])).describe("Identifier currencies analyzed to determine audience membership or attributes.").optional(), "audience_scope": z.enum(["single_domain","cross_domain_owned","cross_domain_unowned","offline"]).describe("Context within which the audience attribute was determined. 'single_domain' requires originating_domain.").optional(), "originating_domain": z.string().describe("Domain of the digital property where the audience originates. Required when audience_scope is 'single_domain'.").optional(), "countries": z.array(z.string().regex(new RegExp("^[A-Z]{2}$"))).describe("ISO 3166-1 alpha-2 country codes where the signal is applicable. Sellers must not expose a signal for media buys in countries outside this list. Federating agents that surface a peer's signal MUST treat the peer-published list as an upper bound, re-check it against the buyer's intended deployment countries, and may apply only narrower local policy.").optional(), "consent_basis": z.array(z.enum(["consent","legitimate_interest","contract","legal_obligation"]).describe("Common GDPR lawful bases relevant to advertising. Covers the Article 6(1) bases used in programmatic advertising contexts.")).describe("Declared GDPR Article 6 lawful basis or consent basis under which this signal's data is processed. For non-GDPR regimes, use countries, policy_categories, and disclosure fields to describe jurisdiction-specific obligations unless a future enum value applies.").optional(), "art9_basis": z.enum(["explicit_consent","manifestly_made_public","substantial_public_interest","vital_interests"]).describe("GDPR Article 9 basis when restricted_attributes is non-empty and the signal is used in jurisdictions where Article 9 applies. Required by policy for applicable use cases rather than universally required at schema level because sensitivity and lawful basis are jurisdiction-relative.").optional(), "modeling": z.object({ "method": z.enum(["lookalike","supervised","embedding","rules"]), "seed_source": z.object({ "type": z.enum(["first_party_crm","panel","declared_survey","transactional","behavioral"]), "provider_signed": z.boolean().describe("Whether the seed source carries a signed attestation under one of the provider's published signing keys. This is a forward-looking claim until the consumer can resolve and verify the provider's applicable signing keys through the AdCP signing profile; consumers MUST NOT treat this boolean alone as cryptographic proof.") }).strict(), "training_data_jurisdictions": z.array(z.string().regex(new RegExp("^[A-Z]{2}$"))).describe("ISO 3166-1 alpha-2 country codes where the model's training data was collected."), "ai_act_risk_class": z.enum(["minimal","limited","high_risk"]).describe("EU AI Act risk classification self-declared by the provider. Prohibited-risk modeled signals must not be published as AdCP signal definitions."), "disclosure": z.object({ "required": z.boolean().describe("The provider's claim that a modeling or AI-use disclosure is required for this signal in at least one applicable jurisdiction. This is a declared compliance signal, not a protocol-level legal determination."), "jurisdictions": z.array(z.object({ "country": z.string().regex(new RegExp("^[A-Z]{2}$")).describe("ISO 3166-1 alpha-2 country code."), "region": z.string().describe("Provider-defined sub-national region code or name when the obligation is regional. No global canonical format is implied.").optional(), "regulation": z.string().describe("Provider-supplied regulation identifier for the disclosure obligation."), "disclosure_text": z.string().describe("Human-readable disclosure text or summary the provider expects buyers or reviewers to see.").optional(), "disclosure_url": z.string().url().describe("Optional URL to the provider's canonical disclosure or methodology page for this jurisdiction.").optional(), "audience": z.enum(["buyer","data_subject","regulator","public"]).describe("Primary audience for this disclosure entry.").optional() }).strict()).describe("Jurisdictions where a modeling or AI-use disclosure applies.").optional(), "notes": z.string().max(2000).describe("Optional provider notes on how the disclosure should be interpreted. Informational only; buyers should not branch programmatically on this text.").optional() }).strict().and(z.any()).describe("Signal/modeling-specific disclosure requirements and jurisdictional notes. This is not creative provenance render guidance.").optional() }).strict().describe("Modeling disclosure for modeled data signals. Required when methodology is 'modeled' or audience_expansion is true. This describes data modeling and intentionally does not reuse creative provenance, which is content/render oriented.").optional(), "data_subject_rights": z.object({ "upstream_source_domain": z.string().max(253).describe("Domain of the upstream data source whose rights process these channels reach, when different from the publishing domain.").optional(), "channels": z.array(z.object({ "rights": z.array(z.enum(["access","rectification","erasure","portability","objection"])).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Rights supported by this channel."), "url": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL for submitting the rights request.").optional(), "email": z.string().email().describe("Email address for submitting the rights request.").optional(), "languages": z.array(z.string()).describe("BCP 47 language tags supported by this channel.").optional(), "countries": z.array(z.string().regex(new RegExp("^[A-Z]{2}$"))).describe("ISO 3166-1 alpha-2 countries this channel serves, when the provider routes rights by country.").optional() }).strict().and(z.union([z.any(), z.any()]))).describe("Rights request channels and the rights each channel supports. At least one declared channel MUST support one or more of access, erasure, or objection; schema validators enforce this with draft-07 contains, and consumers whose SDK drops contains need an equivalent runtime check."), "response_sla_days": z.number().int().gte(1).lte(90).describe("Maximum response time in days for rights requests handled through the declared rights channels. For provider-published signals, providers SHOULD avoid duplicating this field across every signal unless the value varies by signal or upstream source; consumers MAY also consult the provider's public privacy policy or registry disclosures when present.").optional(), "ccpa_opt_out_url": z.string().url().regex(new RegExp("^https://")).describe("US-specific 'Do Not Sell or Share' opt-out URL where required.").optional() }).strict().describe("Per-signal data-subject-rights routing. Inline on the signal because upstream source, rights routing, and response commitments can differ by segment or may be unavailable from a public provider document for custom/private signals. This is a contact/routing reference, not a machine-callable AdCP API.").optional(), "last_updated": z.string().datetime().describe("When this definition record was last updated. This indicates freshness of the definition record, not an attestation that the underlying data or model was refreshed at that time.").optional(), "dts_compliant_version": z.string().describe("IAB Data Transparency Standard version this signal definition self-attests as satisfying, when applicable.").optional() }).catchall(z.any()).and(z.intersection(z.intersection(z.any(), z.intersection(z.any(), z.any())), z.intersection(z.any(), z.intersection(z.any(), z.any())))).describe("Definition of a signal in published adagents.json signals[]. The publishing domain supplies the namespace, so this definition carries a local id rather than a signal_ref. Media-buy products reference this definition with signal_ref scope 'data_provider', data_provider_domain set to the publishing domain, and signal_id set to this id. Some constraints use JSON Schema draft-07 conditional keywords such as if/then and contains; SDK generators that drop those keywords need equivalent runtime guards.")).describe("Signal definitions published by this domain. Each entry defines a signal id within this file's publishing-domain namespace; entries do not include signal_ref objects. Signals Protocol discovery and media-buy product targeting reference these through signal_ref scope 'data_provider', with data_provider_domain set to this file's publishing domain and signal_id set to signals[].id.").optional(), "signal_tags": z.record(z.string(), z.object({ "name": z.string().describe("Human-readable name for this tag"), "description": z.string().describe("Description of what this tag represents") }).catchall(z.any())).describe("Metadata for each tag referenced by signals. Provides human-readable context for signal tag values.").optional() }).catchall(z.any()).and(z.intersection(z.union([z.any(), z.any(), z.any(), z.any(), z.any(), z.any()]), z.any())).describe("Inline structure variant - contains full agent authorization data")]).describe("Declaration of authorized agents for advertising inventory and data signals. Hosted at /.well-known/adagents.json on publisher domains (for properties) or data provider domains (for signals). Can either contain the full structure inline or reference an authoritative URL.") +})), z.intersection(z.any().refine((value) => !z.intersection(z.any(), z.any()).safeParse(value).success, "Invalid input: Should NOT be valid against schema"), z.intersection(z.any().refine((value) => !z.any().safeParse(value).success, "Invalid input: Should NOT be valid against schema"), z.any())))).describe("Inline format declaration on a product. The `format_kind` discriminator names which canonical format the product narrows; `params` carries the canonical's parameter schema (slots, dimensions, durations, codecs, character limits, platform_extensions, etc.). Optional `format_option_id` (stable identifier for routing when a product's `format_options` contains multiple declarations sharing the same `format_kind`), optional `publisher_domain` (namespace for the format option when it comes from a publisher adagents.json catalog), `display_name` (seller-controlled human-readable label for dashboard and catalog UIs), and `applies_to_channels` (subset of the product's declared channels this declaration applies to — lets a multi-channel product carry distinct format_options per channel). Discriminated-union shape generates clean tagged unions in TypeScript and Pydantic codegen. Replaces v1's named-format pattern (where products referenced a separately-defined format file via compound `format_id`). Canonical product-bound declarations are the AdCP 3.2 authoring path; v1 named formats remain only as deprecated 3.x compatibility projections.\n\n**Closed-set semantics (normative).** `format_options[]` is the closed set of accepted formats for this product. Sellers MUST reject `create_media_buy` requests targeting any `format_kind` (or format option reference) not present in this list — typically with `UNSUPPORTED_FEATURE` or a seller-specific code; the rejection is structural, not negotiable. `seller_preference` modulates *within* the accepted set (a soft ranking hint between equally-acceptable options), it is NOT an enforcement axis. A product wanting to say 'this format is the only one that works' lists exactly that one entry in `format_options[]`; everything else falls outside the set and is rejected by the closed-set rule.\n\n**Format matching vs satisfaction (normative).** Legacy named formats MUST be normalized to canonical declarations before comparison; do not exact-match raw `(agent_url, id)` pairs once a `format_id` has been projected through `canonical`, `v1_format_ref`, or the canonical mapping registry. Equivalence matching can treat a legacy fixed-size display ID and `format_kind: \"image\"` with matching `width`/`height` as the same underlying shape. Product satisfaction is stricter and directional: when this declaration specifies fixed constraints such as `width`, `height`, `duration_ms_exact`, or `duration_ms_range`, a buyer request or creative manifest MUST declare and satisfy those constraints. A broad request with no dimensions or duration does not satisfy a fixed-size or fixed-duration product; a broad product MAY accept a more specific creative unless another product constraint excludes it. Duration precedence is `duration_ms_exact` > `duration_ms_range`. Range constraints use containment: a range-based request satisfies this declaration only when every value it permits falls within this declaration's accepted range; overlap alone is insufficient. An exact value satisfies a range when the exact value falls inside the accepted interval. For hosted audio/video, a null range endpoint is unbounded: [null, 60000] means up to 60s, and [15000, null] means at least 15s; [null, null] is invalid because at least one endpoint must be bounded.\n\n**Custom format_kind** (`format_kind: \"custom\"`): for adopter-defined shapes that don't fit the 14 canonicals (branded content, cross-screen sponsorship, sponsorship lockup, newsletter sponsorship, AR lens, playable, live event sponsorship). When `format_kind` is `custom`, the declaration MUST carry `format_shape` (recognized global pattern from the [format-shape vocabulary registry](/schemas/core/format-shape-vocabulary.json)) AND `format_schema` (URI+digest reference to a fetchable schema describing the actual `params` and `slots`). Buyer agents fetch the schema, validate manifests structurally, and reason about manifests without per-seller integration code. See [adcp#3666](https://github.com/adcontextprotocol/adcp/issues/3666) for the canonical promotion queue."), z.object({ "applies_to_property_ids": z.array(z.string().regex(new RegExp("^[a-z0-9_]+$")).describe("Identifier for a publisher property. Must be lowercase alphanumeric with underscores only.")).describe("Optional property IDs from this file's `properties[]` that this format declaration applies to. When omitted, the declaration applies to all properties in the file. Mutually compatible with `applies_to_property_tags` (union is the effective scope). Example: Meta declares Reels with `applies_to_property_ids: [\"instagram\", \"facebook\"]` because WhatsApp doesn't carry Reels inventory.").optional(), "applies_to_property_tags": z.array(z.string().regex(new RegExp("^[a-z0-9_]+$")).describe("Tag for categorizing publisher properties. Must be lowercase alphanumeric with underscores only.")).describe("Optional property tags from this file's `tags` map that this format declaration applies to. When omitted, the declaration applies to all properties in the file. Useful for network-wide formats (e.g., a managed network declaring `applies_to_property_tags: [\"premium_video\"]`). Compatible with `applies_to_property_ids` — a property is in scope if it matches either ID list or tag list.").optional(), "reference_renderer": z.object({ "runtime": z.literal("browser-esm").describe("Execution contract for the referenced package. browser-esm means a browser-safe ECMAScript module that accepts canonical manifest data and returns an inert presentation without Node.js APIs, ambient credentials, delivery tracking, or undeclared network access. Non-JavaScript clients use a hosted preview_creative provider or display the manifest."), "package": z.string().regex(new RegExp("^(?:@[a-z0-9][a-z0-9._~-]*/)?[a-z0-9][a-z0-9._~-]*$")).describe("npm package name, scoped or unscoped. The package is resolved from the npm registry; the AdCP registry does not proxy its executable contents."), "version": z.string().regex(new RegExp("^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$")).describe("Exact semantic version. Ranges and tags such as latest are forbidden so the registry entry is reproducible. Package semantic versioning identifies the pinned distribution artifact; it is independent of any one format revision because one package may expose renderers for multiple formats."), "export": z.string().min(1).describe("Named package export that implements the renderer contract for this enclosing format entry. Compatibility is bound at the export-to-entry edge, not to the package major: the export's documented input/output contract MUST implement the enclosing format entry's revision. When that format entry moves to a new major revision, the registry MUST point it to a compatible export, rotating package version and integrity only when the selected artifact changes. One package version MAY expose different named exports for different formats or format revisions."), "format_revision": z.string().regex(new RegExp("^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)$")).describe("Exact canonical format-entry revision implemented by this named export. It MUST equal the enclosing community format entry's format_revision; compatibility binds to this export edge, not the package major."), "integrity": z.string().regex(new RegExp("^(?:sha256-[A-Za-z0-9+/]{43}=|sha384-[A-Za-z0-9+/]{64}|sha512-[A-Za-z0-9+/]{86}==)$")).describe("Subresource Integrity value for the exact npm package tarball. Consumers MUST compare this value before loading code, require the provenance subject digest to match the same tarball, and fail closed on mismatch."), "provenance": z.object({ "source_repository": z.string().url().regex(new RegExp("^https://github\\.com/")).describe("Allowlisted source repository that the npm provenance attestation MUST identify."), "workflow_path": z.string().regex(new RegExp("^\\.github/workflows/[A-Za-z0-9._/-]+\\.ya?ml$")).describe("Repository-relative GitHub Actions workflow path that npm provenance buildDefinition.externalParameters.workflow.path MUST identify.") }).strict() }).strict().describe("Optional only for an AgenticAdvertising.org community-registry format entry fetched from the configured registry origin. Points to a pinned browser-ESM OSS renderer package export that provides a non-authoritative reference presentation. The named export's contract, not the package major, MUST implement this enclosing format entry's revision. Non-JavaScript clients use hosted preview_creative or display the manifest. Publisher-origin catalogs and seller product declarations do not use this field: publishers describe placement-specific chrome with placements[].presentation_ref and grant route-specific preview authority with placements[].preview_provider.").optional(), "format_revision": z.string().regex(new RegExp("^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)$")).describe("Registry-managed revision of this community format entry. Required when reference_renderer is present and MUST equal reference_renderer.format_revision.").optional() }).and(z.any()))).describe("Publisher-authoritative format catalog. Declares the 3.1+ canonical format-option shapes the publisher supports across its properties — the single place a publisher (or its community-registry stand-in) asserts \"these are the formats my inventory accepts.\" Products selling this publisher's inventory SHOULD reference these declarations by `format_option_id` on the placement or via inline `format_options` whose `format_option_id` matches an entry here, eliminating the N-copies-of-Meta-Reels-on-N-products drift surface.\n\nEach item is a `ProductFormatDeclaration` (same 3.1+ canonical format-option shape used on Products) plus optional `applies_to_property_ids` / `applies_to_property_tags` for property scoping within the file. A `formats[]` entry without scope applies to all properties in the file; with scope, only to the named subset (e.g., Reels applies to Instagram + Facebook but not WhatsApp).\n\n**Community registry pattern (normative for unadopted platforms).** When a platform hasn't adopted AdCP (Meta, TikTok, Snap, Pinterest, etc.), AAO publishes a community-maintained adagents.json at `https://creative.adcontextprotocol.org/translated//adagents.json` carrying that platform's `formats[]`. Buyer SDKs fetch the platform's own `/.well-known/adagents.json` first; on 404 or absence-of-formats[], they fall back to the AAO mirror. When the platform adopts AdCP and publishes their own adagents.json with `formats[]`, the platform-hosted file takes precedence and the mirror entry becomes redundant (AAO maintainers deprecate it).\n\nThe `v1_format_ref.agent_url` on each declaration SHOULD match the agent_url of the file's hosting location — platform-hosted formats point at the platform's agent, community-mirror formats point at `https://creative.adcontextprotocol.org/translated/`. This keeps the legacy named-format namespace converged regardless of which side hosts the catalog. See `docs/creative/canonical-formats.mdx` Meta Reels worked example.").optional(), "superseded_by": z.string().url().regex(new RegExp("^https://")).describe("Optional pointer indicating this adagents.json file has been superseded by another adagents.json at a different URL. Used by the AAO community-mirror lifecycle: when a platform (e.g., Meta) adopts AdCP and publishes its own adagents.json at `/.well-known/adagents.json`, the AAO mirror file at `creative.adcontextprotocol.org/translated//adagents.json` sets `superseded_by` to the platform-hosted URL. Buyer SDKs encountering a file with `superseded_by` SHOULD short-circuit and re-fetch from the named URL rather than serving stale content from the mirror. The mirror SHOULD continue serving with `superseded_by` set for ≥1 minor release after platform adoption so buyer caches keyed on the mirror URL get an explicit migration signal rather than a silent break.").optional(), "tags": z.record(z.string(), z.object({ "name": z.string().describe("Human-readable name for this tag"), "description": z.string().describe("Description of what this tag represents") }).catchall(z.any())).describe("Metadata for each tag referenced by properties. Provides human-readable context for property tag values.").optional(), "placement_tags": z.record(z.string(), z.object({ "name": z.string().describe("Human-readable name for this placement tag"), "description": z.string().describe("Description of what this placement tag represents") }).catchall(z.any())).describe("Metadata for each tag referenced by placements. Provides human-readable context for publisher-defined placement tag values used in grouping and authorization.").optional(), "authorized_agents": z.array(z.union([z.object({ "authorization_type": z.literal("property_ids").describe("Discriminator indicating authorization by specific property IDs"), "property_ids": z.array(z.string().regex(new RegExp("^[a-z0-9_]+$")).describe("Identifier for a publisher property. Must be lowercase alphanumeric with underscores only.")).describe("Property IDs this agent is authorized for. Resolved against the top-level properties array in this file"), "collections": z.array(z.object({ "publisher_domain": z.string().regex(new RegExp("^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$")).describe("Domain where the adagents.json declaring these collections is hosted (e.g., 'mrbeast.com'). The collections array in that file contains the authoritative collection definitions."), "collection_ids": z.array(z.string()).describe("Collection IDs from the adagents.json collections array. Each ID must match a collection_id declared in that file.") }).catchall(z.any()).describe("References collections declared in an adagents.json. Buyers resolve full collection objects by fetching the adagents.json at the given domain and matching collection_ids against its collections array.")).describe("Optional collection constraints. When present, authorization only applies to inventory associated with these collections.").optional(), "placement_ids": z.array(z.string()).describe("Optional placement constraints. When present, authorization only applies to these placement IDs from the top-level placements array in this file.").optional(), "placement_tags": z.array(z.string()).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Optional placement tag constraints. When present, authorization only applies to placements whose tags include any of these publisher-defined values.").optional(), "delegation_type": z.enum(["direct","delegated","ad_network"]).describe("Commercial relationship for this inventory path. 'direct' means the publisher treats this as a direct way to buy from them, even if a third party operates the software. 'delegated' means the agent is authorized to sell on the publisher's behalf. 'ad_network' means the inventory is sold as part of a network/package context rather than as the publisher's direct endpoint.").optional(), "exclusive": z.boolean().describe("Whether this agent is the publisher's sole authorized path for the scoped inventory slice. When false or absent, other authorized agents may also sell the same inventory.").optional(), "countries": z.array(z.string().regex(new RegExp("^[A-Z]{2}$"))).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Optional ISO 3166-1 alpha-2 country codes limiting where this authorization applies. Omit for worldwide authorization.").optional(), "effective_from": z.string().datetime().describe("Optional start time for this authorization window.").optional(), "effective_until": z.string().datetime().describe("Optional end time for this authorization window.").optional() }).catchall(z.any()).and(z.object({ "url": z.string().url().describe("The authorized agent's API endpoint URL. Callers comparing this URL against a registry (sales-agent list, signal-provider registry, TMP provider lookup, etc.) MUST canonicalize both sides per the AdCP URL canonicalization rules, not byte-equality — two URLs that differ only in case, default port, or percent-encoding of unreserved characters are the same agent. See docs/reference/url-canonicalization."), "authorized_for": z.string().min(1).max(500).describe("Human-readable description of what this agent is authorized to do — what it sells (for sales/property agents) or what data it provides (for signal agents). The variant the entry uses (`property_ids`, `signal_tags`, etc.) tells consumers which inflection applies; this field carries the operator-supplied label."), "signing_keys": z.array(z.object({ "kid": z.string().describe("Key identifier for selecting the correct signing key."), "kty": z.string().describe("JWK key type, such as 'OKP', 'EC', or 'RSA'."), "alg": z.string().describe("Expected signing algorithm for this key, such as 'EdDSA' or 'RS256'.").optional(), "use": z.string().describe("Optional JWK use value. Typically 'sig' for signing keys.").optional(), "crv": z.string().describe("Curve name for OKP or EC keys, such as 'Ed25519' or 'P-256'.").optional(), "x": z.string().describe("Base64url-encoded public key x coordinate or public key value for OKP keys.").optional(), "y": z.string().describe("Base64url-encoded public key y coordinate for EC keys.").optional(), "n": z.string().describe("Base64url-encoded RSA modulus.").optional(), "e": z.string().describe("Base64url-encoded RSA public exponent.").optional(), "revoked_at": z.string().datetime().describe("Optional revocation timestamp. When present, verifiers MUST reject any signature produced with this key whose signing epoch (or equivalent time reference) is at or after this timestamp. The key may continue to appear in the trust anchor during a grace period so caches that have not yet refreshed still find the key and can evaluate the revocation marker. Keys past their revocation can be removed once the cache TTL (recommended: 5 minutes) has elapsed across all verifiers.").optional() }).catchall(z.any()).describe("Publisher-attested public key material for an authorized agent. Buyers use these keys to verify signed agent responses against the trust anchor published in adagents.json rather than trusting key discovery from the agent domain alone.")).describe("Optional publisher-attested public signing keys for this agent. Use these as the trust anchor for verifying signed agent responses instead of relying on key discovery from the agent domain alone.").optional(), "encryption_keys": z.array(z.object({ "kid": z.string().max(8).describe("Key identifier. Opaque — MUST NOT encode geographic or deployment information."), "kty": z.literal("OKP").describe("JWK key type. Must be OKP for X25519."), "crv": z.literal("X25519").describe("Curve name. Must be X25519 for TMPX encryption."), "use": z.literal("enc").describe("JWK use value. Must be enc for encryption keys."), "x": z.string().describe("Base64url-encoded X25519 public key (32 bytes).") }).strict().describe("X25519 public key for HPKE encryption. Used for TMPX exposure token encryption with HPKE mode_base.")).describe("X25519 public keys for TMPX exposure token encryption. Each key identifies a cluster master that can decrypt TMPX tokens. Used with HPKE mode_base — read replicas encrypt with this public key, only the master can decrypt.").optional(), "last_updated": z.string().datetime().describe("Optional ISO 8601 timestamp indicating when this `authorized_agents[]` entry last changed. Independent of the file-level `last_updated`. Lets validators perform a partial walk by skipping entries whose `last_updated` is older than their indexed value. Advisory — consumers MAY ignore and re-index the full file.").optional() }).describe("Fields shared by every variant of `authorized_agents[*]` in adagents.json, regardless of authorization_type. Variants `allOf` this base and add their discriminator-specific fields. Centralized to prevent drift across all authorization-type variants.")), z.object({ "authorization_type": z.literal("property_tags").describe("Discriminator indicating authorization by property tags"), "property_tags": z.array(z.string().regex(new RegExp("^[a-z0-9_]+$")).describe("Tag for categorizing publisher properties. Must be lowercase alphanumeric with underscores only.")).describe("Tags identifying which properties this agent is authorized for. Resolved against the top-level properties array in this file using tag matching"), "collections": z.array(z.object({ "publisher_domain": z.string().regex(new RegExp("^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$")).describe("Domain where the adagents.json declaring these collections is hosted (e.g., 'mrbeast.com'). The collections array in that file contains the authoritative collection definitions."), "collection_ids": z.array(z.string()).describe("Collection IDs from the adagents.json collections array. Each ID must match a collection_id declared in that file.") }).catchall(z.any()).describe("References collections declared in an adagents.json. Buyers resolve full collection objects by fetching the adagents.json at the given domain and matching collection_ids against its collections array.")).describe("Optional collection constraints. When present, authorization only applies to inventory associated with these collections.").optional(), "placement_ids": z.array(z.string()).describe("Optional placement constraints. When present, authorization only applies to these placement IDs from the top-level placements array in this file.").optional(), "placement_tags": z.array(z.string()).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Optional placement tag constraints. When present, authorization only applies to placements whose tags include any of these publisher-defined values.").optional(), "delegation_type": z.enum(["direct","delegated","ad_network"]).describe("Commercial relationship for this inventory path. 'direct' means the publisher treats this as a direct way to buy from them, even if a third party operates the software. 'delegated' means the agent is authorized to sell on the publisher's behalf. 'ad_network' means the inventory is sold as part of a network/package context rather than as the publisher's direct endpoint.").optional(), "exclusive": z.boolean().describe("Whether this agent is the publisher's sole authorized path for the scoped inventory slice. When false or absent, other authorized agents may also sell the same inventory.").optional(), "countries": z.array(z.string().regex(new RegExp("^[A-Z]{2}$"))).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Optional ISO 3166-1 alpha-2 country codes limiting where this authorization applies. Omit for worldwide authorization.").optional(), "effective_from": z.string().datetime().describe("Optional start time for this authorization window.").optional(), "effective_until": z.string().datetime().describe("Optional end time for this authorization window.").optional() }).catchall(z.any()).and(z.object({ "url": z.string().url().describe("The authorized agent's API endpoint URL. Callers comparing this URL against a registry (sales-agent list, signal-provider registry, TMP provider lookup, etc.) MUST canonicalize both sides per the AdCP URL canonicalization rules, not byte-equality — two URLs that differ only in case, default port, or percent-encoding of unreserved characters are the same agent. See docs/reference/url-canonicalization."), "authorized_for": z.string().min(1).max(500).describe("Human-readable description of what this agent is authorized to do — what it sells (for sales/property agents) or what data it provides (for signal agents). The variant the entry uses (`property_ids`, `signal_tags`, etc.) tells consumers which inflection applies; this field carries the operator-supplied label."), "signing_keys": z.array(z.object({ "kid": z.string().describe("Key identifier for selecting the correct signing key."), "kty": z.string().describe("JWK key type, such as 'OKP', 'EC', or 'RSA'."), "alg": z.string().describe("Expected signing algorithm for this key, such as 'EdDSA' or 'RS256'.").optional(), "use": z.string().describe("Optional JWK use value. Typically 'sig' for signing keys.").optional(), "crv": z.string().describe("Curve name for OKP or EC keys, such as 'Ed25519' or 'P-256'.").optional(), "x": z.string().describe("Base64url-encoded public key x coordinate or public key value for OKP keys.").optional(), "y": z.string().describe("Base64url-encoded public key y coordinate for EC keys.").optional(), "n": z.string().describe("Base64url-encoded RSA modulus.").optional(), "e": z.string().describe("Base64url-encoded RSA public exponent.").optional(), "revoked_at": z.string().datetime().describe("Optional revocation timestamp. When present, verifiers MUST reject any signature produced with this key whose signing epoch (or equivalent time reference) is at or after this timestamp. The key may continue to appear in the trust anchor during a grace period so caches that have not yet refreshed still find the key and can evaluate the revocation marker. Keys past their revocation can be removed once the cache TTL (recommended: 5 minutes) has elapsed across all verifiers.").optional() }).catchall(z.any()).describe("Publisher-attested public key material for an authorized agent. Buyers use these keys to verify signed agent responses against the trust anchor published in adagents.json rather than trusting key discovery from the agent domain alone.")).describe("Optional publisher-attested public signing keys for this agent. Use these as the trust anchor for verifying signed agent responses instead of relying on key discovery from the agent domain alone.").optional(), "encryption_keys": z.array(z.object({ "kid": z.string().max(8).describe("Key identifier. Opaque — MUST NOT encode geographic or deployment information."), "kty": z.literal("OKP").describe("JWK key type. Must be OKP for X25519."), "crv": z.literal("X25519").describe("Curve name. Must be X25519 for TMPX encryption."), "use": z.literal("enc").describe("JWK use value. Must be enc for encryption keys."), "x": z.string().describe("Base64url-encoded X25519 public key (32 bytes).") }).strict().describe("X25519 public key for HPKE encryption. Used for TMPX exposure token encryption with HPKE mode_base.")).describe("X25519 public keys for TMPX exposure token encryption. Each key identifies a cluster master that can decrypt TMPX tokens. Used with HPKE mode_base — read replicas encrypt with this public key, only the master can decrypt.").optional(), "last_updated": z.string().datetime().describe("Optional ISO 8601 timestamp indicating when this `authorized_agents[]` entry last changed. Independent of the file-level `last_updated`. Lets validators perform a partial walk by skipping entries whose `last_updated` is older than their indexed value. Advisory — consumers MAY ignore and re-index the full file.").optional() }).describe("Fields shared by every variant of `authorized_agents[*]` in adagents.json, regardless of authorization_type. Variants `allOf` this base and add their discriminator-specific fields. Centralized to prevent drift across all authorization-type variants.")), z.object({ "authorization_type": z.literal("inline_properties").describe("Discriminator indicating authorization by inline property definitions. Companion field is `properties` (not `inline_properties`) — the only authorization_type whose companion field name does not mirror the discriminator value."), "properties": z.array(z.object({ "property_id": z.string().regex(new RegExp("^[a-z0-9_]+$")).describe("Unique identifier for this property (optional). Enables referencing properties by ID instead of repeating full objects.").optional(), "property_type": z.enum(["website","mobile_app","ctv_app","desktop_app","dooh","podcast","radio","linear_tv","streaming_audio","ai_assistant"]).describe("Type of advertising property"), "name": z.string().describe("Human-readable property name"), "identifiers": z.array(z.object({ "type": z.enum(["domain","subdomain","network_id","ios_bundle","android_package","apple_app_store_id","google_play_id","roku_store_id","fire_tv_asin","samsung_app_id","apple_tv_bundle","bundle_id","venue_id","screen_id","openooh_venue_type","rss_url","apple_podcast_id","spotify_collection_id","podcast_guid","station_id","facility_id"]).describe("Type of identifier for this property"), "value": z.string().describe("The identifier value. For domain type: 'example.com' matches base domain plus www and m subdomains; 'edition.example.com' matches that specific subdomain; '*.example.com' matches ALL subdomains but NOT base domain") }).catchall(z.any())).describe("Array of identifiers for this property"), "tags": z.array(z.string().regex(new RegExp("^[a-z0-9_]+$")).describe("Tag for categorizing publisher properties. Must be lowercase alphanumeric with underscores only.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Tags for categorization and grouping (e.g., network membership, content categories)").optional(), "supported_channels": z.array(z.enum(["display","olv","social","search","ctv","linear_tv","radio","streaming_audio","podcast","dooh","ooh","print","cinema","email","gaming","retail_media","influencer","affiliate","product_placement","sponsored_intelligence"]).describe("Standardized advertising media channels describing how buyers allocate budget. Channels are planning abstractions, not technical substrates. See the Media Channel Taxonomy specification for detailed definitions.")).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Advertising channels this property supports (e.g., ['display', 'olv', 'social']). Publishers declare which channels their inventory aligns with. Properties may support multiple channels. See the Media Channel Taxonomy for definitions.").optional(), "publisher_domain": z.string().describe("Domain where adagents.json should be checked for authorization validation. Optional in adagents.json (file location implies domain).").optional() }).catchall(z.any()).describe("An advertising property that can be validated via adagents.json")).describe("Specific properties this agent is authorized for, defined inline on the agent entry (alternative to property_ids/property_tags). Note: this is the companion field for `authorization_type: \"inline_properties\"` — the field is named `properties`, not `inline_properties`."), "collections": z.array(z.object({ "publisher_domain": z.string().regex(new RegExp("^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$")).describe("Domain where the adagents.json declaring these collections is hosted (e.g., 'mrbeast.com'). The collections array in that file contains the authoritative collection definitions."), "collection_ids": z.array(z.string()).describe("Collection IDs from the adagents.json collections array. Each ID must match a collection_id declared in that file.") }).catchall(z.any()).describe("References collections declared in an adagents.json. Buyers resolve full collection objects by fetching the adagents.json at the given domain and matching collection_ids against its collections array.")).describe("Optional collection constraints. When present, authorization only applies to inventory associated with these collections.").optional(), "placement_ids": z.array(z.string()).describe("Optional placement constraints. When present, authorization only applies to these placement IDs from the top-level placements array in this file.").optional(), "placement_tags": z.array(z.string()).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Optional placement tag constraints. When present, authorization only applies to placements whose tags include any of these publisher-defined values.").optional(), "delegation_type": z.enum(["direct","delegated","ad_network"]).describe("Commercial relationship for this inventory path. 'direct' means the publisher treats this as a direct way to buy from them, even if a third party operates the software. 'delegated' means the agent is authorized to sell on the publisher's behalf. 'ad_network' means the inventory is sold as part of a network/package context rather than as the publisher's direct endpoint.").optional(), "exclusive": z.boolean().describe("Whether this agent is the publisher's sole authorized path for the scoped inventory slice. When false or absent, other authorized agents may also sell the same inventory.").optional(), "countries": z.array(z.string().regex(new RegExp("^[A-Z]{2}$"))).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Optional ISO 3166-1 alpha-2 country codes limiting where this authorization applies. Omit for worldwide authorization.").optional(), "effective_from": z.string().datetime().describe("Optional start time for this authorization window.").optional(), "effective_until": z.string().datetime().describe("Optional end time for this authorization window.").optional() }).catchall(z.any()).and(z.object({ "url": z.string().url().describe("The authorized agent's API endpoint URL. Callers comparing this URL against a registry (sales-agent list, signal-provider registry, TMP provider lookup, etc.) MUST canonicalize both sides per the AdCP URL canonicalization rules, not byte-equality — two URLs that differ only in case, default port, or percent-encoding of unreserved characters are the same agent. See docs/reference/url-canonicalization."), "authorized_for": z.string().min(1).max(500).describe("Human-readable description of what this agent is authorized to do — what it sells (for sales/property agents) or what data it provides (for signal agents). The variant the entry uses (`property_ids`, `signal_tags`, etc.) tells consumers which inflection applies; this field carries the operator-supplied label."), "signing_keys": z.array(z.object({ "kid": z.string().describe("Key identifier for selecting the correct signing key."), "kty": z.string().describe("JWK key type, such as 'OKP', 'EC', or 'RSA'."), "alg": z.string().describe("Expected signing algorithm for this key, such as 'EdDSA' or 'RS256'.").optional(), "use": z.string().describe("Optional JWK use value. Typically 'sig' for signing keys.").optional(), "crv": z.string().describe("Curve name for OKP or EC keys, such as 'Ed25519' or 'P-256'.").optional(), "x": z.string().describe("Base64url-encoded public key x coordinate or public key value for OKP keys.").optional(), "y": z.string().describe("Base64url-encoded public key y coordinate for EC keys.").optional(), "n": z.string().describe("Base64url-encoded RSA modulus.").optional(), "e": z.string().describe("Base64url-encoded RSA public exponent.").optional(), "revoked_at": z.string().datetime().describe("Optional revocation timestamp. When present, verifiers MUST reject any signature produced with this key whose signing epoch (or equivalent time reference) is at or after this timestamp. The key may continue to appear in the trust anchor during a grace period so caches that have not yet refreshed still find the key and can evaluate the revocation marker. Keys past their revocation can be removed once the cache TTL (recommended: 5 minutes) has elapsed across all verifiers.").optional() }).catchall(z.any()).describe("Publisher-attested public key material for an authorized agent. Buyers use these keys to verify signed agent responses against the trust anchor published in adagents.json rather than trusting key discovery from the agent domain alone.")).describe("Optional publisher-attested public signing keys for this agent. Use these as the trust anchor for verifying signed agent responses instead of relying on key discovery from the agent domain alone.").optional(), "encryption_keys": z.array(z.object({ "kid": z.string().max(8).describe("Key identifier. Opaque — MUST NOT encode geographic or deployment information."), "kty": z.literal("OKP").describe("JWK key type. Must be OKP for X25519."), "crv": z.literal("X25519").describe("Curve name. Must be X25519 for TMPX encryption."), "use": z.literal("enc").describe("JWK use value. Must be enc for encryption keys."), "x": z.string().describe("Base64url-encoded X25519 public key (32 bytes).") }).strict().describe("X25519 public key for HPKE encryption. Used for TMPX exposure token encryption with HPKE mode_base.")).describe("X25519 public keys for TMPX exposure token encryption. Each key identifies a cluster master that can decrypt TMPX tokens. Used with HPKE mode_base — read replicas encrypt with this public key, only the master can decrypt.").optional(), "last_updated": z.string().datetime().describe("Optional ISO 8601 timestamp indicating when this `authorized_agents[]` entry last changed. Independent of the file-level `last_updated`. Lets validators perform a partial walk by skipping entries whose `last_updated` is older than their indexed value. Advisory — consumers MAY ignore and re-index the full file.").optional() }).describe("Fields shared by every variant of `authorized_agents[*]` in adagents.json, regardless of authorization_type. Variants `allOf` this base and add their discriminator-specific fields. Centralized to prevent drift across all authorization-type variants.")), z.object({ "authorization_type": z.literal("publisher_properties").describe("Discriminator indicating authorization for properties from other publisher domains"), "publisher_properties": z.array(z.union([z.object({ "publisher_domain": z.string().regex(new RegExp("^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$")).describe("Domain where publisher's adagents.json is hosted (e.g., 'cnn.com'). XOR with `publisher_domains` — exactly one MUST be present on each `publisher_properties[]` entry; both-present and neither-present both fail validation.").optional(), "publisher_domains": z.array(z.string().regex(new RegExp("^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$"))).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Compact form for fanning the same selector across many publishers (e.g., a managed network listing every publisher it represents). Each entry is the domain where that publisher's adagents.json is hosted. Each listed domain MUST be canonicalized to lowercase (the `pattern` already rejects uppercase). Mutually exclusive with `publisher_domain`. Each listed domain counts as explicitly scoped for the `managerdomain` fallback safety rule.").optional(), "selection_type": z.literal("all").describe("Discriminator indicating all properties from each addressed publisher are included") }).catchall(z.any()).and(z.intersection(z.any().refine((value) => !z.any().safeParse(value).success, "Invalid input: Should NOT be valid against schema"), z.union([z.any(), z.any()]))).describe("Select all properties from one publisher domain, or from each publisher domain when `publisher_domains` is used. Consumers MAY satisfy the selector from the parent file's top-level `properties[]` when those properties carry a `publisher_domain` matching one of the listed domains (see Resolution paths in the spec)."), z.object({ "publisher_domain": z.string().regex(new RegExp("^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$")).describe("Domain where publisher's adagents.json is hosted (e.g., 'cnn.com')."), "selection_type": z.literal("by_id").describe("Discriminator indicating selection by specific property IDs"), "property_ids": z.array(z.string().regex(new RegExp("^[a-z0-9_]+$")).describe("Identifier for a publisher property. Must be lowercase alphanumeric with underscores only.")).describe("Specific property IDs from the publisher's adagents.json") }).catchall(z.any()).describe("Select specific properties by ID. Single-publisher only — property IDs are publisher-scoped, so the compact `publisher_domains[]` form is intentionally NOT available for this selector. Use multiple `publisher_properties[]` entries (one per publisher) when each publisher's ID set differs."), z.object({ "publisher_domain": z.string().regex(new RegExp("^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$")).describe("Domain where publisher's adagents.json is hosted (e.g., 'cnn.com'). XOR with `publisher_domains` — exactly one MUST be present on each `publisher_properties[]` entry; both-present and neither-present both fail validation.").optional(), "publisher_domains": z.array(z.string().regex(new RegExp("^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$"))).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Compact form for fanning the same tag predicate across many publishers (canonical managed-network shape). Each entry is the domain where that publisher's adagents.json is hosted. Each listed domain MUST be canonicalized to lowercase (the `pattern` already rejects uppercase). Mutually exclusive with `publisher_domain`. Each listed domain counts as explicitly scoped for the `managerdomain` fallback safety rule.").optional(), "selection_type": z.literal("by_tag").describe("Discriminator indicating selection by property tags"), "property_tags": z.array(z.string().regex(new RegExp("^[a-z0-9_]+$")).describe("Tag for categorizing publisher properties. Must be lowercase alphanumeric with underscores only.")).describe("Property tags resolved against each addressed publisher's adagents.json, OR against the parent file's top-level `properties[]` when those properties carry a `publisher_domain` matching the selector. Selector covers all properties carrying any of these tags.") }).catchall(z.any()).and(z.intersection(z.any().refine((value) => !z.any().safeParse(value).success, "Invalid input: Should NOT be valid against schema"), z.union([z.any(), z.any()]))).describe("Select properties by tag membership. With `publisher_domains`, the same `property_tags` predicate is resolved against each listed publisher's adagents.json — the common managed-network case where every represented site tags inventory with a shared label. Consumers MAY also satisfy the predicate from the parent file's top-level `properties[]` when those properties carry a `publisher_domain` matching one of the selector's `publisher_domains[]` (see Resolution paths in the spec).")]).describe("Selects properties from a publisher's adagents.json. Used for both product definitions and agent authorization. Supports three selection patterns: all properties, specific IDs, or by tags. Each selector targets one publisher via `publisher_domain` (string) or a fan-out across many publishers that share the same selector via `publisher_domains` (array). Exactly one of `publisher_domain` or `publisher_domains` MUST be present. When `publisher_domains` is used, the selector is logically equivalent to repeating the same entry once per listed domain.")).describe("Properties from other publisher domains this agent is authorized for. Each entry specifies a publisher domain and which of their properties this agent can sell"), "collections": z.array(z.object({ "publisher_domain": z.string().regex(new RegExp("^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$")).describe("Domain where the adagents.json declaring these collections is hosted (e.g., 'mrbeast.com'). The collections array in that file contains the authoritative collection definitions."), "collection_ids": z.array(z.string()).describe("Collection IDs from the adagents.json collections array. Each ID must match a collection_id declared in that file.") }).catchall(z.any()).describe("References collections declared in an adagents.json. Buyers resolve full collection objects by fetching the adagents.json at the given domain and matching collection_ids against its collections array.")).describe("Optional collection constraints. When present, authorization only applies to inventory associated with these collections.").optional(), "placement_ids": z.array(z.string()).describe("Optional placement constraints. When present, authorization only applies to these placement IDs from the top-level placements array in this file.").optional(), "placement_tags": z.array(z.string()).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Optional placement tag constraints. When present, authorization only applies to placements whose tags include any of these publisher-defined values.").optional(), "delegation_type": z.enum(["direct","delegated","ad_network"]).describe("Commercial relationship for this inventory path. 'direct' means the publisher treats this as a direct way to buy from them, even if a third party operates the software. 'delegated' means the agent is authorized to sell on the publisher's behalf. 'ad_network' means the inventory is sold as part of a network/package context rather than as the publisher's direct endpoint.").optional(), "exclusive": z.boolean().describe("Whether this agent is the publisher's sole authorized path for the scoped inventory slice. When false or absent, other authorized agents may also sell the same inventory.").optional(), "countries": z.array(z.string().regex(new RegExp("^[A-Z]{2}$"))).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Optional ISO 3166-1 alpha-2 country codes limiting where this authorization applies. Omit for worldwide authorization.").optional(), "effective_from": z.string().datetime().describe("Optional start time for this authorization window.").optional(), "effective_until": z.string().datetime().describe("Optional end time for this authorization window.").optional() }).catchall(z.any()).and(z.object({ "url": z.string().url().describe("The authorized agent's API endpoint URL. Callers comparing this URL against a registry (sales-agent list, signal-provider registry, TMP provider lookup, etc.) MUST canonicalize both sides per the AdCP URL canonicalization rules, not byte-equality — two URLs that differ only in case, default port, or percent-encoding of unreserved characters are the same agent. See docs/reference/url-canonicalization."), "authorized_for": z.string().min(1).max(500).describe("Human-readable description of what this agent is authorized to do — what it sells (for sales/property agents) or what data it provides (for signal agents). The variant the entry uses (`property_ids`, `signal_tags`, etc.) tells consumers which inflection applies; this field carries the operator-supplied label."), "signing_keys": z.array(z.object({ "kid": z.string().describe("Key identifier for selecting the correct signing key."), "kty": z.string().describe("JWK key type, such as 'OKP', 'EC', or 'RSA'."), "alg": z.string().describe("Expected signing algorithm for this key, such as 'EdDSA' or 'RS256'.").optional(), "use": z.string().describe("Optional JWK use value. Typically 'sig' for signing keys.").optional(), "crv": z.string().describe("Curve name for OKP or EC keys, such as 'Ed25519' or 'P-256'.").optional(), "x": z.string().describe("Base64url-encoded public key x coordinate or public key value for OKP keys.").optional(), "y": z.string().describe("Base64url-encoded public key y coordinate for EC keys.").optional(), "n": z.string().describe("Base64url-encoded RSA modulus.").optional(), "e": z.string().describe("Base64url-encoded RSA public exponent.").optional(), "revoked_at": z.string().datetime().describe("Optional revocation timestamp. When present, verifiers MUST reject any signature produced with this key whose signing epoch (or equivalent time reference) is at or after this timestamp. The key may continue to appear in the trust anchor during a grace period so caches that have not yet refreshed still find the key and can evaluate the revocation marker. Keys past their revocation can be removed once the cache TTL (recommended: 5 minutes) has elapsed across all verifiers.").optional() }).catchall(z.any()).describe("Publisher-attested public key material for an authorized agent. Buyers use these keys to verify signed agent responses against the trust anchor published in adagents.json rather than trusting key discovery from the agent domain alone.")).describe("Optional publisher-attested public signing keys for this agent. Use these as the trust anchor for verifying signed agent responses instead of relying on key discovery from the agent domain alone.").optional(), "encryption_keys": z.array(z.object({ "kid": z.string().max(8).describe("Key identifier. Opaque — MUST NOT encode geographic or deployment information."), "kty": z.literal("OKP").describe("JWK key type. Must be OKP for X25519."), "crv": z.literal("X25519").describe("Curve name. Must be X25519 for TMPX encryption."), "use": z.literal("enc").describe("JWK use value. Must be enc for encryption keys."), "x": z.string().describe("Base64url-encoded X25519 public key (32 bytes).") }).strict().describe("X25519 public key for HPKE encryption. Used for TMPX exposure token encryption with HPKE mode_base.")).describe("X25519 public keys for TMPX exposure token encryption. Each key identifies a cluster master that can decrypt TMPX tokens. Used with HPKE mode_base — read replicas encrypt with this public key, only the master can decrypt.").optional(), "last_updated": z.string().datetime().describe("Optional ISO 8601 timestamp indicating when this `authorized_agents[]` entry last changed. Independent of the file-level `last_updated`. Lets validators perform a partial walk by skipping entries whose `last_updated` is older than their indexed value. Advisory — consumers MAY ignore and re-index the full file.").optional() }).describe("Fields shared by every variant of `authorized_agents[*]` in adagents.json, regardless of authorization_type. Variants `allOf` this base and add their discriminator-specific fields. Centralized to prevent drift across all authorization-type variants.")), z.object({ "authorization_type": z.literal("signal_ids").describe("Discriminator indicating authorization by specific signal IDs"), "signal_ids": z.array(z.string().regex(new RegExp("^[a-zA-Z0-9_-]+$"))).describe("Signal IDs this agent is authorized to resell. Resolved against the top-level signals array in this file") }).catchall(z.any()).and(z.object({ "url": z.string().url().describe("The authorized agent's API endpoint URL. Callers comparing this URL against a registry (sales-agent list, signal-provider registry, TMP provider lookup, etc.) MUST canonicalize both sides per the AdCP URL canonicalization rules, not byte-equality — two URLs that differ only in case, default port, or percent-encoding of unreserved characters are the same agent. See docs/reference/url-canonicalization."), "authorized_for": z.string().min(1).max(500).describe("Human-readable description of what this agent is authorized to do — what it sells (for sales/property agents) or what data it provides (for signal agents). The variant the entry uses (`property_ids`, `signal_tags`, etc.) tells consumers which inflection applies; this field carries the operator-supplied label."), "signing_keys": z.array(z.object({ "kid": z.string().describe("Key identifier for selecting the correct signing key."), "kty": z.string().describe("JWK key type, such as 'OKP', 'EC', or 'RSA'."), "alg": z.string().describe("Expected signing algorithm for this key, such as 'EdDSA' or 'RS256'.").optional(), "use": z.string().describe("Optional JWK use value. Typically 'sig' for signing keys.").optional(), "crv": z.string().describe("Curve name for OKP or EC keys, such as 'Ed25519' or 'P-256'.").optional(), "x": z.string().describe("Base64url-encoded public key x coordinate or public key value for OKP keys.").optional(), "y": z.string().describe("Base64url-encoded public key y coordinate for EC keys.").optional(), "n": z.string().describe("Base64url-encoded RSA modulus.").optional(), "e": z.string().describe("Base64url-encoded RSA public exponent.").optional(), "revoked_at": z.string().datetime().describe("Optional revocation timestamp. When present, verifiers MUST reject any signature produced with this key whose signing epoch (or equivalent time reference) is at or after this timestamp. The key may continue to appear in the trust anchor during a grace period so caches that have not yet refreshed still find the key and can evaluate the revocation marker. Keys past their revocation can be removed once the cache TTL (recommended: 5 minutes) has elapsed across all verifiers.").optional() }).catchall(z.any()).describe("Publisher-attested public key material for an authorized agent. Buyers use these keys to verify signed agent responses against the trust anchor published in adagents.json rather than trusting key discovery from the agent domain alone.")).describe("Optional publisher-attested public signing keys for this agent. Use these as the trust anchor for verifying signed agent responses instead of relying on key discovery from the agent domain alone.").optional(), "encryption_keys": z.array(z.object({ "kid": z.string().max(8).describe("Key identifier. Opaque — MUST NOT encode geographic or deployment information."), "kty": z.literal("OKP").describe("JWK key type. Must be OKP for X25519."), "crv": z.literal("X25519").describe("Curve name. Must be X25519 for TMPX encryption."), "use": z.literal("enc").describe("JWK use value. Must be enc for encryption keys."), "x": z.string().describe("Base64url-encoded X25519 public key (32 bytes).") }).strict().describe("X25519 public key for HPKE encryption. Used for TMPX exposure token encryption with HPKE mode_base.")).describe("X25519 public keys for TMPX exposure token encryption. Each key identifies a cluster master that can decrypt TMPX tokens. Used with HPKE mode_base — read replicas encrypt with this public key, only the master can decrypt.").optional(), "last_updated": z.string().datetime().describe("Optional ISO 8601 timestamp indicating when this `authorized_agents[]` entry last changed. Independent of the file-level `last_updated`. Lets validators perform a partial walk by skipping entries whose `last_updated` is older than their indexed value. Advisory — consumers MAY ignore and re-index the full file.").optional() }).describe("Fields shared by every variant of `authorized_agents[*]` in adagents.json, regardless of authorization_type. Variants `allOf` this base and add their discriminator-specific fields. Centralized to prevent drift across all authorization-type variants.")).describe("Authorization for signals by specific signal IDs"), z.object({ "authorization_type": z.literal("signal_tags").describe("Discriminator indicating authorization by signal tags"), "signal_tags": z.array(z.string().regex(new RegExp("^[a-z0-9_-]+$"))).describe("Signal tags this agent is authorized for. Agent can resell all signals with these tags") }).catchall(z.any()).and(z.object({ "url": z.string().url().describe("The authorized agent's API endpoint URL. Callers comparing this URL against a registry (sales-agent list, signal-provider registry, TMP provider lookup, etc.) MUST canonicalize both sides per the AdCP URL canonicalization rules, not byte-equality — two URLs that differ only in case, default port, or percent-encoding of unreserved characters are the same agent. See docs/reference/url-canonicalization."), "authorized_for": z.string().min(1).max(500).describe("Human-readable description of what this agent is authorized to do — what it sells (for sales/property agents) or what data it provides (for signal agents). The variant the entry uses (`property_ids`, `signal_tags`, etc.) tells consumers which inflection applies; this field carries the operator-supplied label."), "signing_keys": z.array(z.object({ "kid": z.string().describe("Key identifier for selecting the correct signing key."), "kty": z.string().describe("JWK key type, such as 'OKP', 'EC', or 'RSA'."), "alg": z.string().describe("Expected signing algorithm for this key, such as 'EdDSA' or 'RS256'.").optional(), "use": z.string().describe("Optional JWK use value. Typically 'sig' for signing keys.").optional(), "crv": z.string().describe("Curve name for OKP or EC keys, such as 'Ed25519' or 'P-256'.").optional(), "x": z.string().describe("Base64url-encoded public key x coordinate or public key value for OKP keys.").optional(), "y": z.string().describe("Base64url-encoded public key y coordinate for EC keys.").optional(), "n": z.string().describe("Base64url-encoded RSA modulus.").optional(), "e": z.string().describe("Base64url-encoded RSA public exponent.").optional(), "revoked_at": z.string().datetime().describe("Optional revocation timestamp. When present, verifiers MUST reject any signature produced with this key whose signing epoch (or equivalent time reference) is at or after this timestamp. The key may continue to appear in the trust anchor during a grace period so caches that have not yet refreshed still find the key and can evaluate the revocation marker. Keys past their revocation can be removed once the cache TTL (recommended: 5 minutes) has elapsed across all verifiers.").optional() }).catchall(z.any()).describe("Publisher-attested public key material for an authorized agent. Buyers use these keys to verify signed agent responses against the trust anchor published in adagents.json rather than trusting key discovery from the agent domain alone.")).describe("Optional publisher-attested public signing keys for this agent. Use these as the trust anchor for verifying signed agent responses instead of relying on key discovery from the agent domain alone.").optional(), "encryption_keys": z.array(z.object({ "kid": z.string().max(8).describe("Key identifier. Opaque — MUST NOT encode geographic or deployment information."), "kty": z.literal("OKP").describe("JWK key type. Must be OKP for X25519."), "crv": z.literal("X25519").describe("Curve name. Must be X25519 for TMPX encryption."), "use": z.literal("enc").describe("JWK use value. Must be enc for encryption keys."), "x": z.string().describe("Base64url-encoded X25519 public key (32 bytes).") }).strict().describe("X25519 public key for HPKE encryption. Used for TMPX exposure token encryption with HPKE mode_base.")).describe("X25519 public keys for TMPX exposure token encryption. Each key identifies a cluster master that can decrypt TMPX tokens. Used with HPKE mode_base — read replicas encrypt with this public key, only the master can decrypt.").optional(), "last_updated": z.string().datetime().describe("Optional ISO 8601 timestamp indicating when this `authorized_agents[]` entry last changed. Independent of the file-level `last_updated`. Lets validators perform a partial walk by skipping entries whose `last_updated` is older than their indexed value. Advisory — consumers MAY ignore and re-index the full file.").optional() }).describe("Fields shared by every variant of `authorized_agents[*]` in adagents.json, regardless of authorization_type. Variants `allOf` this base and add their discriminator-specific fields. Centralized to prevent drift across all authorization-type variants.")).describe("Authorization for signals by tag membership")])).describe("Array of sales agents authorized to make inventory from this file available to buyers. Authorization can be scoped to specific properties, collections, countries, and time windows, with optional delegation metadata indicating whether the path is direct, delegated, or network-mediated. MAY be empty (`[]`) for a catalog-only community mirror — a file that publishes `formats`/`properties`/`placements` (with `catalog_etag`) for a platform that has not adopted AdCP, where there is no sales agent to authorize (see `catalog_etag`, `formats`, `superseded_by`). An empty array asserts *no sales authorization*: validators MUST NOT read it as deny-all, authorize-all, or a revocation, MUST NOT treat its presence as an error, and MUST still consume the catalog arrays."), "last_updated": z.string().datetime().describe("ISO 8601 timestamp indicating when this file was last updated").optional(), "property_features": z.array(z.object({ "url": z.string().url().describe("The agent's API endpoint URL. Callers comparing this URL against a feature-provider registry MUST canonicalize both sides per the AdCP URL canonicalization rules, not byte-equality. See docs/reference/url-canonicalization."), "name": z.string().describe("Human-readable name of the vendor/agent (e.g., 'Scope3', 'TAG', 'OneTrust')"), "features": z.array(z.string()).describe("Feature IDs this agent provides (e.g., 'carbon_score', 'tag_certified_against_fraud'). Use get_adcp_capabilities on the agent for full definitions."), "publisher_id": z.string().describe("Optional publisher identifier at this agent (for lookup)").optional() }).catchall(z.any())).describe("[AdCP 3.0] Optional list of agents that provide property feature data (certifications, scores, compliance status). Used for discovery - actual data is accessed through property list filters.").optional(), "signals": z.array(z.object({ "id": z.string().regex(new RegExp("^[a-zA-Z0-9_-]+$")).describe("Signal identifier within the publishing domain's adagents.json signals[]"), "name": z.string().min(1).max(255).describe("Human-readable signal name"), "description": z.string().max(2000).describe("Detailed description of what this signal represents and how it's derived").optional(), "value_type": z.enum(["binary","categorical","numeric"]).describe("The data type of this signal's values"), "tags": z.array(z.string().regex(new RegExp("^[a-z0-9_-]+$"))).describe("Tags for grouping and filtering this domain's published signal definitions").optional(), "allowed_values": z.array(z.string()).describe("For categorical signals, the valid values users can be assigned").optional(), "restricted_attributes": z.array(z.enum(["racial_ethnic_origin","political_opinions","religious_beliefs","trade_union_membership","health_data","sex_life_sexual_orientation","genetic_data","biometric_data","age","familial_status"]).describe("Personal data categories that may be restricted from use in audience targeting. Combines GDPR Article 9 special categories with US civil-rights protected classes (FHA familial_status, ADEA age). Used in two places: (1) on campaign plans via restricted_attributes to declare which categories are prohibited, and (2) on signal-definition.json via restricted_attributes to declare which categories a signal touches. Governance agents match plan restrictions against signal declarations for structural validation.")).describe("Restricted attribute categories this signal touches. Data providers SHOULD declare these so governance agents can structurally match signals against a plan's restricted_attributes without relying on semantic inference from the signal name or description.").optional(), "demographic_predicate": z.object({ "age": z.object({ "min": z.number().int().gte(0).lte(150).describe("Inclusive minimum age in completed years. Omit for an open lower bound.").optional(), "max": z.number().int().gte(0).lte(150).describe("Inclusive maximum age in completed years. Omit for an open upper bound.").optional(), "include_unknown": z.boolean().describe("Whether delivery to people whose age is unavailable is part of this predicate. This field has no default and MUST be supplied.") }).catchall(z.any()).and(z.union([z.any(), z.any()])).describe("A canonical audience-age predicate in completed integer years. min and max are inclusive; omitting one bound means no restriction in that direction. At least one bound is required. include_unknown is always explicit because people whose age is unavailable are not members of any numeric interval. Implementations MUST reject min greater than max; JSON Schema draft-07 cannot compare sibling numeric values.") }).catchall(z.any()).describe("Authoritative machine-readable demographic meaning for this signal. Signal names and taxonomy labels alone never establish exact demographic equivalence. Signals carrying this field MUST also declare restricted_attributes including age.").optional(), "policy_categories": z.array(z.string()).describe("Policy categories this signal is sensitive for (e.g., a children's interest signal declares ['children_directed']). Governance agents match these against a plan's policy_categories to flag sensitive data usage.").optional(), "range": z.object({ "min": z.number().describe("Minimum value"), "max": z.number().describe("Maximum value"), "unit": z.string().describe("Unit of measurement (e.g., 'score', 'dollars', 'years')").optional() }).strict().describe("For numeric signals, the valid value range").optional(), "taxonomy": z.object({ "ref": z.string().url().describe("URI identifying the taxonomy or taxonomy documentation."), "version": z.string().describe("Version identifier for the taxonomy when the taxonomy has versioned definitions.").optional(), "segtax": z.number().int().gte(1).describe("OpenRTB segtax code when the taxonomy maps to an OpenRTB segment taxonomy.").optional(), "etag": z.string().describe("Optional validator for custom taxonomy definitions so consumers can detect drift between signal publication and taxonomy resolution.").optional(), "values": z.array(z.object({ "id": z.string().min(1).describe("Taxonomy node identifier."), "path": z.string().describe("Optional human-readable or taxonomy-native path for display and review.").optional(), "modifiers": z.array(z.string()).describe("Optional taxonomy-specific modifiers that qualify the node.").optional() }).strict()).describe("Taxonomy node values that describe this signal. These are meaning/discovery metadata for the signal definition, not the values a buyer submits in package targeting expressions."), "value_mappings": z.array(z.object({ "value": z.string().describe("Categorical value from allowed_values[]."), "taxonomy_value_id": z.string().describe("Taxonomy node identifier corresponding to this categorical value."), "path": z.string().describe("Optional human-readable or taxonomy-native path for display and review.").optional(), "modifiers": z.array(z.string()).describe("Optional taxonomy-specific modifiers that qualify the mapped value.").optional() }).strict()).describe("For categorical signals, maps package-targeting allowed_values[] strings to stable taxonomy node identifiers. The allowed_values[] strings remain the wire values buyers submit; this mapping explains how those strings resolve into the published taxonomy. Each value_mappings[].value SHOULD match one of the signal's allowed_values[] entries; JSON Schema draft-07 cannot enforce this cross-array constraint.").optional(), "parent_match_behavior": z.enum(["exact_only","descendants_supported","unknown"]).describe("Whether this signal definition supports treating a parent taxonomy node as matching descendant nodes for discovery/filtering or seller-side expansion. 'exact_only' means only explicitly listed node ids match. 'descendants_supported' means the seller can expand known, version-pinned parent nodes to descendants internally, typically by ORing the children in its execution system. 'unknown' means the provider has not declared parent-node behavior. This field is metadata about discovery/translation behavior, not a package targeting operator.").optional() }).strict().describe("Optional taxonomy metadata describing what this signal means in an external audience, content, retail-media, or provider-owned taxonomy. Taxonomy metadata does not create a new value_type and does not change package targeting grammar: buyers still target the named signal according to value_type. When a taxonomy value is a parent node, parent/descendant expansion is seller behavior and must be declared through parent_match_behavior rather than assumed.").optional(), "segmentation_criteria": z.string().max(500).describe("Rules governing inclusion of identifiers in the segment. Aligns with IAB Data Transparency Standard audience criteria disclosure.").optional(), "criteria_url": z.string().url().describe("Optional URL to a longer-form methodology or criteria document. This is a disclosure pointer; buyers should not branch programmatically on the linked content.").optional(), "data_sources": z.array(z.enum(["app_behavior","app_usage","web_usage","geo_location","email","tv_ott_or_stb_device","panel","online_ecommerce","credit_data","loyalty_card","transaction","online_survey","offline_survey","public_record_census","public_record_voter_file","public_record_other","offline_transaction"])).describe("Origin categories of the raw data used to compile the signal, aligned with IAB Data Transparency Standard source disclosure. Use 'panel' for respondent-panel or JIC-style audience sources. Offline and public-record sources require onboarder disclosure. Co-viewing projection and reconciliation of seller claims against a JIC or measurement vendor belong in measurement reporting/vendor metrics rather than package signal targeting.").optional(), "methodology": z.enum(["observed","declared","derived","inferred","modeled"]).describe("How the signal's audience membership or attribute was determined. 'modeled' requires the modeling block.").optional(), "audience_expansion": z.boolean().describe("Whether look-alike or similar-audience expansion was used to include additional identifiers. When true, modeling is required.").optional(), "device_expansion": z.boolean().describe("Whether the signal was expanded deterministically across devices of the same user, household, or business. Probabilistic cross-device expansion is modeling and should use methodology 'modeled' or the modeling block.").optional(), "refresh_cadence": z.enum(["intra_day","daily","weekly","monthly","bi_monthly","quarterly","bi_annually","annually"]).describe("Cadence at which the signal definition's underlying segment membership is refreshed.").optional(), "lookback_window": z.enum(["intra_day","daily","weekly","monthly","bi_monthly","quarterly","bi_annually","annually"]).describe("Time window in which a qualifying event can occur for inclusion.").optional(), "onboarder": z.object({ "match_keys": z.array(z.enum(["name","address","email","postal","lat_long","mobile_id","cookie_id","ip","customer_id","phone"])), "pre_onboarding_audience_expansion": z.boolean().optional(), "pre_onboarding_device_expansion": z.boolean().optional(), "pre_onboarding_precision_level": z.enum(["individual","household","business","geography"]).optional() }).strict().describe("Onboarder disclosure. Required when data_sources includes an offline_* or public_record_* source.").optional(), "subject_type": z.enum(["individual","household","business","contextual","none"]).describe("What kind of subject this signal characterizes.").optional(), "resolution_method": z.enum(["deterministic_id","probabilistic_device","browser","geographic","content_signal","mixed"]).describe("How the subject is resolved at decision time.").optional(), "id_types": z.array(z.enum(["cookie","mobile_id","platform_id","user_enabled_id"])).describe("Identifier currencies analyzed to determine audience membership or attributes.").optional(), "audience_scope": z.enum(["single_domain","cross_domain_owned","cross_domain_unowned","offline"]).describe("Context within which the audience attribute was determined. 'single_domain' requires originating_domain.").optional(), "originating_domain": z.string().describe("Domain of the digital property where the audience originates. Required when audience_scope is 'single_domain'.").optional(), "countries": z.array(z.string().regex(new RegExp("^[A-Z]{2}$"))).describe("ISO 3166-1 alpha-2 country codes where the signal is applicable. Sellers must not expose a signal for media buys in countries outside this list. Federating agents that surface a peer's signal MUST treat the peer-published list as an upper bound, re-check it against the buyer's intended deployment countries, and may apply only narrower local policy.").optional(), "consent_basis": z.array(z.enum(["consent","legitimate_interest","contract","legal_obligation"]).describe("Common GDPR lawful bases relevant to advertising. Covers the Article 6(1) bases used in programmatic advertising contexts.")).describe("Declared GDPR Article 6 lawful basis or consent basis under which this signal's data is processed. For non-GDPR regimes, use countries, policy_categories, and disclosure fields to describe jurisdiction-specific obligations unless a future enum value applies.").optional(), "art9_basis": z.enum(["explicit_consent","manifestly_made_public","substantial_public_interest","vital_interests"]).describe("GDPR Article 9 basis when restricted_attributes is non-empty and the signal is used in jurisdictions where Article 9 applies. Required by policy for applicable use cases rather than universally required at schema level because sensitivity and lawful basis are jurisdiction-relative.").optional(), "modeling": z.object({ "method": z.enum(["lookalike","supervised","embedding","rules"]), "seed_source": z.object({ "type": z.enum(["first_party_crm","panel","declared_survey","transactional","behavioral"]), "provider_signed": z.boolean().describe("Whether the seed source carries a signed attestation under one of the provider's published signing keys. This is a forward-looking claim until the consumer can resolve and verify the provider's applicable signing keys through the AdCP signing profile; consumers MUST NOT treat this boolean alone as cryptographic proof.") }).strict(), "training_data_jurisdictions": z.array(z.string().regex(new RegExp("^[A-Z]{2}$"))).describe("ISO 3166-1 alpha-2 country codes where the model's training data was collected."), "ai_act_risk_class": z.enum(["minimal","limited","high_risk"]).describe("EU AI Act risk classification self-declared by the provider. Prohibited-risk modeled signals must not be published as AdCP signal definitions."), "disclosure": z.object({ "required": z.boolean().describe("The provider's claim that a modeling or AI-use disclosure is required for this signal in at least one applicable jurisdiction. This is a declared compliance signal, not a protocol-level legal determination."), "jurisdictions": z.array(z.object({ "country": z.string().regex(new RegExp("^[A-Z]{2}$")).describe("ISO 3166-1 alpha-2 country code."), "region": z.string().describe("Provider-defined sub-national region code or name when the obligation is regional. No global canonical format is implied.").optional(), "regulation": z.string().describe("Provider-supplied regulation identifier for the disclosure obligation."), "disclosure_text": z.string().describe("Human-readable disclosure text or summary the provider expects buyers or reviewers to see.").optional(), "disclosure_url": z.string().url().describe("Optional URL to the provider's canonical disclosure or methodology page for this jurisdiction.").optional(), "audience": z.enum(["buyer","data_subject","regulator","public"]).describe("Primary audience for this disclosure entry.").optional() }).strict()).describe("Jurisdictions where a modeling or AI-use disclosure applies.").optional(), "notes": z.string().max(2000).describe("Optional provider notes on how the disclosure should be interpreted. Informational only; buyers should not branch programmatically on this text.").optional() }).strict().and(z.any()).describe("Signal/modeling-specific disclosure requirements and jurisdictional notes. This is not creative provenance render guidance.").optional() }).strict().describe("Modeling disclosure for modeled data signals. Required when methodology is 'modeled' or audience_expansion is true. This describes data modeling and intentionally does not reuse creative provenance, which is content/render oriented.").optional(), "data_subject_rights": z.object({ "upstream_source_domain": z.string().max(253).describe("Domain of the upstream data source whose rights process these channels reach, when different from the publishing domain.").optional(), "channels": z.array(z.object({ "rights": z.array(z.enum(["access","rectification","erasure","portability","objection"])).refine((arr) => arr.every((item, i) => arr.indexOf(item) == i), "All items must be unique!").describe("Rights supported by this channel."), "url": z.string().url().regex(new RegExp("^https://")).describe("HTTPS URL for submitting the rights request.").optional(), "email": z.string().email().describe("Email address for submitting the rights request.").optional(), "languages": z.array(z.string()).describe("BCP 47 language tags supported by this channel.").optional(), "countries": z.array(z.string().regex(new RegExp("^[A-Z]{2}$"))).describe("ISO 3166-1 alpha-2 countries this channel serves, when the provider routes rights by country.").optional() }).strict().and(z.union([z.any(), z.any()]))).describe("Rights request channels and the rights each channel supports. At least one declared channel MUST support one or more of access, erasure, or objection; schema validators enforce this with draft-07 contains, and consumers whose SDK drops contains need an equivalent runtime check."), "response_sla_days": z.number().int().gte(1).lte(90).describe("Maximum response time in days for rights requests handled through the declared rights channels. For provider-published signals, providers SHOULD avoid duplicating this field across every signal unless the value varies by signal or upstream source; consumers MAY also consult the provider's public privacy policy or registry disclosures when present.").optional(), "ccpa_opt_out_url": z.string().url().regex(new RegExp("^https://")).describe("US-specific 'Do Not Sell or Share' opt-out URL where required.").optional() }).strict().describe("Per-signal data-subject-rights routing. Inline on the signal because upstream source, rights routing, and response commitments can differ by segment or may be unavailable from a public provider document for custom/private signals. This is a contact/routing reference, not a machine-callable AdCP API.").optional(), "last_updated": z.string().datetime().describe("When this definition record was last updated. This indicates freshness of the definition record, not an attestation that the underlying data or model was refreshed at that time.").optional(), "dts_compliant_version": z.string().describe("IAB Data Transparency Standard version this signal definition self-attests as satisfying, when applicable.").optional() }).catchall(z.any()).and(z.intersection(z.intersection(z.any(), z.intersection(z.any(), z.any())), z.intersection(z.any(), z.intersection(z.any(), z.any())))).describe("Definition of a signal in published adagents.json signals[]. The publishing domain supplies the namespace, so this definition carries a local id rather than a signal_ref. Media-buy products reference this definition with signal_ref scope 'data_provider', data_provider_domain set to the publishing domain, and signal_id set to this id. Some constraints use JSON Schema draft-07 conditional keywords such as if/then and contains; SDK generators that drop those keywords need equivalent runtime guards.")).describe("Signal definitions published by this domain. Each entry defines a signal id within this file's publishing-domain namespace; entries do not include signal_ref objects. Signals Protocol discovery and media-buy product targeting reference these through signal_ref scope 'data_provider', with data_provider_domain set to this file's publishing domain and signal_id set to signals[].id.").optional(), "signal_tags": z.record(z.string(), z.object({ "name": z.string().describe("Human-readable name for this tag"), "description": z.string().describe("Description of what this tag represents") }).catchall(z.any())).describe("Metadata for each tag referenced by signals. Provides human-readable context for signal tag values.").optional() }).catchall(z.any()).and(z.intersection(z.union([z.any(), z.any(), z.any(), z.any(), z.any(), z.any()]), z.any())).describe("Inline structure variant - contains full agent authorization data")]).describe("Declaration of authorized agents for advertising inventory and data signals. Hosted at /.well-known/adagents.json on publisher domains (for properties) or data provider domains (for signals). Can either contain the full structure inline or reference an authoritative URL.") export type AdagentsJson = z.infer; diff --git a/src/lib/utils/adcp-version-config.ts b/src/lib/utils/adcp-version-config.ts index 6ea2ef88d..9ac38bcfd 100644 --- a/src/lib/utils/adcp-version-config.ts +++ b/src/lib/utils/adcp-version-config.ts @@ -15,6 +15,22 @@ import { ADCP_VERSION, COMPATIBLE_ADCP_VERSIONS, parseAdcpMajorVersion } from '../version'; import { ConfigurationError } from '../errors'; import { hasSchemaBundle, resolveBundleKey, toReleasePrecisionWire } from '../validation/schema-loader'; +import { gte as semverGte, valid as validSemver } from 'semver'; + +function comparableAdcpSemver(version: string): string | undefined { + const match = /^v?(\d+)\.(\d+)(?:\.(\d+))?((?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?)$/.exec(version.trim()); + if (!match) return undefined; + const normalized = `${match[1]}.${match[2]}.${match[3] ?? '0'}${match[4] ?? ''}`; + return validSemver(normalized) ?? undefined; +} + +/** Compare full-semver and release-precision AdCP identifiers safely. */ +export function isAdcpVersionAtLeast(version: string | undefined, minimum: string): boolean { + if (version === undefined) return false; + const comparable = comparableAdcpSemver(version); + const comparableMinimum = comparableAdcpSemver(minimum); + return comparable !== undefined && comparableMinimum !== undefined && semverGte(comparable, comparableMinimum); +} /** * Resolve and validate a configured `adcpVersion`. Returns the value to store diff --git a/src/lib/v2/projection/builders.ts b/src/lib/v2/projection/builders.ts index 0d629ca32..4143addb1 100644 --- a/src/lib/v2/projection/builders.ts +++ b/src/lib/v2/projection/builders.ts @@ -12,7 +12,9 @@ import type { ImageFormatDeclaration, NativeInFeedFormatDeclaration, ResponsiveCreativeFormatDeclaration, + SellerRenderedStatefulDisplayFormatDeclaration, SponsoredPlacementFormatDeclaration, + CoordinatedPlacementsFormatDeclaration, VASTVideoFormatDeclaration, } from '../../types/tools.generated'; import type { CanonicalFormatDeclaration as CanonicalBaseFormatDeclaration } from './legacy-metadata'; @@ -35,6 +37,8 @@ interface CanonicalFormatDeclarationMap { native_in_feed: WithCommonDeclaration; responsive_creative: WithCommonDeclaration; agent_placement: WithCommonDeclaration; + seller_rendered_stateful_display: WithCommonDeclaration; + coordinated_placements: WithCommonDeclaration; custom: WithCommonDeclaration; } @@ -185,6 +189,20 @@ export function agentPlacementFormatDeclaration( return canonicalFormatDeclaration('agent_placement', params, fields); } +export function sellerRenderedStatefulDisplayFormatDeclaration( + params: CanonicalFormatParams<'seller_rendered_stateful_display'>, + fields: CanonicalFormatDeclarationFields<'seller_rendered_stateful_display'> = {} +): CanonicalFormatDeclaration<'seller_rendered_stateful_display'> { + return canonicalFormatDeclaration('seller_rendered_stateful_display', params, fields); +} + +export function coordinatedPlacementsFormatDeclaration( + params: CanonicalFormatParams<'coordinated_placements'>, + fields: CanonicalFormatDeclarationFields<'coordinated_placements'> = {} +): CanonicalFormatDeclaration<'coordinated_placements'> { + return canonicalFormatDeclaration('coordinated_placements', params, fields); +} + export function customFormatDeclaration( format_shape: string, format_schema: NonNullable['format_schema']>, @@ -224,6 +242,8 @@ export const CanonicalFormat = { nativeInFeed: nativeInFeedFormatDeclaration, responsiveCreative: responsiveCreativeFormatDeclaration, agentPlacement: agentPlacementFormatDeclaration, + sellerRenderedStatefulDisplay: sellerRenderedStatefulDisplayFormatDeclaration, + coordinatedPlacements: coordinatedPlacementsFormatDeclaration, custom: customFormatDeclaration, productCard, productCardDetailed, diff --git a/src/lib/v2/projection/creative-delivery.ts b/src/lib/v2/projection/creative-delivery.ts index 974e19f51..43b60ee95 100644 --- a/src/lib/v2/projection/creative-delivery.ts +++ b/src/lib/v2/projection/creative-delivery.ts @@ -270,6 +270,8 @@ export function stripLegacyCreativeIdentity(value: T): CanonicalCreativeRespo 'native_in_feed', 'responsive_creative', 'agent_placement', + 'seller_rendered_stateful_display', + 'coordinated_placements', 'custom', ]); const creativeIdentityKey = (key: string): boolean => @@ -648,6 +650,8 @@ const CANONICAL_FORMAT_KINDS = new Set([ 'native_in_feed', 'responsive_creative', 'agent_placement', + 'seller_rendered_stateful_display', + 'coordinated_placements', 'custom', ]); diff --git a/src/lib/v2/projection/index.ts b/src/lib/v2/projection/index.ts index 60aec5cd4..b04af3653 100644 --- a/src/lib/v2/projection/index.ts +++ b/src/lib/v2/projection/index.ts @@ -96,7 +96,9 @@ export { productCard, productCardDetailed, responsiveCreativeFormatDeclaration, + sellerRenderedStatefulDisplayFormatDeclaration, sponsoredPlacementFormatDeclaration, + coordinatedPlacementsFormatDeclaration, videoHostedFormatDeclaration, videoVastFormatDeclaration, type CanonicalFormatDeclaration, diff --git a/src/lib/v2/projection/types.ts b/src/lib/v2/projection/types.ts index e675b6bd1..f3f814128 100644 --- a/src/lib/v2/projection/types.ts +++ b/src/lib/v2/projection/types.ts @@ -44,6 +44,8 @@ export type CanonicalFormatKind = | 'native_in_feed' | 'responsive_creative' | 'agent_placement' + | 'seller_rendered_stateful_display' + | 'coordinated_placements' | 'custom'; /** diff --git a/src/lib/version.ts b/src/lib/version.ts index 73c02beb8..0107a2ca9 100644 --- a/src/lib/version.ts +++ b/src/lib/version.ts @@ -9,7 +9,7 @@ export const LIBRARY_VERSION = '14.0.0-beta.7'; /** * AdCP specification version this library is built for */ -export const ADCP_VERSION = '3.2.0-beta.5'; +export const ADCP_VERSION = '3.2.0-beta.6'; /** * AdCP major version sent with every request (adcp_major_version field). @@ -81,8 +81,8 @@ export const COMPATIBLE_ADCP_VERSIONS = [ '3.1.16', '3.1.17', '3.1.18', - '3.2.0-beta.5', - '3.2-beta.5', + '3.2.0-beta.6', + '3.2-beta.6', ] as const; /** @@ -100,9 +100,9 @@ export type AdcpVersion = (typeof COMPATIBLE_ADCP_VERSIONS)[number]; */ export const VERSION_INFO = { library: '14.0.0-beta.7', - adcp: '3.2.0-beta.5', + adcp: '3.2.0-beta.6', compatibleVersions: COMPATIBLE_ADCP_VERSIONS, - generatedAt: '2026-08-22T16:49:19.654Z', + generatedAt: '2026-08-24T04:17:13.847Z', } as const; /** diff --git a/src/type-tests/projection-adcp-3-2-fields.type-test.ts b/src/type-tests/projection-adcp-3-2-fields.type-test.ts index 28db91df8..23c8ad885 100644 --- a/src/type-tests/projection-adcp-3-2-fields.type-test.ts +++ b/src/type-tests/projection-adcp-3-2-fields.type-test.ts @@ -3,6 +3,7 @@ import { toCanonicalFormatOptionsWithRoutes as projectionToCanonicalFormatOptionsWithRoutes, type V2ProductFormatDeclaration, } from '../lib/v2/projection'; +import type { CanonicalFormatOption } from '../lib/types/core.generated'; type Equal = (() => T extends A ? 1 : 2) extends () => T extends B ? 1 : 2 @@ -41,6 +42,11 @@ const declarations: V2ProductFormatDeclaration[] = [ }, ]; +const promotedCompactOptions: CanonicalFormatOption[] = [ + { format_kind: 'seller_rendered_stateful_display', params: {} }, + { format_kind: 'coordinated_placements', params: {} }, +]; + const projectedFromRoot = toCanonicalFormatOptionsWithRoutes('adcp-3-2-product', declarations); const projectedFromSubpath = projectionToCanonicalFormatOptionsWithRoutes('adcp-3-2-product', declarations); const sampleRenderUrl: ProductFormatDeclaration['sample_render_url'] = @@ -50,3 +56,4 @@ const localePolicy: ProductFormatDeclaration['locale_policy'] = projectedFromRoo void projectedFromSubpath; void sampleRenderUrl; void localePolicy; +void promotedCompactOptions; diff --git a/src/type-tests/zod-schema-ergonomics.type-test.ts b/src/type-tests/zod-schema-ergonomics.type-test.ts index 2937d005c..39717a8a4 100644 --- a/src/type-tests/zod-schema-ergonomics.type-test.ts +++ b/src/type-tests/zod-schema-ergonomics.type-test.ts @@ -11,7 +11,28 @@ import { CanonicalFormatImageSchema, MediaBuyFeaturesSchema, ProductSchema, + ProductFormatDeclarationSchema, + PlacementSchema, + FormatSchema, + TransformerSchema, + AvailablePackageSchema, + ListCreativeFormatsResponseSchema, + PackageStatusSchema, + ListTransformersResponseCreativeAgentSchema, + GetAdCPCapabilitiesResponseSchema, + ListTransformersResponseSchema, } from '../lib/types/schemas.generated'; +import type { Format, AvailablePackage } from '../lib/types/core.generated'; +import type { + ProductFormatDeclaration, + Placement, + Transformer, + ListCreativeFormatsResponse, + PackageStatus, + ListTransformersResponseCreativeAgent, + GetAdCPCapabilitiesResponse, + ListTransformersResponse, +} from '../lib/types/tools.generated'; const ProductWithCacheSchema = ProductSchema.extend({ _cached_at: z.string().datetime(), @@ -52,3 +73,41 @@ const MediaBuyFeaturesExtended = MediaBuyFeaturesSchema.extend({ _evaluated_at: z.string().datetime(), }); void MediaBuyFeaturesExtended; + +// Beta.6 expansion pushed these declarations across TS7056's serialization +// threshold. Their explicit annotations must retain parse output types and, +// for object schemas, the public composition helpers. +declare const unknownInput: unknown; +const productFormat: ProductFormatDeclaration = ProductFormatDeclarationSchema.parse(unknownInput); +const placement: Placement = PlacementSchema.parse(unknownInput); +const format: Format = FormatSchema.parse(unknownInput); +const transformer: Transformer = TransformerSchema.parse(unknownInput); +const availablePackage: AvailablePackage = AvailablePackageSchema.parse(unknownInput); +const formatsResponse: ListCreativeFormatsResponse = ListCreativeFormatsResponseSchema.parse(unknownInput); +const packageStatus: PackageStatus = PackageStatusSchema.parse(unknownInput); +const transformerResponse: ListTransformersResponseCreativeAgent = + ListTransformersResponseCreativeAgentSchema.parse(unknownInput); +const capabilities: GetAdCPCapabilitiesResponse = GetAdCPCapabilitiesResponseSchema.parse(unknownInput); +const listTransformers: ListTransformersResponse = ListTransformersResponseSchema.parse(unknownInput); +void [ + productFormat, + placement, + format, + transformer, + availablePackage, + formatsResponse, + packageStatus, + transformerResponse, + capabilities, + listTransformers, +]; + +ProductFormatDeclarationSchema.pick({ format_kind: true }); +FormatSchema.pick({ format_id: true }); +TransformerSchema.pick({ transformer_id: true }); +AvailablePackageSchema.pick({ package_id: true }); +ListCreativeFormatsResponseSchema.pick({ formats: true }); +PackageStatusSchema.pick({ package_id: true }); +ListTransformersResponseCreativeAgentSchema.pick({ transformers: true }); +GetAdCPCapabilitiesResponseSchema.pick({ status: true }); +ListTransformersResponseSchema.pick({ transformers: true }); diff --git a/test/capabilities-overrides.test.js b/test/capabilities-overrides.test.js index 9ec278b70..c060fa69f 100644 --- a/test/capabilities-overrides.test.js +++ b/test/capabilities-overrides.test.js @@ -218,7 +218,7 @@ describe('capabilities.overrides — per-domain merge (#654)', () => { }); const caps = await callCapabilities(server); assert.strictEqual(caps.request_signing.covers_content_digest, 'required'); - assert.strictEqual(caps.adcp_version, '3.2-beta.5'); + assert.strictEqual(caps.adcp_version, '3.2-beta.6'); }); it('undefined overrides are no-ops', async () => { diff --git a/test/lib/adcp-major-version.test.js b/test/lib/adcp-major-version.test.js index a0fb9e0f8..2a273ebaf 100644 --- a/test/lib/adcp-major-version.test.js +++ b/test/lib/adcp-major-version.test.js @@ -201,10 +201,10 @@ describe('AdCP 3.2 strict compact request envelopes', () => { const agent = { id: 'beta1-a2a', name: 'beta1-a2a', agent_uri: 'https://seller.example/a2a', protocol: 'a2a' }; for (const [toolName, request] of Object.entries(compactRequests)) { - const prepared = prepareProtocolToolCall(agent, request, { toolName, adcpVersion: '3.2.0-beta.5' }).args; - assert.strictEqual(prepared.adcp_version, '3.2-beta.5', `${toolName} keeps the release pin`); + const prepared = prepareProtocolToolCall(agent, request, { toolName, adcpVersion: '3.2.0-beta.6' }).args; + assert.strictEqual(prepared.adcp_version, '3.2-beta.6', `${toolName} keeps the release pin`); assert.strictEqual(prepared.adcp_major_version, 3, `${toolName} keeps the SDK-injected major`); - const validation = validateRequest(toolName, prepared, '3.2.0-beta.5'); + const validation = validateRequest(toolName, prepared, '3.2.0-beta.6'); assert.strictEqual(validation.valid, true, `${toolName}: ${JSON.stringify(validation.issues)}`); } }); diff --git a/test/lib/adcp-version-projection.test.js b/test/lib/adcp-version-projection.test.js index 96adff571..f68919541 100644 --- a/test/lib/adcp-version-projection.test.js +++ b/test/lib/adcp-version-projection.test.js @@ -4,8 +4,16 @@ const { sellerAdvertises31, shouldOmit31Fields, omit31BrandFields, + isAdcpVersionAtLeast, } = require('../../dist/lib/utils/adcp-version-config.js'); +test('isAdcpVersionAtLeast compares release-precision prerelease pins', () => { + assert.equal(isAdcpVersionAtLeast('3.2-beta.4', '3.2.0-beta.5'), false); + assert.equal(isAdcpVersionAtLeast('3.2-beta.5', '3.2.0-beta.5'), true); + assert.equal(isAdcpVersionAtLeast('3.2-beta.6', '3.2.0-beta.5'), true); + assert.equal(isAdcpVersionAtLeast('3.2.0', '3.2.0-beta.5'), true); +}); + test('sellerAdvertises31: buildVersion is advisory and never used for negotiation', () => { assert.equal(sellerAdvertises31({ buildVersion: '3.1.0' }), false); assert.equal(sellerAdvertises31({ buildVersion: '3.2.1', supportedVersions: ['3.0'] }), false); diff --git a/test/lib/beta6-reporting-version.test.js b/test/lib/beta6-reporting-version.test.js new file mode 100644 index 000000000..8f881090e --- /dev/null +++ b/test/lib/beta6-reporting-version.test.js @@ -0,0 +1,184 @@ +const { describe, test } = require('node:test'); +const assert = require('node:assert'); + +const { SingleAgentClient, ProtocolClient, ProtocolFeatureUnsupportedError } = require('../../dist/lib/index.js'); + +function clientForSellerVersion(version) { + const client = new SingleAgentClient( + { + id: 'seller', + name: 'Seller', + agent_uri: 'https://seller.example.com/mcp', + protocol: 'mcp', + }, + { + adcpVersion: '3.2.0-beta.6', + validateFeatures: false, + validation: { requests: 'off', responses: 'off' }, + } + ); + client.getCapabilities = async () => ({ + version: 'v3', + majorVersions: [3], + supportedVersions: [version], + protocols: ['media_buy'], + features: {}, + extensions: [], + _synthetic: false, + }); + client.ensureEndpointDiscovered = async () => client.agent; + return client; +} + +function beta6MetricRequests(client) { + return [ + () => client.getProducts({ filters: { required_metrics: ['viewable_rate'] } }), + () => + client.createMediaBuy({ + account: { account_id: 'account-1' }, + brand: { domain: 'advertiser.example' }, + start_time: '2026-09-01T00:00:00Z', + end_time: '2026-09-30T00:00:00Z', + packages: [ + { + package_id: 'package-1', + product_id: 'product-1', + pricing_option_id: 'price-1', + committed_metrics: [{ scope: 'standard', metric_id: 'quartile_25' }], + }, + ], + }), + () => + client.updateMediaBuy({ + account: { account_id: 'account-1' }, + media_buy_id: 'buy-1', + packages: [ + { package_id: 'package-1', committed_metrics: [{ scope: 'standard', metric_id: 'viewed_seconds' }] }, + ], + }), + () => + client.providePerformanceFeedback({ + account: { account_id: 'account-1' }, + idempotency_key: 'feedback-key-0001', + metric: { scope: 'standard', metric_id: 'measurable_impressions' }, + }), + () => + client.providePerformanceFeedback({ + account: { account_id: 'account-1' }, + idempotency_key: 'feedback-key-0002', + metric: { + scope: 'vendor', + vendor: { domain: 'measurement.example' }, + metric_id: 'attention_units', + qualifier: { attribution_methodology: 'modeled' }, + }, + }), + ]; +} + +describe('beta.6 reporting version gates', () => { + test('direct client rejects beta.6 delivery controls for beta.5 without dispatch', async () => { + const originalCallTool = ProtocolClient.callTool; + let calls = 0; + ProtocolClient.callTool = async () => { + calls += 1; + return { status: 'completed', media_buy_deliveries: [] }; + }; + try { + await assert.rejects( + clientForSellerVersion('3.2.0-beta.5').getMediaBuyDelivery({ + requested_metrics: ['viewable_rate'], + reporting_dimensions: { format: { sort_direction: 'asc' } }, + }), + error => error instanceof ProtocolFeatureUnsupportedError && error.details.required_version === '3.2.0-beta.6' + ); + assert.strictEqual(calls, 0); + } finally { + ProtocolClient.callTool = originalCallTool; + } + }); + + test('direct client dispatches beta.6 delivery controls to beta.6 sellers', async () => { + const originalCallTool = ProtocolClient.callTool; + const calls = []; + ProtocolClient.callTool = async (_agent, taskName, params) => { + calls.push({ taskName, params }); + return { status: 'completed', media_buy_deliveries: [] }; + }; + try { + await clientForSellerVersion('3.2.0-beta.6').getMediaBuyDelivery({ + requested_metrics: ['viewable_rate'], + reporting_dimensions: { format: { sort_direction: 'asc' } }, + }); + assert.strictEqual(calls.length, 1); + assert.strictEqual(calls[0].taskName, 'get_media_buy_delivery'); + assert.deepStrictEqual(calls[0].params.requested_metrics, ['viewable_rate']); + } finally { + ProtocolClient.callTool = originalCallTool; + } + }); + + test('direct client rejects beta.6 metric identities across established tools for beta.5', async () => { + const originalCallTool = ProtocolClient.callTool; + let calls = 0; + ProtocolClient.callTool = async () => { + calls += 1; + return { status: 'completed' }; + }; + try { + for (const request of beta6MetricRequests(clientForSellerVersion('3.2.0-beta.5'))) { + await assert.rejects( + request(), + error => error instanceof ProtocolFeatureUnsupportedError && error.details.required_version === '3.2.0-beta.6' + ); + } + assert.strictEqual(calls, 0); + } finally { + ProtocolClient.callTool = originalCallTool; + } + }); + + test('configured beta.5 pin rejects beta.6 reporting before capability discovery', async () => { + const client = clientForSellerVersion('3.2.0-beta.6'); + client.config.wireAdcpVersion = '3.2.0-beta.5'; + let capabilityCalls = 0; + client.getCapabilities = async () => { + capabilityCalls += 1; + throw new Error('capability discovery must not run'); + }; + + await assert.rejects( + client.getMediaBuyDelivery({ requested_metrics: ['viewable_rate'] }), + error => error instanceof ProtocolFeatureUnsupportedError && error.details.current_version === '3.2.0-beta.5' + ); + assert.strictEqual(capabilityCalls, 0); + }); + + test('synthetic v2 capability fallback still rejects beta.6 reporting without dispatch', async () => { + const client = clientForSellerVersion('3.2.0-beta.6'); + client.getCapabilities = async () => ({ + version: 'v2', + majorVersions: [2], + supportedVersions: [], + protocols: ['media_buy'], + features: {}, + extensions: [], + _synthetic: true, + }); + const originalCallTool = ProtocolClient.callTool; + let calls = 0; + ProtocolClient.callTool = async () => { + calls += 1; + return { status: 'completed' }; + }; + try { + await assert.rejects( + client.getProductsLegacy({ filters: { required_metrics: ['viewable_rate'] } }), + error => error instanceof ProtocolFeatureUnsupportedError && error.details.required_version === '3.2.0-beta.6' + ); + assert.strictEqual(calls, 0); + } finally { + ProtocolClient.callTool = originalCallTool; + } + }); +}); diff --git a/test/lib/canonical-format-builders.test.js b/test/lib/canonical-format-builders.test.js index cd6aa88af..cebdbbb48 100644 --- a/test/lib/canonical-format-builders.test.js +++ b/test/lib/canonical-format-builders.test.js @@ -74,11 +74,21 @@ describe('canonical creative format helpers', () => { root.CanonicalFormat.nativeInFeed({ assets: [] }), root.CanonicalFormat.responsiveCreative({ aspect_ratios: ['1:1'] }), root.CanonicalFormat.agentPlacement({ requirements: {} }), + root.CanonicalFormat.sellerRenderedStatefulDisplay({ states: [], breakpoints: [], canvas: {} }), + root.CanonicalFormat.coordinatedPlacements({ placements: [] }), ]; assert.deepStrictEqual( declarations.map(decl => decl.format_kind), - ['image_carousel', 'sponsored_placement', 'native_in_feed', 'responsive_creative', 'agent_placement'] + [ + 'image_carousel', + 'sponsored_placement', + 'native_in_feed', + 'responsive_creative', + 'agent_placement', + 'seller_rendered_stateful_display', + 'coordinated_placements', + ] ); }); diff --git a/test/lib/cli-storyboard-file-flag.test.js b/test/lib/cli-storyboard-file-flag.test.js index e6dd87c8a..fed1f0e44 100644 --- a/test/lib/cli-storyboard-file-flag.test.js +++ b/test/lib/cli-storyboard-file-flag.test.js @@ -144,11 +144,11 @@ test('--compliance-version rejects an environment cache from a different protoco scenarioPath, '--dry-run', '--compliance-version', - '3.2.0-beta.5', + '3.2.0-beta.6', ]); assert.strictEqual(result.status, 2); - assert.match(result.stderr, /--compliance-version 3\.2\.0-beta\.5/); + assert.match(result.stderr, /--compliance-version 3\.2\.0-beta\.6/); assert.match(result.stderr, /cache that declares AdCP 3\.1\.13/); } finally { if (oldComplianceDir === undefined) delete process.env.ADCP_COMPLIANCE_DIR; @@ -208,7 +208,7 @@ test('--file forwards explicit cache authority for a declared test kit', { timeo const cliHome = path.join(fixtureRoot, 'home'); mkdirSync(kitDir, { recursive: true }); mkdirSync(cliHome, { recursive: true }); - writeComplianceIndex(complianceDir, '3.2.0-beta.5'); + writeComplianceIndex(complianceDir, '3.2.0-beta.6'); writeFileSync( path.join(kitDir, 'live.yaml'), ['auth:', ' api_key: "cli-declared-kit-key"', ' probe_task: list_creatives', ''].join('\n') @@ -287,7 +287,7 @@ test('--file forwards explicit cache authority for a declared test kit', { timeo assert.strictEqual(explicitDir.status, 0, explicitDir.stderr); assert.ok(authorizations.includes('Bearer cli-declared-kit-key')); - const versionOnly = await run(['--compliance-version', '3.2.0-beta.5'], { + const versionOnly = await run(['--compliance-version', '3.2.0-beta.6'], { ADCP_COMPLIANCE_DIR: complianceDir, }); assert.strictEqual(versionOnly.status, 0, versionOnly.stderr); diff --git a/test/lib/cli-storyboard-show-specialism.test.js b/test/lib/cli-storyboard-show-specialism.test.js index 594661b95..b7271da37 100644 --- a/test/lib/cli-storyboard-show-specialism.test.js +++ b/test/lib/cli-storyboard-show-specialism.test.js @@ -101,7 +101,10 @@ test('show --specialism --json emits structured envelope', () => { test('show --specialism renders compound capability gates instead of always graded', () => { const complianceDir = mkdtempSync(path.join(os.tmpdir(), 'adcp-compound-gate-cli-')); try { - cpSync(path.resolve(__dirname, '../../compliance/cache/3.2.0-beta.5'), complianceDir, { recursive: true }); + cpSync(path.resolve(__dirname, '../../compliance/cache/latest'), complianceDir, { + recursive: true, + dereference: true, + }); const indexPath = path.join(complianceDir, 'index.json'); const index = JSON.parse(readFileSync(indexPath, 'utf8')); index.universal.push('compound-gated'); diff --git a/test/lib/cli-test-kit-compliance-version.test.js b/test/lib/cli-test-kit-compliance-version.test.js index 54b5b28e9..88ed47a0b 100644 --- a/test/lib/cli-test-kit-compliance-version.test.js +++ b/test/lib/cli-test-kit-compliance-version.test.js @@ -327,7 +327,7 @@ describe('storyboard run --test-kit compliance-line selection', () => { }); test('malformed test-kit YAML never echoes source credentials', () => { - const fixture = createTestKitCache('3.2.0-beta.5'); + const fixture = createTestKitCache('3.2.0-beta.6'); const secret = 'sk-cli-secret-that-must-not-reach-stderr'; writeFileSync(fixture.testKitPath, `auth: [${secret}`); try { @@ -350,7 +350,7 @@ describe('storyboard run --test-kit compliance-line selection', () => { }); test('a falsy parsed --test-kit is forwarded and rejected instead of falling back to disk', () => { - const fixture = createTestKitCache('3.2.0-beta.5'); + const fixture = createTestKitCache('3.2.0-beta.6'); writeFileSync(fixture.testKitPath, 'null\n'); try { const result = runCli([ diff --git a/test/lib/compact-commitment-cli.test.js b/test/lib/compact-commitment-cli.test.js index 630e82896..76c1c64b0 100644 --- a/test/lib/compact-commitment-cli.test.js +++ b/test/lib/compact-commitment-cli.test.js @@ -96,7 +96,7 @@ before(async () => { createAdcpServer({ name: 'Compact commitment seller', version: '1.0.0', - adcpVersion: '3.2.0-beta.5', + adcpVersion: '3.2.0-beta.6', idempotency: 'disabled', validation: { requests: 'strict', responses: 'strict' }, mediaBuy: { @@ -167,7 +167,7 @@ for (const [tool, payload] of calls) { ); assert.doesNotMatch(result.stderr, /Schema validation failed/, `${tool}: ${result.stderr}`); const output = JSON.parse(result.stdout); - assert.strictEqual(output.data.adcp_version, '3.2-beta.5'); + assert.strictEqual(output.data.adcp_version, '3.2-beta.6'); assert.strictEqual(output.data.media_buy_id, 'mb-1'); }); } diff --git a/test/lib/compact-lifecycle-storyboard-runner.test.js b/test/lib/compact-lifecycle-storyboard-runner.test.js index e1897c37c..fdfe6ff5e 100644 --- a/test/lib/compact-lifecycle-storyboard-runner.test.js +++ b/test/lib/compact-lifecycle-storyboard-runner.test.js @@ -12,7 +12,7 @@ const { TOOL_INPUT_SHAPE, toMcpResponse } = require('../../dist/lib/server/test- const { getComplianceStoryboardById } = require('../../dist/lib/testing/storyboard/index.js'); const { runStoryboard } = require('../../dist/lib/testing/storyboard/runner.js'); -const ADCP_VERSION = '3.2.0-beta.5'; +const ADCP_VERSION = '3.2.0-beta.6'; const ACCOUNT = { brand: { domain: 'acmeoutdoor.example' }, operator: 'pinnacle-agency.example', @@ -41,7 +41,7 @@ function closeServer(server) { } function assertBeta3Envelope(request) { - assert.equal(request.adcp_version, '3.2-beta.5'); + assert.equal(request.adcp_version, '3.2-beta.6'); assert.equal(request.adcp_major_version, 3); } diff --git a/test/lib/conformance-arbitrary.test.js b/test/lib/conformance-arbitrary.test.js index 58ab9bfb9..8e3aefc3e 100644 --- a/test/lib/conformance-arbitrary.test.js +++ b/test/lib/conformance-arbitrary.test.js @@ -64,7 +64,7 @@ describe('conformance: schemaToArbitrary', { concurrency: false }, () => { assert.ok(!UPDATE_TIER_TOOLS.includes(tool), `${tool} is not forced into legacy defaults`); } for (const tool of compactTools) { - assert.equal(hasSchemas(tool, { version: '3.2.0-beta.5' }), true, `${tool} 3.2 schemas are loadable`); + assert.equal(hasSchemas(tool, { version: '3.2.0-beta.6' }), true, `${tool} 3.2 schemas are loadable`); assert.equal(hasSchemas(tool, { version: '3.1.18' }), false, `${tool} is gated out of 3.1`); assert.equal(hasSchemas(tool, { version: '3.0.25' }), false, `${tool} is gated out of 3.0`); } @@ -74,9 +74,9 @@ describe('conformance: schemaToArbitrary', { concurrency: false }, () => { const versionOnly = prepareConformanceProbeRequest( 'list_products', { adcp_version: '99.99' }, - { adcpVersion: '3.2.0-beta.5' } + { adcpVersion: '3.2.0-beta.6' } ); - assert.equal(versionOnly.adcp_version, '3.2-beta.5'); + assert.equal(versionOnly.adcp_version, '3.2-beta.6'); assert.equal(Object.hasOwn(versionOnly, 'adcp_major_version'), false); const majorOnly = prepareConformanceProbeRequest( @@ -140,8 +140,8 @@ describe('conformance: schemaToArbitrary', { concurrency: false }, () => { }; for (const [tool, sample] of Object.entries(samples)) { - const probe = prepareConformanceProbeRequest(tool, sample, { fixtures, adcpVersion: '3.2.0-beta.5' }); - const validate = makeAjv().compile(loadRequestSchema(tool, { version: '3.2.0-beta.5' })); + const probe = prepareConformanceProbeRequest(tool, sample, { fixtures, adcpVersion: '3.2.0-beta.6' }); + const validate = makeAjv().compile(loadRequestSchema(tool, { version: '3.2.0-beta.6' })); assert.equal(validate(probe), true, `${tool}: ${JSON.stringify(validate.errors)}`); } @@ -284,6 +284,32 @@ describe('conformance: schemaToArbitrary', { concurrency: false }, () => { } }); + test('if/else: removes fields forbidden outside the discriminator branch', () => { + const schema = { + type: 'object', + properties: { + format_kind: { enum: ['image', 'coordinated_placements'] }, + component_assets: { type: 'object' }, + }, + allOf: [ + { + if: { + properties: { format_kind: { const: 'coordinated_placements' } }, + required: ['format_kind'], + }, + then: { required: ['component_assets'] }, + else: { not: { required: ['component_assets'] } }, + }, + ], + }; + const validate = makeAjv().compile(schema); + const samples = fc.sample(schemaToArbitrary(schema), { numRuns: 100, seed: 42 }); + assert.ok( + samples.every(sample => validate(sample)), + JSON.stringify(validate.errors) + ); + }); + test('fixtures: scalar creative_id draws from the pool', () => { const pool = ['cre_abc', 'cre_def', 'cre_ghi']; const schema = { diff --git a/test/lib/conformance-seeder.test.js b/test/lib/conformance-seeder.test.js index a9b067c16..58012de2e 100644 --- a/test/lib/conformance-seeder.test.js +++ b/test/lib/conformance-seeder.test.js @@ -271,8 +271,8 @@ describe('conformance: seedFixtures', () => { }); let proposalSequence = 0; const { server, port } = await startAgent({ - adcpVersion: '3.2.0-beta.5', - // This regression fixture must stay valid against the selected beta.1 + adcpVersion: '3.2.0-beta.6', + // This regression fixture must stay valid against the selected beta.6 // response schemas. Strict mode turns schema drift into a test failure // instead of the SDK's usual non-blocking validation warning. validation: { responses: 'strict' }, @@ -419,7 +419,7 @@ describe('conformance: seedFixtures', () => { brand_id: 'compact_brand', }); assert.equal(observed.bought.length, 1); - assert.equal(observed.bought[0].adcp_version, '3.2-beta.5'); + assert.equal(observed.bought[0].adcp_version, '3.2-beta.6'); assert.equal(observed.bought[0].adcp_major_version, 3); assert.match(observed.bought[0].idempotency_key, /^[0-9a-f-]{36}$/); assert.equal(observed.bought[0].feed_version, 'feed-compact-1'); @@ -435,7 +435,7 @@ describe('conformance: seedFixtures', () => { test('default seeding does not probe the compact lifecycle unless the selected bundle enables it', async () => { let compactCalls = 0; const { server, port } = await startAgent({ - adcpVersion: '3.2.0-beta.5', + adcpVersion: '3.2.0-beta.6', mediaBuy: { getProducts: async () => ({ products: [], cache_scope: 'public' }), listProducts: async () => { diff --git a/test/lib/creative-format-projection.test.js b/test/lib/creative-format-projection.test.js index 7becbb591..eff0cec44 100644 --- a/test/lib/creative-format-projection.test.js +++ b/test/lib/creative-format-projection.test.js @@ -89,6 +89,29 @@ describe('creative format delivery projection', () => { assert.equal(safe.format_id, undefined); }); + test('preserves beta.6 canonical format kinds in diagnostics and canonical delivery', () => { + for (const formatKind of ['seller_rendered_stateful_display', 'coordinated_placements']) { + const safe = stripLegacyCreativeIdentity({ + format_kind: formatKind, + message: `${formatKind} accepted`, + }); + assert.equal(safe.format_kind, formatKind); + assert.equal(safe.message, `${formatKind} accepted`); + + const canonical = projectCreativeForDelivery( + { + creative_id: `creative-${formatKind}`, + name: formatKind, + format_kind: formatKind, + assets: {}, + }, + {}, + 'canonical' + ); + assert.equal(canonical.format_kind, formatKind); + } + }); + test('sanitizes class-instance own fields and fails closed on neutral tuples and accessors', () => { class LegacyCarrier { constructor() { diff --git a/test/lib/enum-arrays.test.js b/test/lib/enum-arrays.test.js index 298826d12..40c23a7f7 100644 --- a/test/lib/enum-arrays.test.js +++ b/test/lib/enum-arrays.test.js @@ -43,6 +43,38 @@ describe('Enum value arrays (enums.generated)', () => { ); }); + it('exports the beta.6 delivery metric identity and sorting surface', async () => { + if (!enums) enums = await import('../../dist/lib/types/enums.generated.js'); + + const leafMetrics = [ + 'quartile_25', + 'quartile_50', + 'quartile_75', + 'quartile_100', + 'viewable_rate', + 'viewable_impressions', + 'measurable_impressions', + 'viewed_seconds', + ]; + const transactionalSortMetrics = [ + 'cpm', + 'cost_per_completed_view', + 'downloads', + 'units_sold', + 'new_to_brand_units', + 'plays', + 'commissionable_value', + ]; + + for (const metric of [...leafMetrics, 'time_based_views']) { + assert.ok(enums.AvailableMetricValues.includes(metric), `AvailableMetricValues should include ${metric}`); + } + for (const metric of [...leafMetrics, ...transactionalSortMetrics]) { + assert.ok(enums.SortMetricValues.includes(metric), `SortMetricValues should include ${metric}`); + } + assert.deepEqual([...enums.ViewThresholdBasisValues], ['play_time', 'in_view']); + }); + it("matches the corresponding Zod schema's literals (Pacing)", async () => { if (!enums) enums = await import('../../dist/lib/types/enums.generated.js'); if (!schemas) schemas = await import('../../dist/lib/types/schemas.generated.js'); diff --git a/test/lib/get-signals-pre31-wholesale.test.js b/test/lib/get-signals-pre31-wholesale.test.js index 86bf01889..c0afb3a81 100644 --- a/test/lib/get-signals-pre31-wholesale.test.js +++ b/test/lib/get-signals-pre31-wholesale.test.js @@ -63,7 +63,7 @@ function makeModernClientTargeting30(protocol = 'mcp', config = {}, capabilityOv protocol, }, { - adcpVersion: '3.2.0-beta.5', + adcpVersion: '3.2.0-beta.6', validateFeatures: false, validation: { requests: 'off', responses: 'off' }, ...config, diff --git a/test/lib/mcp-modern-negotiation.test.js b/test/lib/mcp-modern-negotiation.test.js index fff5c55a3..0fbd5ae5a 100644 --- a/test/lib/mcp-modern-negotiation.test.js +++ b/test/lib/mcp-modern-negotiation.test.js @@ -586,7 +586,7 @@ test('modern serving honors the resolved AdCP MCP tool profile', async () => { createAdcpServer({ name: 'modern-profile-test', version: '1.0.0', - adcpVersion: '3.2.0-beta.5', + adcpVersion: '3.2.0-beta.6', ...(mcpToolProfile !== undefined && { mcpToolProfile }), stateStore: new InMemoryStateStore(), mediaBuy: { @@ -646,7 +646,7 @@ test('modern serving honors the resolved AdCP MCP tool profile', async () => { compactNames.every(name => MEDIA_BUY_MCP_TOOL_PROFILE.includes(name)), compactNames.join(', ') ); - assert.equal(compact._meta.adcp_version, '3.2.0-beta.5'); + assert.equal(compact._meta.adcp_version, '3.2.0-beta.6'); assert.equal(compact._meta.adcp_profile, 'media-buy'); assert.equal( compact.tools.find(tool => tool.name === 'list_products').description, @@ -690,7 +690,7 @@ test('modern serving honors the resolved AdCP MCP tool profile', async () => { 'https://json-schema.org/draft/2020-12/schema' ); assert.doesNotMatch(all.tools.find(tool => tool.name === 'request_proposals').inputSchema.$id, /\/profiles\//); - assert.equal(all._meta.adcp_version, '3.2.0-beta.5'); + assert.equal(all._meta.adcp_version, '3.2.0-beta.6'); assert.equal(all._meta.adcp_profile, 'all'); }); @@ -711,7 +711,7 @@ test('modern serving preserves explicitly registered custom schemas and descript }, async () => ({ content: [{ type: 'text', text: 'unused' }] }) ); - const adapter = createModernMcpServerAdapter(wrapMcpServer(legacy, undefined, '3.2.0-beta.5')); + const adapter = createModernMcpServerAdapter(wrapMcpServer(legacy, undefined, '3.2.0-beta.6')); const httpServer = createServer((req, res) => void adapter.handle(req, res)); const url = await listen(httpServer); const client = new Client( diff --git a/test/lib/media-buy-lifecycle-compatibility.test.js b/test/lib/media-buy-lifecycle-compatibility.test.js index bb490e659..d2294121b 100644 --- a/test/lib/media-buy-lifecycle-compatibility.test.js +++ b/test/lib/media-buy-lifecycle-compatibility.test.js @@ -22,7 +22,7 @@ async function withDualSurfaceSeller(serverAdcpVersion, buyerAdcpVersion, run, o end_time: '2027-02-01T00:00:00Z', confirmed_at: '2027-01-01T00:00:00Z', }; - const supportedVersions = ['3.0.25', '3.1.18', '3.2.0-beta.5'].filter(version => { + const supportedVersions = ['3.0.25', '3.1.18', '3.2.0-beta.6'].filter(version => { if (serverAdcpVersion.startsWith('3.0.')) return version.startsWith('3.0.'); if (serverAdcpVersion.startsWith('3.1.')) return !version.startsWith('3.2.'); return true; @@ -134,8 +134,8 @@ async function withDualSurfaceSeller(serverAdcpVersion, buyerAdcpVersion, run, o test('forced established diagnostics use actual MCP tool discovery on an all-tools 3.2 seller', async () => { await withDualSurfaceSeller( - '3.2.0-beta.5', - '3.2.0-beta.5', + '3.2.0-beta.6', + '3.2.0-beta.6', async ({ buyer, calls }) => { const lifecycle = await buyer.negotiateMediaBuyLifecycle({ preferredLifecycle: 'established', @@ -276,7 +276,7 @@ test('forced established diagnostics use actual MCP tool discovery on an all-too }); test('SDK buyer uses the compact lifecycle against a 3.2 seller profile', async () => { - await withDualSurfaceSeller('3.2.0-beta.5', '3.2.0-beta.5', async ({ buyer, mcpClient, calls }) => { + await withDualSurfaceSeller('3.2.0-beta.6', '3.2.0-beta.6', async ({ buyer, mcpClient, calls }) => { const listed = await mcpClient.listTools(); assert.ok(listed.tools.some(tool => tool.name === 'list_products')); assert.ok(!listed.tools.some(tool => tool.name === 'get_products')); @@ -284,15 +284,15 @@ test('SDK buyer uses the compact lifecycle against a 3.2 seller profile', async const result = await buyer.listProducts({ max_results: 10 }); assert.strictEqual(result.success, true, JSON.stringify(result)); assert.strictEqual(result.data.feed_version, 'feed-modern'); - assert.deepStrictEqual(calls, [['list_products', '3.2-beta.5', 3]]); + assert.deepStrictEqual(calls, [['list_products', '3.2-beta.6', 3]]); const lifecycle = await buyer.negotiateMediaBuyLifecycle(); const compatible = await lifecycle.listProducts({ max_results: 5 }); - assert.strictEqual(lifecycle.negotiated_version, '3.2-beta.5'); + assert.strictEqual(lifecycle.negotiated_version, '3.2-beta.6'); assert.strictEqual(compatible.compatibility.lifecycle, 'compact'); assert.deepStrictEqual(compatible.compatibility.tools_used, ['list_products']); assert.strictEqual(compatible.data.feed_version, 'feed-modern'); - assert.deepStrictEqual(calls.at(-1), ['list_products', '3.2-beta.5', 3]); + assert.deepStrictEqual(calls.at(-1), ['list_products', '3.2-beta.6', 3]); const rejected = await mcpClient.callTool({ name: 'request_proposals', @@ -317,7 +317,7 @@ test('SDK buyer uses the compact lifecycle against a 3.2 seller profile', async for (const adcpVersion of ['3.1.18', '3.0.25']) { test(`SDK buyer pinned to ${adcpVersion} can call a 3.2 seller's hidden legacy facade`, async () => { - await withDualSurfaceSeller('3.2.0-beta.5', adcpVersion, async ({ buyer, mcpClient, calls }) => { + await withDualSurfaceSeller('3.2.0-beta.6', adcpVersion, async ({ buyer, mcpClient, calls }) => { const listed = await mcpClient.listTools(); assert.ok(!listed.tools.some(tool => tool.name === 'get_products')); @@ -337,7 +337,7 @@ for (const adcpVersion of ['3.1.18', '3.0.25']) { }); test(`SDK buyer pinned to ${adcpVersion} preserves the full hidden legacy lifecycle on a normal 3.2 profile`, async () => { - await withDualSurfaceSeller('3.2.0-beta.5', adcpVersion, async ({ buyer, mcpClient, calls }) => { + await withDualSurfaceSeller('3.2.0-beta.6', adcpVersion, async ({ buyer, mcpClient, calls }) => { const listedTools = await mcpClient.listTools(); for (const hidden of ['get_products', 'create_media_buy', 'update_media_buy']) { assert.ok(!listedTools.tools.some(tool => tool.name === hidden), `${hidden} must stay hidden from tools/list`); diff --git a/test/lib/media-buy-lifecycle-coordinator.test.js b/test/lib/media-buy-lifecycle-coordinator.test.js index 003188aba..7a43d0640 100644 --- a/test/lib/media-buy-lifecycle-coordinator.test.js +++ b/test/lib/media-buy-lifecycle-coordinator.test.js @@ -30,7 +30,7 @@ const PRODUCTS_ONLY_BRIEF_VECTORS = JSON.parse( readFileSync( path.resolve( __dirname, - '../../compliance/cache/3.2.0-beta.5/test-vectors/products-only-brief-compatibility/vectors.json' + '../../compliance/cache/latest/test-vectors/products-only-brief-compatibility/vectors.json' ), 'utf8' ) @@ -43,7 +43,7 @@ const AGENT = { protocol: 'mcp', }; -function capabilities({ version = '3.2.0-beta.5', tools, discoveredTools, replayTtlSeconds = 3600 } = {}) { +function capabilities({ version = '3.2.0-beta.6', tools, discoveredTools, replayTtlSeconds = 3600 } = {}) { if (version === '2.5') { return { version: 'v2', @@ -3672,8 +3672,8 @@ describe('MediaBuyLifecycleCoordinator negotiation matrix', () => { test('does not select a newer prerelease than the compact buyer pin', async () => { const caps = capabilities({ tools: COMPACT_TOOLS }); - caps.supportedVersions = ['3.1', '3.2.0-beta.6']; - const agent = clientWithCaps(caps, '3.2.0-beta.5'); + caps.supportedVersions = ['3.1', '3.2.0-beta.7']; + const agent = clientWithCaps(caps, '3.2.0-beta.6'); const calls = []; agent.getProducts = async () => { calls.push('get_products'); @@ -3689,12 +3689,12 @@ describe('MediaBuyLifecycleCoordinator negotiation matrix', () => { test('fails closed when every valid advertised version is newer than the buyer pin', async () => { const caps = capabilities({ tools: COMPACT_TOOLS }); - caps.supportedVersions = ['3.2.0-beta.6']; - const agent = clientWithCaps(caps, '3.2.0-beta.5'); + caps.supportedVersions = ['3.2.0-beta.7']; + const agent = clientWithCaps(caps, '3.2.0-beta.6'); await assert.rejects( agent.negotiateMediaBuyLifecycle(), - /advertises only AdCP versions newer than the client pin 3\.2\.0-beta\.5/ + /advertises only AdCP versions newer than the client pin 3\.2\.0-beta\.6/ ); }); @@ -3702,7 +3702,7 @@ describe('MediaBuyLifecycleCoordinator negotiation matrix', () => { const caps = capabilities({ tools: COMPACT_TOOLS }); delete caps.supportedVersions; caps._synthetic = true; - const agent = clientWithCaps(caps, '3.2.0-beta.5'); + const agent = clientWithCaps(caps, '3.2.0-beta.6'); const calls = []; agent.listProducts = async () => { calls.push('list_products'); @@ -3713,7 +3713,7 @@ describe('MediaBuyLifecycleCoordinator negotiation matrix', () => { const coordinator = await agent.negotiateMediaBuyLifecycle(); await coordinator.listProducts({}); - assert.equal(coordinator.negotiated_version, '3.2.0-beta.5'); + assert.equal(coordinator.negotiated_version, '3.2.0-beta.6'); assert.deepEqual(calls, ['list_products']); }); @@ -3724,19 +3724,19 @@ describe('MediaBuyLifecycleCoordinator negotiation matrix', () => { await assert.rejects( agent.negotiateMediaBuyLifecycle(), - /served AdCP 3\.3, which is newer than the client pin 3\.2\.0-beta\.5/ + /served AdCP 3\.3, which is newer than the client pin 3\.2\.0-beta\.6/ ); }); test('fails closed when an exact newer prerelease is served despite an older advertised fallback', async () => { const caps = capabilities({ tools: COMPACT_TOOLS }); - caps.servedVersion = '3.2.0-beta.6'; - caps.supportedVersions = ['3.2.0-beta.5', '3.2.0-beta.6']; - const agent = clientWithCaps(caps, '3.2.0-beta.5'); + caps.servedVersion = '3.2.0-beta.7'; + caps.supportedVersions = ['3.2.0-beta.6', '3.2.0-beta.7']; + const agent = clientWithCaps(caps, '3.2.0-beta.6'); await assert.rejects( agent.negotiateMediaBuyLifecycle(), - /served AdCP 3\.2\.0-beta\.6, which is newer than the client pin 3\.2\.0-beta\.5/ + /served AdCP 3\.2\.0-beta\.7, which is newer than the client pin 3\.2\.0-beta\.6/ ); }); @@ -3755,8 +3755,8 @@ describe('MediaBuyLifecycleCoordinator negotiation matrix', () => { test('accepts an authoritative served release at or below the buyer pin', async () => { const caps = capabilities({ tools: COMPACT_TOOLS }); caps.servedVersion = '3.2.0-beta.2'; - caps.supportedVersions = ['3.2.0-beta.5']; - const agent = clientWithCaps(caps, '3.2.0-beta.5'); + caps.supportedVersions = ['3.2.0-beta.6']; + const agent = clientWithCaps(caps, '3.2.0-beta.6'); agent.listProducts = async () => completed('list_products', { products: [], feed_version: 'feed-1' }); const coordinator = await agent.negotiateMediaBuyLifecycle(); @@ -3770,7 +3770,7 @@ describe('MediaBuyLifecycleCoordinator negotiation matrix', () => { const caps = capabilities({ tools: COMPACT_TOOLS }); delete caps.supportedVersions; caps.buildVersion = '3.2.0-beta.5+sha.abc123'; - const agent = clientWithCaps(caps, '3.2.0-beta.5'); + const agent = clientWithCaps(caps, '3.2.0-beta.6'); agent.getProducts = async () => completed('get_products', { products: [] }); agent.listProducts = async () => assert.fail('build metadata must not enable compact wire tools'); @@ -6056,11 +6056,11 @@ describe('legacy products-only purchase continuations', () => { const native = clientWithCaps( capabilities({ - version: '3.2.0-beta.5', + version: '3.2.0-beta.6', tools: COMPACT_TOOLS, discoveredTools: ['get_products', ...COMPACT_TOOLS], }), - '3.2.0-beta.5' + '3.2.0-beta.6' ); native.getProducts = async () => completed('get_products', { products: [{ product_id: 'p-native', name: 'Native' }] }); @@ -6080,7 +6080,7 @@ describe('legacy products-only purchase continuations', () => { test('executes the signed account-fenced listed_purchase vector through native buy_products', async () => { const vector = PRODUCTS_ONLY_BRIEF_VECTORS.listed_purchase_cases[0]; - const agent = clientWithCaps(capabilities({ version: '3.2.0-beta.5', tools: COMPACT_TOOLS }), '3.2.0-beta.5'); + const agent = clientWithCaps(capabilities({ version: '3.2.0-beta.6', tools: COMPACT_TOOLS }), '3.2.0-beta.6'); const calls = []; agent.buyProducts = async request => { calls.push(request); @@ -10980,7 +10980,7 @@ describe('MediaBuyLifecycleCoordinator mutation boundaries', () => { }); test('readback fields are gated by the exact established schema version', async () => { - for (const version of ['3.0', '3.1', '3.2.0-beta.5']) { + for (const version of ['3.0', '3.1', '3.2.0-beta.6']) { const tools = version.startsWith('3.2') ? [...COMPACT_TOOLS, 'get_media_buys', 'get_media_buy_delivery'] : undefined; @@ -11052,6 +11052,118 @@ describe('MediaBuyLifecycleCoordinator mutation boundaries', () => { } }); + test('beta.6 delivery metric and sorting requests fail closed for older sellers', async () => { + for (const version of ['3.2.0-beta.5', '3.2.0-beta.6']) { + const agent = clientWithCaps(capabilities({ version, tools: [...COMPACT_TOOLS, 'get_media_buy_delivery'] })); + let readbacks = 0; + agent.getMediaBuyDelivery = async () => { + readbacks += 1; + return completed('get_media_buy_delivery', { media_buy_deliveries: [] }); + }; + const coordinator = await agent.negotiateMediaBuyLifecycle(); + const requests = [ + [{ requested_metrics: ['viewable_rate'] }, 'requested_metrics'], + [ + { reporting_dimensions: { placement: { sort_by: 'viewable_rate' } } }, + 'reporting_dimensions.placement.sort_by', + ], + [ + { reporting_dimensions: { placement: { sort_direction: 'asc' } } }, + 'reporting_dimensions.placement.sort_direction', + ], + [{ reporting_dimensions: { format: {} } }, 'reporting_dimensions.format'], + ]; + + for (const [request, feature] of requests) { + if (version === '3.2.0-beta.5') { + await assert.rejects( + coordinator.getMediaBuyDelivery(request), + error => error instanceof MediaBuyLifecycleCompatibilityError && error.feature === feature + ); + } else { + await coordinator.getMediaBuyDelivery(request); + } + } + assert.equal(readbacks, version === '3.2.0-beta.5' ? 0 : requests.length); + } + }); + + test('beta.6 metric identities fail closed across compact beta.5 requests', async () => { + const agent = clientWithCaps(capabilities({ version: '3.2.0-beta.5', tools: COMPACT_TOOLS })); + let calls = 0; + for (const method of [ + 'listProducts', + 'requestProposals', + 'refineProposals', + 'buyProducts', + 'acceptProposal', + 'controlMediaBuy', + ]) { + agent[method] = async () => { + calls += 1; + return completed('unexpected', {}); + }; + } + const coordinator = await agent.negotiateMediaBuyLifecycle(); + const cases = [ + [ + () => + coordinator.listProducts({ + criteria: { offer_filters: { required_metrics: ['viewable_rate'] } }, + }), + 'criteria.offer_filters.required_metrics', + ], + [ + () => + coordinator.requestProposals({ + criteria: { offer_filters: { required_metrics: ['quartile_100'] } }, + }), + 'criteria.offer_filters.required_metrics', + ], + [ + () => + coordinator.refineProposals({ + refinements: [ + { + proposal_id: 'proposal-1', + criteria: { offer_filters: { required_metrics: ['time_based_views'] } }, + }, + ], + }), + 'refinements[0].criteria.offer_filters.required_metrics', + ], + [ + () => + coordinator.buyProducts({ + purchases: [ + { + committed_metrics: [ + { scope: 'standard', metric_id: 'measurable_impressions', committed_at: '2026-08-24T00:00:00Z' }, + ], + }, + ], + }), + 'purchases[0].committed_metrics[0].metric_id', + ], + [ + () => coordinator.acceptProposal({ reporting_webhook: { requested_metrics: ['viewed_seconds'] } }), + 'reporting_webhook.requested_metrics', + ], + [ + () => coordinator.controlMediaBuy({ reporting_webhook: { requested_metrics: ['quartile_25'] } }), + 'reporting_webhook.requested_metrics', + ], + ]; + + for (const [invoke, feature] of cases) { + await assert.rejects( + invoke(), + error => error instanceof MediaBuyLifecycleCompatibilityError && error.feature === feature + ); + } + assert.equal(calls, 0); + }); + test('product field selection is gated by the exact established enum', async () => { for (const [version, field] of [ ['3.0', 'format_options'], @@ -13622,7 +13734,7 @@ describe('MediaBuyLifecycleCoordinator mutation boundaries', () => { }); test('rejects media-buy cancellation combined with name on compact and established lifecycles', async () => { - for (const { version, tools } of [{ version: '3.0' }, { version: '3.2.0-beta.5', tools: COMPACT_TOOLS }]) { + for (const { version, tools } of [{ version: '3.0' }, { version: '3.2.0-beta.6', tools: COMPACT_TOOLS }]) { const agent = clientWithCaps(capabilities({ version, tools })); let mutations = 0; agent.updateMediaBuy = async () => { diff --git a/test/lib/media-buy-lifecycle-release-gate.test.js b/test/lib/media-buy-lifecycle-release-gate.test.js index b369284fd..30f944f51 100644 --- a/test/lib/media-buy-lifecycle-release-gate.test.js +++ b/test/lib/media-buy-lifecycle-release-gate.test.js @@ -109,7 +109,7 @@ async function withHonestEstablishedSeller(version, run) { idempotency: { supported: true, replay_ttl_seconds: 86400 }, ...(version.startsWith('3.1') && { supported_versions: ['3.0', '3.1'], build_version: version }), ...(version.startsWith('3.2') && { - supported_versions: ['3.0', '3.1', '3.2-beta.5'], + supported_versions: ['3.0', '3.1', version.replace('.0-', '-')], build_version: version, }), }, @@ -197,7 +197,7 @@ async function withHonestEstablishedSeller(version, run) { const mcp = new Client({ name: `compact-buyer-for-${version}`, version: '1.0.0' }); await Promise.all([mcp.connect(clientTransport), server.connect(serverTransport)]); const buyer = AgentClient.fromMCPClient(mcp, { - adcpVersion: '3.2.0-beta.5', + adcpVersion: '3.2.0-beta.6', validation: { requests: 'strict', responses: 'strict' }, }); try { @@ -274,7 +274,7 @@ async function withHonestV25Seller(run) { const mcp = new Client({ name: 'compact-buyer-for-v2.5', version: '1.0.0' }); await Promise.all([mcp.connect(clientTransport), server.connect(serverTransport)]); const buyer = AgentClient.fromMCPClient(mcp, { - adcpVersion: '3.2.0-beta.5', + adcpVersion: '3.2.0-beta.6', allowV2: true, validation: { requests: 'strict', responses: 'strict' }, }); @@ -326,7 +326,7 @@ async function withHonestEstablishedProposalState(state, run) { const mcp = new Client({ name: `proposal-${state}-buyer`, version: '1.0.0' }); await Promise.all([mcp.connect(clientTransport), server.connect(serverTransport)]); const buyer = AgentClient.fromMCPClient(mcp, { - adcpVersion: '3.2.0-beta.5', + adcpVersion: '3.2.0-beta.6', validation: { requests: 'strict', responses: 'strict' }, }); try { @@ -434,7 +434,7 @@ for (const state of ['submitted', 'input-required']) { }); } -for (const version of ['3.0.25', '3.1.18', '3.2.0-beta.5']) { +for (const version of ['3.0.25', '3.1.18', '3.2.0-beta.6']) { test(`3.2 compact facade preserves the complete ${version} direct lifecycle over honest MCP wire`, async () => { await withHonestEstablishedSeller(version, async ({ buyer, calls, mutations }) => { const lifecycle = await buyer.negotiateMediaBuyLifecycle({ @@ -446,7 +446,7 @@ for (const version of ['3.0.25', '3.1.18', '3.2.0-beta.5']) { assert.equal(lifecycle.lifecycle, 'established'); assert.equal( lifecycle.negotiated_version, - version.startsWith('3.2') ? '3.2-beta.5' : version.startsWith('3.1') ? '3.1' : '3.0' + version.startsWith('3.2') ? '3.2-beta.6' : version.startsWith('3.1') ? '3.1' : '3.0' ); assert.equal(listed.data.feed_version, 'legacy-feed-1'); assert.equal(listed.data.pricing_version, 'legacy-price-1'); @@ -816,7 +816,7 @@ test('the same compact-first buyer facade projects established direct and propos try { const buyer = new AgentClient( { id: 'a2a-release-gate', name: 'A2A release gate', agent_uri: url, protocol: 'a2a' }, - { adcpVersion: '3.2.0-beta.5', validation: { requests: 'strict', responses: 'strict' } } + { adcpVersion: '3.2.0-beta.6', validation: { requests: 'strict', responses: 'strict' } } ); const lifecycle = await buyer.negotiateMediaBuyLifecycle({ principalScope: 'release-gate-buyer', @@ -1067,8 +1067,8 @@ test('the compact-first buyer uses the native 3.2 lifecycle discovered over offi const adcp = createAdcpServer({ name: 'a2a-compact-release-gate', version: '1.0.0', - adcpVersion: '3.2.0-beta.5', - capabilities: { supported_versions: ['3.0', '3.1', '3.2-beta.5'] }, + adcpVersion: '3.2.0-beta.6', + capabilities: { supported_versions: ['3.0', '3.1', '3.2-beta.6'] }, validation: { requests: 'strict', responses: 'strict' }, mediaBuy: { listProducts: async params => { @@ -1187,13 +1187,13 @@ test('the compact-first buyer uses the native 3.2 lifecycle discovered over offi try { const buyer = new AgentClient( { id: 'a2a-compact-release-gate', name: 'A2A compact release gate', agent_uri: url, protocol: 'a2a' }, - { adcpVersion: '3.2.0-beta.5', validation: { requests: 'strict', responses: 'strict' } } + { adcpVersion: '3.2.0-beta.6', validation: { requests: 'strict', responses: 'strict' } } ); const capabilities = await buyer.getAdcpCapabilities({}); assert.equal(capabilities.success, true, JSON.stringify(capabilities)); const lifecycle = await buyer.negotiateMediaBuyLifecycle({ principalScope: 'release-gate-buyer' }); assert.equal(lifecycle.lifecycle, 'compact'); - assert.equal(lifecycle.negotiated_version, '3.2-beta.5'); + assert.equal(lifecycle.negotiated_version, '3.2-beta.6'); const listed = await lifecycle.listProducts({ account: ACCOUNT, brand: BRAND }); assert.equal(listed.success, true, JSON.stringify(listed)); diff --git a/test/lib/proposal-negotiation-server.test.js b/test/lib/proposal-negotiation-server.test.js index 77ca8eb51..ee89d1933 100644 --- a/test/lib/proposal-negotiation-server.test.js +++ b/test/lib/proposal-negotiation-server.test.js @@ -100,7 +100,7 @@ describe('refine_proposals server integration', () => { test('modern platform seam auto-pins to an advertised 3.2 release', async () => { const server = createAdcpServerFromPlatform( { - capabilities: { specialisms: [], supported_versions: ['3.2-beta.5'] }, + capabilities: { specialisms: [], supported_versions: ['3.2-beta.6'] }, accounts: { resolution: 'derived', resolve: async () => ({ id: 'account-1', metadata: {} }), @@ -133,8 +133,8 @@ describe('refine_proposals server integration', () => { const listed = await server.dispatchTestRequest({ method: 'tools/list' }); assert.ok(listed.tools.some(tool => tool.name === 'refine_proposals')); const capabilities = await call(server, 'get_adcp_capabilities', {}); - assert.equal(capabilities.structuredContent.adcp_version, '3.2-beta.5'); - assert.deepEqual(capabilities.structuredContent.adcp.supported_versions, ['3.2-beta.5']); + assert.equal(capabilities.structuredContent.adcp_version, '3.2-beta.6'); + assert.deepEqual(capabilities.structuredContent.adcp.supported_versions, ['3.2-beta.6']); assert.deepEqual(capabilities.structuredContent.media_buy.proposal_refinement, { supported_dimensions: [], }); @@ -150,11 +150,11 @@ describe('refine_proposals server integration', () => { supported_dimensions: ['total_budget', 'alternatives'], max_alternatives: 4, }); - assert.equal(capabilities.structuredContent.adcp_version, '3.2-beta.5'); + assert.equal(capabilities.structuredContent.adcp_version, '3.2-beta.6'); assert.ok(capabilities.structuredContent.media_buy.lifecycle_tools.includes('refine_proposals')); const response = await call(server, 'refine_proposals', request('version-envelope-key-0001'), 'buyer-a'); - assert.equal(response.structuredContent.adcp_version, '3.2-beta.5'); + assert.equal(response.structuredContent.adcp_version, '3.2-beta.6'); }); test('rejects an explicit pre-3.2 server pin', () => { @@ -170,7 +170,7 @@ describe('refine_proposals server integration', () => { test('hides proposal refinement capabilities from a negotiated pre-3.2 response', async () => { const server = negotiationServer(undefined, { - serverCapabilities: { supported_versions: ['3.1', '3.2-beta.5'] }, + serverCapabilities: { supported_versions: ['3.1', '3.2-beta.6'] }, }); const legacy = await call(server, 'get_adcp_capabilities', { @@ -182,10 +182,10 @@ describe('refine_proposals server integration', () => { assert.equal(legacy.structuredContent.media_buy.lifecycle_tools, undefined); const modern = await call(server, 'get_adcp_capabilities', { - adcp_version: '3.2-beta.5', + adcp_version: '3.2-beta.6', adcp_major_version: 3, }); - assert.equal(modern.structuredContent.adcp_version, '3.2-beta.5'); + assert.equal(modern.structuredContent.adcp_version, '3.2-beta.6'); assert.deepEqual(modern.structuredContent.media_buy.proposal_refinement, { supported_dimensions: ['total_budget', 'alternatives'], max_alternatives: 4, @@ -311,7 +311,7 @@ describe('refine_proposals server integration', () => { assert.equal(response.isError, undefined); assert.equal(response.structuredContent.status, 'submitted'); assert.equal(response.structuredContent.task_id, 'proposal-task-1'); - assert.equal(response.structuredContent.adcp_version, '3.2-beta.5'); + assert.equal(response.structuredContent.adcp_version, '3.2-beta.6'); assert.equal(response.structuredContent.results, undefined); assert.equal(response.structuredContent.products, undefined); }); diff --git a/test/lib/proposal-negotiation-validation.test.js b/test/lib/proposal-negotiation-validation.test.js index 55150e4d2..976983c63 100644 --- a/test/lib/proposal-negotiation-validation.test.js +++ b/test/lib/proposal-negotiation-validation.test.js @@ -122,7 +122,7 @@ test('builder pins the 3.2 wire envelope and returns an immutable deep snapshot input.context.planning.attempt = 2; input.refinements[0].ask = 'Changed after construction'; - assert.equal(built.adcp_version, '3.2-beta.5'); + assert.equal(built.adcp_version, '3.2-beta.6'); assert.equal(built.adcp_major_version, 3); assert.equal(built.context.planning.attempt, 1); assert.equal(built.refinements[0].ask, 'Improve the terms'); diff --git a/test/lib/proposal-negotiation.test.js b/test/lib/proposal-negotiation.test.js index 3853e3ea9..b74c0ecb0 100644 --- a/test/lib/proposal-negotiation.test.js +++ b/test/lib/proposal-negotiation.test.js @@ -339,7 +339,7 @@ test('AgentClient.refineProposals dispatches through the official MCP client wit const mcpClient = new Client({ name: 'Buyer', version: '1.0.0' }); await mcpClient.connect(clientTransport); const agent = AgentClient.fromMCPClient(mcpClient, { - adcpVersion: '3.2.0-beta.5', + adcpVersion: '3.2.0-beta.6', wireAdcpVersion: '3.2.0-beta.1', }); @@ -357,7 +357,7 @@ test('AgentClient.refineProposals dispatches through the official MCP client wit test('ADCPMultiAgentClient.simple forwards an exact prerelease wire pin to proposal requests', async () => { const { ADCPMultiAgentClient } = require('../../dist/lib/index.js'); const client = ADCPMultiAgentClient.simple('https://seller.example.com/mcp', { - adcpVersion: '3.2.0-beta.5', + adcpVersion: '3.2.0-beta.6', wireAdcpVersion: '3.2.0-beta.1', }); const agent = client.agent('default-agent'); @@ -381,7 +381,7 @@ test('ADCPMultiAgentClient.simple forwards an exact prerelease wire pin to propo }; await agent.refineProposals({ refinements: [revise()] }); - assert.equal(client.getAdcpVersion(), '3.2.0-beta.5'); + assert.equal(client.getAdcpVersion(), '3.2.0-beta.6'); assert.equal(captured.adcp_version, '3.2-beta.1'); assert.equal(captured.adcp_major_version, 3); }); diff --git a/test/lib/request-validation.test.js b/test/lib/request-validation.test.js index 6ff61c564..b11c01d27 100644 --- a/test/lib/request-validation.test.js +++ b/test/lib/request-validation.test.js @@ -302,6 +302,7 @@ describe('SingleAgentClient Request Validation', () => { inner.cachedCapabilities = { version: 'v3', majorVersions: [3], + supportedVersions: ['3.2.0-beta.6'], protocols: ['media_buy'], features: { inlineCreativeManagement: false, @@ -1023,6 +1024,23 @@ describe('v3 partial-schema field stripping', () => { preservedValue: ['mb_123', 'mb_456'], mockResponse: { deliveries: [] }, }, + { + name: 'get_media_buy_delivery requested_metrics', + toolName: 'get_media_buy_delivery', + invoke: agent => agent.getMediaBuyDelivery.bind(agent), + partialToolSchema: { status_filter: {} }, + request: { + requested_metrics: ['viewable_rate', 'quartile_100', 'time_based_views'], + reporting_dimensions: { format: { sort_by: 'viewable_rate', sort_direction: 'asc' } }, + totally_made_up_field: 'junk', + }, + preservedField: 'requested_metrics', + preservedValue: ['viewable_rate', 'quartile_100', 'time_based_views'], + additionalPreserved: { + reporting_dimensions: { format: { sort_by: 'viewable_rate', sort_direction: 'asc' } }, + }, + mockResponse: { deliveries: [] }, + }, { name: 'sync_creatives', toolName: 'sync_creatives', @@ -1076,6 +1094,7 @@ describe('v3 partial-schema field stripping', () => { inner.cachedCapabilities = { version: 'v3', majorVersions: [3], + supportedVersions: ['3.2.0-beta.6'], protocols: ['media_buy'], features: { inlineCreativeManagement: false, @@ -1116,6 +1135,13 @@ describe('v3 partial-schema field stripping', () => { tc.preservedValue, `canonical ${tc.preservedField} must be preserved even though the agent under-declares it` ); + for (const [field, value] of Object.entries(tc.additionalPreserved ?? {})) { + assert.deepStrictEqual( + call.args[field], + value, + `canonical ${field} must be preserved even though the agent under-declares it` + ); + } assert.strictEqual( call.args.totally_made_up_field, undefined, diff --git a/test/lib/schema-validation.test.js b/test/lib/schema-validation.test.js index 295468b97..1c30cb921 100644 --- a/test/lib/schema-validation.test.js +++ b/test/lib/schema-validation.test.js @@ -234,10 +234,10 @@ describe('schema-driven validation', () => { test('accepts SDK-stamped adcp_version through nested compact commitment aliases', () => { for (const tool of ['buy_products', 'accept_proposal']) { for (const response of [ - compactCommitmentResponse({ adcp_version: '3.2-beta.5' }), - failedCompactCommitmentResponse({ adcp_version: '3.2-beta.5' }), + compactCommitmentResponse({ adcp_version: '3.2-beta.6' }), + failedCompactCommitmentResponse({ adcp_version: '3.2-beta.6' }), ]) { - const outcome = validateResponse(tool, response, '3.2.0-beta.5'); + const outcome = validateResponse(tool, response, '3.2.0-beta.6'); assert.strictEqual(outcome.valid, true, `${tool} ${response.status}: ${formatIssues(outcome.issues)}`); } } @@ -246,7 +246,7 @@ describe('schema-driven validation', () => { test('compact commitment relaxation does not make nested bodies permissive', () => { const response = compactCommitmentResponse(); response.accepted_proposal.commercial_terms.unknown_nested_field = 'typo'; - const outcome = validateResponse('buy_products', response, '3.2.0-beta.5'); + const outcome = validateResponse('buy_products', response, '3.2.0-beta.6'); assert.ok( outcome.issues.some( issue => diff --git a/test/lib/task-executor-operation-error.test.js b/test/lib/task-executor-operation-error.test.js index ab165bb08..da03ac600 100644 --- a/test/lib/task-executor-operation-error.test.js +++ b/test/lib/task-executor-operation-error.test.js @@ -15,7 +15,7 @@ describe('TaskExecutor business rejection diagnostics', () => { suggestions: ['Try broadening the requested geography'], }, }); - const executor = new TaskExecutor({ strictSchemaValidation: false, adcpVersion: '3.2.0-beta.5' }); + const executor = new TaskExecutor({ strictSchemaValidation: false, adcpVersion: '3.2.0-beta.6' }); try { const result = await executor.executeTask( { id: 'seller', name: 'Seller', agent_uri: 'https://seller.example/mcp', protocol: 'mcp' }, diff --git a/test/lib/version-sync.test.js b/test/lib/version-sync.test.js index c1611de4c..4f3cae622 100644 --- a/test/lib/version-sync.test.js +++ b/test/lib/version-sync.test.js @@ -25,11 +25,11 @@ test('AdCP semver pins normalize to release-precision wire values', () => { }); test('3.2 beta remains exact while retaining the complete supported 3.0 and 3.1 GA lines', () => { - assert.equal(ADCP_VERSION, '3.2.0-beta.5'); + assert.equal(ADCP_VERSION, '3.2.0-beta.6'); assert.ok(COMPATIBLE_ADCP_VERSIONS.includes('3.0.25')); assert.ok(COMPATIBLE_ADCP_VERSIONS.includes('3.1.18')); - assert.ok(COMPATIBLE_ADCP_VERSIONS.includes('3.2.0-beta.5')); - assert.ok(COMPATIBLE_ADCP_VERSIONS.includes('3.2-beta.5')); + assert.ok(COMPATIBLE_ADCP_VERSIONS.includes('3.2.0-beta.6')); + assert.ok(COMPATIBLE_ADCP_VERSIONS.includes('3.2-beta.6')); assert.ok(!COMPATIBLE_ADCP_VERSIONS.includes('3.2.0-beta.1')); assert.ok(!COMPATIBLE_ADCP_VERSIONS.includes('3.2-beta.1')); assert.ok(!COMPATIBLE_ADCP_VERSIONS.includes('3.2.0-beta.0')); diff --git a/test/lib/zod-schemas.test.js b/test/lib/zod-schemas.test.js index c5d5da228..1f7d882d2 100644 --- a/test/lib/zod-schemas.test.js +++ b/test/lib/zod-schemas.test.js @@ -132,7 +132,7 @@ describe('Zod Schema Validation', () => { readFileSync( path.resolve( __dirname, - '../../compliance/cache/3.2.0-beta.5/test-vectors/products-only-brief-compatibility/vectors.json' + '../../compliance/cache/latest/test-vectors/products-only-brief-compatibility/vectors.json' ), 'utf8' ) @@ -160,7 +160,7 @@ describe('Zod Schema Validation', () => { readFileSync( path.resolve( __dirname, - '../../compliance/cache/3.2.0-beta.5/test-vectors/products-only-brief-compatibility/vectors.json' + '../../compliance/cache/latest/test-vectors/products-only-brief-compatibility/vectors.json' ), 'utf8' ) @@ -435,6 +435,8 @@ describe('Zod Schema Validation', () => { assert.equal(typeof schemas.ProductSchema.omit, 'function', 'ProductSchema should support omit'); assert.equal(typeof schemas.ProductSchema.pick, 'function', 'ProductSchema should support pick'); assert.ok(schemas.CanonicalFormatImageSchema.shape.image_formats, 'canonical formats should expose object shape'); + assert.equal(schemas.CanonicalFormatImageSchema.safeParse({ motion_level: 'limited_motion' }).success, true); + assert.equal(schemas.CanonicalFormatImageSchema.safeParse({ motion_level: 'full_motion' }).success, false); }); test('PriceBreakdownSchema preserves adjustment XOR and 1..20 bounds', async () => { @@ -555,6 +557,344 @@ describe('Zod Schema Validation', () => { ); }); + test('beta.6 reporting request and delivery identities round-trip', async () => { + if (!schemas) { + schemas = await import('../../dist/lib/types/schemas.generated.js'); + } + + const request = { + requested_metrics: ['viewable_rate', 'quartile_100', 'time_based_views'], + reporting_dimensions: { + creative: { limit: 5, sort_by: 'quartile_100', sort_direction: 'asc' }, + keyword: { sort_by: 'viewable_rate', sort_direction: 'desc' }, + catalog_item: {}, + format: { limit: 3, sort_by: 'cpm', sort_direction: 'desc' }, + }, + }; + assert.ok( + schemas.GetMediaBuyDeliveryRequestSchema.safeParse(request).success, + 'requested metric leaves and negotiated breakdown controls should validate' + ); + assert.ok( + !schemas.GetMediaBuyDeliveryRequestSchema.safeParse({ requested_metrics: [] }).success, + 'requested metrics must not be empty' + ); + assert.ok( + !schemas.GetMediaBuyDeliveryRequestSchema.safeParse({ requested_metrics: ['clicks', 'clicks'] }).success, + 'requested metrics must be unique' + ); + + const deliveryMetrics = { + impressions: 100, + spend: 25, + time_based_views: [ + { threshold_seconds: 2, basis: 'play_time', views: 80 }, + { threshold_seconds: 2, basis: 'in_view', views: 60, standard: 'mrc' }, + ], + vendor_metric_values: [ + { + vendor: { domain: 'measurement.example' }, + metric_id: 'incremental_outcomes', + value: 12, + qualifier: { attribution_window: { interval: 7, unit: 'days' } }, + }, + { + vendor: { domain: 'measurement.example' }, + metric_id: 'incremental_outcomes', + value: 18, + qualifier: { attribution_window: { interval: 30, unit: 'days' } }, + }, + ], + }; + assert.ok( + schemas.DeliveryMetricsSchema.safeParse(deliveryMetrics).success, + 'time-based views and qualifier-distinct vendor rows should validate' + ); + assert.ok( + !schemas.DeliveryMetricsSchema.safeParse({ + time_based_views: [{ threshold_seconds: 0, basis: 'play_time', views: 1 }], + }).success, + 'time-based view thresholds must be positive' + ); + assert.ok( + !schemas.VendorMetricValueSchema.safeParse({ + vendor: { domain: 'measurement.example' }, + metric_id: 'incremental_outcomes', + value: 1, + qualifier: { unrecognized_identity: 'poison' }, + }).success, + 'vendor metric identity qualifiers must reject unknown keys' + ); + + const aggregateRows = [ + { + scope: 'standard', + metric_id: 'viewable_rate', + value: 0.75, + measurable_impressions: 80, + viewable_impressions: 60, + qualifier: { viewability_standard: 'mrc' }, + }, + { + scope: 'standard', + metric_id: 'completion_rate', + value: 0.5, + impressions: 100, + completed_views: 50, + }, + { scope: 'standard', metric_id: 'cost_per_acquisition', value: 4, spend: 20, conversions: 5 }, + { scope: 'standard', metric_id: 'roas', value: 3, spend: 20, conversion_value: 60 }, + ]; + for (const aggregate of aggregateRows) { + assert.ok(schemas.DeliveryMetricAggregateSchema.safeParse(aggregate).success); + for (const requiredComponent of Object.keys(aggregate).filter( + key => !['scope', 'metric_id', 'value', 'qualifier'].includes(key) + )) { + const incomplete = { ...aggregate }; + delete incomplete[requiredComponent]; + assert.ok( + !schemas.DeliveryMetricAggregateSchema.safeParse(incomplete).success, + `${aggregate.metric_id} must require ${requiredComponent}` + ); + } + } + assert.ok( + !schemas.DeliveryMetricAggregateSchema.safeParse({ + scope: 'standard', + metric_id: 'clicks', + value: 1, + qualifier: { unrecognized_identity: 'poison' }, + }).success, + 'standard delivery aggregate qualifiers must reject unknown identity keys' + ); + assert.ok( + !schemas.DeliveryMetricAggregateSchema.safeParse({ + scope: 'vendor', + vendor: { domain: 'measurement.example' }, + metric_id: 'incremental_outcomes', + value: 1, + qualifier: { unrecognized_identity: 'poison' }, + }).success, + 'delivery aggregate qualifiers must reject unknown identity keys' + ); + + const response = { + status: 'completed', + reporting_period: { start: '2026-08-01T00:00:00Z', end: '2026-08-02T00:00:00Z' }, + currency: 'USD', + media_buy_deliveries: [ + { + media_buy_id: 'buy-1', + status: 'active', + totals: deliveryMetrics, + by_package: [ + { + package_id: 'package-1', + impressions: 100, + spend: 25, + by_format: [{ format_kind: 'video_vast', impressions: 100, spend: 25 }], + by_format_truncated: false, + by_format_sorted_by: 'cpm', + by_format_sort_direction: 'desc', + }, + ], + }, + ], + }; + assert.ok( + schemas.GetMediaBuyDeliveryResponseSchema.safeParse(response).success, + 'format breakdown completeness and applied-sort echoes should validate' + ); + }); + + test('beta.6 promoted canonical formats preserve nested wire constraints', async () => { + if (!schemas) { + schemas = await import('../../dist/lib/types/schemas.generated.js'); + } + + for (const format_kind of ['seller_rendered_stateful_display', 'coordinated_placements']) { + const option = { format_kind, params: {} }; + assert.ok( + schemas.CanonicalFormatOptionSchema.safeParse(option).success, + `${format_kind} must be accepted by compact format options` + ); + assert.ok( + schemas.CanonicalProductSchema.safeParse({ + product_id: `product-${format_kind}`, + name: format_kind, + format_options: [option], + }).success, + `${format_kind} must be accepted in canonical products` + ); + } + + const stateful = { + states: [ + { + state_id: 'default', + anchoring: 'inline', + breakpoints: [{ breakpoint_id: 'desktop', width: 300, height: 250 }], + close_affordance: false, + }, + ], + initial_state_id: 'default', + user_controls: { dismissible: false, user_collapsible: false }, + }; + assert.ok(schemas.CanonicalFormatSellerRenderedStatefulDisplaySchema.shape); + assert.ok(schemas.CanonicalFormatCoordinatedPlacementsSchema.shape); + assert.ok(schemas.CanonicalFormatSellerRenderedStatefulDisplaySchema.safeParse(stateful).success); + for (const invalid of [ + { ...stateful, experimental: 'wrong' }, + { ...stateful, v1_translatable: 42 }, + { ...stateful, composition_model: { bad: true } }, + { ...stateful, slots: { not: 'an array' } }, + { + ...stateful, + states: [ + { + ...stateful.states[0], + breakpoints: [{ breakpoint_id: 'desktop', width: 300, width_range: [250, 350], height: 250 }], + }, + ], + }, + { + ...stateful, + states: [ + { + ...stateful.states[0], + breakpoints: [{ breakpoint_id: 'desktop', width: 300, height: 250, height_range: [200, 300] }], + }, + ], + }, + { + ...stateful, + states: [{ ...stateful.states[0], breakpoints: [{ breakpoint_id: 'desktop', width: 300 }] }], + }, + { ...stateful, duration_ms_range: [null, null] }, + { ...stateful, duration_ms_range: [0, 100, 200] }, + { ...stateful, containers: ['mp4', 'mp4'] }, + ]) { + assert.ok( + !schemas.CanonicalFormatSellerRenderedStatefulDisplaySchema.safeParse(invalid).success, + `stateful-display must reject ${JSON.stringify(invalid)}` + ); + } + assert.ok( + !schemas.CanonicalFormatSellerRenderedStatefulDisplaySchema.safeParse({ + ...stateful, + states: [{ ...stateful.states[0], breakpoints: [{}] }], + }).success, + 'stateful-display breakpoints must retain required identity and geometry' + ); + assert.ok( + !schemas.CanonicalFormatSellerRenderedStatefulDisplaySchema.safeParse({ + ...stateful, + transitions: [{ trigger: 'timer' }], + }).success, + 'stateful-display transitions must retain common and arm-specific required fields' + ); + const transitionBase = { + transition_id: 'expand', + from_state_id: 'default', + to_state_id: 'expanded', + trigger: 'timer', + transition_mode: 'animated', + delay_ms: 250, + }; + assert.ok( + schemas.CanonicalFormatSellerRenderedStatefulDisplaySchema.safeParse({ + ...stateful, + transitions: [transitionBase], + }).success + ); + assert.ok( + !schemas.CanonicalFormatSellerRenderedStatefulDisplaySchema.safeParse({ + ...stateful, + transitions: [{ ...transitionBase, input: 'tap' }], + }).success, + 'timer transitions reject user input' + ); + assert.ok( + !schemas.CanonicalFormatSellerRenderedStatefulDisplaySchema.safeParse({ + ...stateful, + transitions: [ + { + ...transitionBase, + trigger: 'scroll_progress', + input: 'scroll', + scroll_reference: 'page', + scroll_start_percent: 0, + scroll_end_percent: 100, + }, + ], + }).success, + 'scroll-progress transitions require scroll-linked mode and forbid timer fields' + ); + + const coordinated = { + components: [ + { + component_id: 'hero', + placement_ref: { placement_id: 'hero-placement' }, + required: true, + format_kind: 'image', + params: {}, + }, + { + component_id: 'rail', + placement_ref: { placement_id: 'rail-placement' }, + required: false, + format_option_ref: { scope: 'product', format_option_id: 'rail-option' }, + }, + ], + }; + assert.ok(schemas.CanonicalFormatCoordinatedPlacementsSchema.safeParse(coordinated).success); + assert.ok( + !schemas.CanonicalFormatCoordinatedPlacementsSchema.safeParse({ components: [{}, {}] }).success, + 'coordinated components must retain required identity, placement, and format selection' + ); + assert.ok( + !schemas.CanonicalFormatCoordinatedPlacementsSchema.safeParse({ + components: coordinated.components.map(component => ({ ...component, required: false })), + }).success, + 'at least one coordinated component must be required' + ); + assert.ok( + !schemas.CanonicalFormatCoordinatedPlacementsSchema.safeParse({ + components: [ + { + ...coordinated.components[0], + format_option_ref: { scope: 'product', format_option_id: 'also-a-reference' }, + }, + coordinated.components[1], + ], + }).success, + 'inline and referenced component formats are mutually exclusive' + ); + for (const firstComponent of [ + { ...coordinated.components[0], params: undefined }, + { ...coordinated.components[0], format_kind: 'custom' }, + { ...coordinated.components[0], format_kind: 'coordinated_placements' }, + { ...coordinated.components[0], params: { width: 0 } }, + ]) { + assert.ok( + !schemas.CanonicalFormatCoordinatedPlacementsSchema.safeParse({ + components: [firstComponent, coordinated.components[1]], + }).success, + `coordinated inline format must reject ${JSON.stringify(firstComponent)}` + ); + } + for (const consumed_by of [[], ['hero', 'hero']]) { + assert.ok( + !schemas.CanonicalFormatCoordinatedPlacementsSchema.safeParse({ + ...coordinated, + shared_slots: [{ asset_group_id: 'logo', asset_type: 'image', consumed_by }], + }).success, + `shared-slot consumers must reject ${JSON.stringify(consumed_by)}` + ); + } + }); + test('PostalCountrySystemSchema requires a valid country and system pair', async () => { if (!schemas) { schemas = await import('../../dist/lib/types/schemas.generated.js'); @@ -1996,7 +2336,7 @@ describe('Zod Schema Validation', () => { ['space-separator', '2027-01-02 00:00:00Z'], ['compact-offset', '2027-01-02T00:00:00+0100'], ]; - const exactValidator = getSchemaValidatorByRef('media-buy/refine-proposals-response.json', '3.2.0-beta.5'); + const exactValidator = getSchemaValidatorByRef('media-buy/refine-proposals-response.json'); assert.ok(exactValidator, 'exact refine_proposals response validator should be available'); const exactAccepts = payload => exactValidator(payload); const zodAccepts = payload => schemas.RefineProposalsResponseSchema.safeParse(payload).success; diff --git a/test/request-signing-agent-integration.test.js b/test/request-signing-agent-integration.test.js index 96d916be7..2381b1ea4 100644 --- a/test/request-signing-agent-integration.test.js +++ b/test/request-signing-agent-integration.test.js @@ -451,9 +451,10 @@ test('customHeaders signing-reserved keys are stripped before signing', async () /keyid="attacker"/, 'attacker-supplied Signature-Input was overwritten' ); - assert.match( + assert.match(call.headers['content-digest'], /^sha-256=:[A-Za-z0-9+/]{43}=:/); + assert.notStrictEqual( call.headers['content-digest'], - /sha-256=:[^A]/, + 'sha-256=:AAAA:', 'signer recomputed Content-Digest from the real body' ); assert.strictEqual(call.headers['x-benign-header'], 'yes', 'non-reserved customHeaders still pass through'); diff --git a/test/request-signing-runner-integration.test.js b/test/request-signing-runner-integration.test.js index af51f6fd5..3f48b94e5 100644 --- a/test/request-signing-runner-integration.test.js +++ b/test/request-signing-runner-integration.test.js @@ -126,7 +126,7 @@ function startReferenceVerifier({ replayCap = 1000 } = {}) { describe('request-signing: synthesize step expansion', () => { test('compliance loader synthesizes vectors from the selected frozen bundle', () => { const previousComplianceDir = process.env.ADCP_COMPLIANCE_DIR; - process.env.ADCP_COMPLIANCE_DIR = path.join('compliance', 'cache', '3.2.0-beta.5'); + process.env.ADCP_COMPLIANCE_DIR = path.join('compliance', 'cache', 'latest'); let storyboards; try { storyboards = loadBundleStoryboards({ diff --git a/test/server-create-adcp-server.test.js b/test/server-create-adcp-server.test.js index 8bee1907d..060b2dbdf 100644 --- a/test/server-create-adcp-server.test.js +++ b/test/server-create-adcp-server.test.js @@ -99,7 +99,7 @@ describe('createAdcpServer', () => { it('stays exactly aligned with the checked-in AdCP 3.2 media-buy manifest', () => { const manifest = JSON.parse( - readFileSync('schemas/cache/3.2.0-beta.5/mcp/2026-07-28/profiles/media-buy/manifest.json', 'utf8') + readFileSync('schemas/cache/latest/mcp/2026-07-28/profiles/media-buy/manifest.json', 'utf8') ); assert.deepStrictEqual([...MEDIA_BUY_MCP_TOOL_PROFILE], manifest.filters.include_tools); }); @@ -109,8 +109,8 @@ describe('createAdcpServer', () => { const server = createAdcpServer({ name: 'Profile seller', version: '1.0.0', - adcpVersion: '3.2.0-beta.5', - capabilities: { supported_versions: ['3.0.25', '3.1.18', '3.2.0-beta.5'] }, + adcpVersion: '3.2.0-beta.6', + capabilities: { supported_versions: ['3.0.25', '3.1.18', '3.2.0-beta.6'] }, mediaBuy: { ...compactHandlers, getProducts: async params => { @@ -147,14 +147,14 @@ describe('createAdcpServer', () => { assert.ok(!names.includes('update_media_buy'), 'deprecated update alias should not be advertised'); assert.ok(!names.includes('build_creative'), 'creative-builder tools are outside the media-buy role profile'); assert.deepStrictEqual(listed._meta, { - adcp_version: '3.2.0-beta.5', + adcp_version: '3.2.0-beta.6', adcp_profile: 'media-buy', }); - assert.strictEqual(listed.tools[0]._meta.adcp_version, '3.2.0-beta.5'); + assert.strictEqual(listed.tools[0]._meta.adcp_version, '3.2.0-beta.6'); const requestProposalsTool = listed.tools.find(tool => tool.name === 'request_proposals'); const officialRequestSchema = JSON.parse( readFileSync( - 'schemas/cache/3.2.0-beta.5/mcp/2026-07-28/profiles/media-buy/media-buy/request-proposals-request.json', + 'schemas/cache/latest/mcp/2026-07-28/profiles/media-buy/media-buy/request-proposals-request.json', 'utf8' ) ); @@ -203,7 +203,7 @@ describe('createAdcpServer', () => { const server = createAdcpServer({ name: 'Migration seller', version: '1.0.0', - adcpVersion: '3.2.0-beta.5', + adcpVersion: '3.2.0-beta.6', mcpToolProfile: 'all', mediaBuy: { ...compactHandlers, @@ -859,7 +859,7 @@ describe('createAdcpServer', () => { acceptProposal: async () => ({}), }, }); - const modern = await callTool(server, 'get_adcp_capabilities', { adcp_version: '3.2-beta.5' }); + const modern = await callTool(server, 'get_adcp_capabilities', { adcp_version: '3.2-beta.6' }); assert.deepStrictEqual(modern.media_buy.lifecycle_tools, [ 'list_products', 'request_proposals', @@ -2267,7 +2267,7 @@ describe('createAdcpServer', () => { assert.strictEqual(status.task_type, 'sync_creatives'); assert.strictEqual(status.protocol, 'creative'); assert.strictEqual(status.has_webhook, true); - assert.strictEqual(status.adcp_version, '3.2-beta.5'); + assert.strictEqual(status.adcp_version, '3.2-beta.6'); assert.deepStrictEqual(status.result, { creatives: [{ creative_id: 'cr_1' }] }); assert.deepStrictEqual(status.context, { trace_id: 'trace_1' }); @@ -2312,7 +2312,7 @@ describe('createAdcpServer', () => { assert.strictEqual(listed.tasks[0].task_type, 'sync_creatives'); assert.strictEqual(listed.tasks[0].has_webhook, true); assert.strictEqual(listed.pagination.total_count, 1); - assert.strictEqual(listed.adcp_version, '3.2-beta.5'); + assert.strictEqual(listed.adcp_version, '3.2-beta.6'); const buyerTwoList = await callTool( server, @@ -2326,7 +2326,7 @@ describe('createAdcpServer', () => { const badCursor = await callToolRaw(server, 'list_tasks', { pagination: { cursor: 'not-a-number' } }, buyerOne); assert.strictEqual(badCursor.isError, true); assert.strictEqual(badCursor.structuredContent.adcp_error.code, 'INVALID_REQUEST'); - assert.strictEqual(badCursor.structuredContent.adcp_version, '3.2-beta.5'); + assert.strictEqual(badCursor.structuredContent.adcp_version, '3.2-beta.6'); const opaqueTaskId = 'opaque_' + 'x'.repeat(160); const opaque = await taskRegistry.create({ @@ -2754,12 +2754,12 @@ describe('createAdcpServer', () => { const status = await callToolRaw(server, 'get_task_status', { task_id: owned.taskId }, extra); assert.strictEqual(status.isError, true); assert.strictEqual(status.structuredContent.adcp_error.code, 'PERMISSION_DENIED'); - assert.strictEqual(status.structuredContent.adcp_version, '3.2-beta.5'); + assert.strictEqual(status.structuredContent.adcp_version, '3.2-beta.6'); const listed = await callToolRaw(server, 'list_tasks', {}, extra); assert.strictEqual(listed.isError, true); assert.strictEqual(listed.structuredContent.adcp_error.code, 'PERMISSION_DENIED'); - assert.strictEqual(listed.structuredContent.adcp_version, '3.2-beta.5'); + assert.strictEqual(listed.structuredContent.adcp_version, '3.2-beta.6'); const contextLeak = await callToolRaw( server, diff --git a/test/server-decisioning-from-platform.test.js b/test/server-decisioning-from-platform.test.js index f34915884..caf9b276a 100644 --- a/test/server-decisioning-from-platform.test.js +++ b/test/server-decisioning-from-platform.test.js @@ -23,10 +23,7 @@ const { createIdempotencyStore, memoryBackend } = require('../dist/lib/server/id const PRODUCTS_ONLY_BRIEF_VECTORS = JSON.parse( readFileSync( - path.resolve( - __dirname, - '../compliance/cache/3.2.0-beta.5/test-vectors/products-only-brief-compatibility/vectors.json' - ), + path.resolve(__dirname, '../compliance/cache/latest/test-vectors/products-only-brief-compatibility/vectors.json'), 'utf8' ) ); @@ -128,8 +125,8 @@ describe('createAdcpServerFromPlatform — v6.0 alpha', () => { const server = createAdcpServerFromPlatform(platform, { name: 'compact-and-legacy', version: '1.0.0', - adcpVersion: '3.2.0-beta.5', - capabilities: { supported_versions: ['3.0.25', '3.1.18', '3.2.0-beta.5'] }, + adcpVersion: '3.2.0-beta.6', + capabilities: { supported_versions: ['3.0.25', '3.1.18', '3.2.0-beta.6'] }, validation: { requests: 'off', responses: 'off' }, }); @@ -155,7 +152,7 @@ describe('createAdcpServerFromPlatform — v6.0 alpha', () => { method: 'tools/call', params: { name: 'list_products', - arguments: { adcp_version: '3.2.0-beta.5', account: { account_id: 'acc-modern' } }, + arguments: { adcp_version: '3.2.0-beta.6', account: { account_id: 'acc-modern' } }, }, }); assert.notStrictEqual(compact.isError, true, JSON.stringify(compact.structuredContent)); @@ -172,7 +169,7 @@ describe('createAdcpServerFromPlatform — v6.0 alpha', () => { assert.notStrictEqual(legacy.isError, true, JSON.stringify(legacy.structuredContent)); } assert.deepStrictEqual(calls, [ - ['list_products', '3.2.0-beta.5', 'acc-modern'], + ['list_products', '3.2.0-beta.6', 'acc-modern'], ['get_products', '3.1.18', 'acc-3.1.18'], ['get_products', '3.0.25', 'acc-3.0.25'], ]); @@ -219,8 +216,8 @@ describe('createAdcpServerFromPlatform — v6.0 alpha', () => { const server = createAdcpServerFromPlatform(platform, { name: 'signed-reverse-compatibility', version: '1.0.0', - adcpVersion: '3.2.0-beta.5', - capabilities: { supported_versions: ['3.1.18', '3.2.0-beta.5'] }, + adcpVersion: '3.2.0-beta.6', + capabilities: { supported_versions: ['3.1.18', '3.2.0-beta.6'] }, validation: { requests: 'off', responses: 'off' }, legacyCreativeFormatConverter: ({ formatId }) => formatId.id === 'display-300x250' @@ -274,7 +271,7 @@ describe('createAdcpServerFromPlatform — v6.0 alpha', () => { const server = createAdcpServerFromPlatform(platform, { name: 'compact-only', version: '1.0.0', - adcpVersion: '3.2.0-beta.5', + adcpVersion: '3.2.0-beta.6', validation: { requests: 'off', responses: 'off' }, }); @@ -307,7 +304,7 @@ describe('createAdcpServerFromPlatform — v6.0 alpha', () => { const server = createAdcpServerFromPlatform(platform, { name: 'scoped-compact', version: '1.0.0', - adcpVersion: '3.2.0-beta.5', + adcpVersion: '3.2.0-beta.6', validation: { requests: 'off', responses: 'off' }, }); const response = await server.dispatchTestRequest( @@ -344,7 +341,7 @@ describe('createAdcpServerFromPlatform — v6.0 alpha', () => { const server = createAdcpServerFromPlatform(platform, { name: 'anonymous-session', version: '1.0.0', - adcpVersion: '3.2.0-beta.5', + adcpVersion: '3.2.0-beta.6', resolveSessionKey: () => 'anonymous-session', validation: { requests: 'off', responses: 'off' }, }); @@ -372,7 +369,7 @@ describe('createAdcpServerFromPlatform — v6.0 alpha', () => { const server = createAdcpServerFromPlatform(platform, { name: 'compact-replay-auth', version: '1.0.0', - adcpVersion: '3.2.0-beta.5', + adcpVersion: '3.2.0-beta.6', idempotency: createIdempotencyStore({ backend: memoryBackend({ sweepIntervalMs: 0 }) }), resolveIdempotencyPrincipal: () => 'deliberately-shared-principal', resolveSessionKey: () => 'deliberately-shared-session', @@ -421,7 +418,7 @@ describe('createAdcpServerFromPlatform — v6.0 alpha', () => { const server = createAdcpServerFromPlatform(platform, { name: 'refinement-scope', version: '1.0.0', - adcpVersion: '3.2.0-beta.5', + adcpVersion: '3.2.0-beta.6', validation: { requests: 'off', responses: 'off' }, }); const response = await server.dispatchTestRequest( @@ -6152,7 +6149,7 @@ describe('HITL push notification webhook on terminal state', () => { assertMcpWebhookPayloadValid(emit.payload); }); - it('rejects beta.5 webhook registration when operation_id is omitted', async () => { + it('rejects beta.6 webhook registration when operation_id is omitted', async () => { const emits = []; const fakeEmitter = { emit: async params => { @@ -6162,37 +6159,39 @@ describe('HITL push notification webhook on terminal state', () => { }; const platform = buildHitlPlatform(async () => ({ media_buy_id: 'mb_42', status: 'active' })); - const server = createAdcpServerFromPlatform(platform, { - name: 'webhook', - version: '0.0.1', - validation: { requests: 'off', responses: 'off' }, - taskWebhookEmitter: fakeEmitter, - }); - - const result = await server.dispatchTestRequest({ - method: 'tools/call', - params: { - name: 'create_media_buy', - arguments: { - buyer_ref: 'b1', - idempotency_key: '11111111-1111-1111-1111-111111111111', - packages: [], - start_time: '2026-05-01T00:00:00Z', - end_time: '2026-06-01T00:00:00Z', - account: { account_id: 'acc_1' }, - adcp_major_version: 3, - adcp_version: '3.2-beta.5', - push_notification_config: { - url: 'https://buyer.example.com/step/create_media_buy/op_url_must_not_be_parsed', - token: 'webhook-token-1234', + for (const adcpVersion of ['3.2-beta.6']) { + const server = createAdcpServerFromPlatform(platform, { + name: 'webhook', + version: '0.0.1', + adcpVersion: '3.2.0-beta.6', + validation: { requests: 'off', responses: 'off' }, + taskWebhookEmitter: fakeEmitter, + }); + const result = await server.dispatchTestRequest({ + method: 'tools/call', + params: { + name: 'create_media_buy', + arguments: { + buyer_ref: 'b1', + idempotency_key: '11111111-1111-1111-1111-111111111111', + packages: [], + start_time: '2026-05-01T00:00:00Z', + end_time: '2026-06-01T00:00:00Z', + account: { account_id: 'acc_1' }, + adcp_major_version: 3, + adcp_version: adcpVersion, + push_notification_config: { + url: 'https://buyer.example.com/step/create_media_buy/op_url_must_not_be_parsed', + token: 'webhook-token-1234', + }, }, }, - }, - }); + }); - assert.strictEqual(result.isError, true); - assert.strictEqual(result.structuredContent.adcp_error.code, 'INVALID_REQUEST'); - assert.strictEqual(result.structuredContent.adcp_error.field, 'push_notification_config.operation_id'); + assert.strictEqual(result.isError, true); + assert.strictEqual(result.structuredContent.adcp_error.code, 'INVALID_REQUEST'); + assert.strictEqual(result.structuredContent.adcp_error.field, 'push_notification_config.operation_id'); + } assert.strictEqual(emits.length, 0); }); @@ -6626,7 +6625,7 @@ describe('tasks_get wire tool (B9)', () => { assert.strictEqual(status.structuredContent.status, 'submitted'); assert.strictEqual(status.structuredContent.has_webhook, true); assert.strictEqual(status.structuredContent.result, undefined); - assert.strictEqual(status.structuredContent.adcp_version, '3.2-beta.5'); + assert.strictEqual(status.structuredContent.adcp_version, '3.2-beta.6'); const listed = await server.dispatchTestRequest({ method: 'tools/call', @@ -7383,7 +7382,7 @@ describe('createAdcpServerFromPlatform — default resolveIdempotencyPrincipal', { name: 'principal-compat', version: '0.0.1', - adcpVersion: '3.2.0-beta.5', + adcpVersion: '3.2.0-beta.6', idempotency, validation: { requests: 'off', responses: 'off' }, } diff --git a/test/server-wire-safe.test.js b/test/server-wire-safe.test.js index 6ec8749e4..b6616eb41 100644 --- a/test/server-wire-safe.test.js +++ b/test/server-wire-safe.test.js @@ -83,6 +83,28 @@ describe('WIRE_SPEC_FIELDS — codegen output', () => { }); describe('pickWireSpecFields', () => { + it('preserves beta.6 delivery metric narrowing for grading and fan-out', () => { + const fields = WIRE_SPEC_FIELDS.GetMediaBuyDeliveryRequest.fields; + assert.ok(fields.includes('requested_metrics')); + + const safe = pickWireSpecFields( + { + media_buy_ids: ['mb_1'], + requested_metrics: ['viewable_rate', 'quartile_100'], + reporting_dimensions: { + format: { sort_by: 'viewable_rate', sort_direction: 'asc' }, + }, + attacker_field: 'drop-me', + }, + 'GetMediaBuyDeliveryRequest' + ); + assert.deepStrictEqual(safe.requested_metrics, ['viewable_rate', 'quartile_100']); + assert.deepStrictEqual(safe.reporting_dimensions, { + format: { sort_by: 'viewable_rate', sort_direction: 'asc' }, + }); + assert.ok(!('attacker_field' in safe)); + }); + it('preserves flattened protocol-envelope fields from MCP request projections', () => { assert.ok(WIRE_SPEC_FIELDS.ReportUsageRequest.fields.includes('adcp_version')); assert.ok(WIRE_SPEC_FIELDS.ReportUsageRequest.fields.includes('adcp_major_version'));